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

Merge branch 'develop' into jg/batch_decoder_force_close

parents 665f5269 60129ad5
---
'@eth-optimism/contracts-bedrock': patch
---
Optionally print cast commands during migration
---
'@eth-optimism/common-ts': patch
---
Fix BaseServiceV2 configuration for caseCase options
---
'@eth-optimism/atst': patch
---
Update docs
...@@ -66,9 +66,37 @@ jobs: ...@@ -66,9 +66,37 @@ jobs:
- checkout - checkout
- check-changed: - check-changed:
patterns: op-bindings,op-chain-ops,packages/ patterns: op-bindings,op-chain-ops,packages/
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- run: - run:
name: Install dependencies name: Install dependencies
command: yarn && git diff --exit-code command: yarn install && git diff --exit-code
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-v2-{{ checksum "yarn.lock" }}
paths:
- "node_modules"
- "packages/actor-tests/node_modules"
- "packages/atst/node_modules"
- "packages/balance-monitor/node_modules"
- "packages/chain-mon/node_modules"
- "packages/common-ts/node_modules"
- "packages/contracts/node_modules"
- "packages/contracts-bedrock/node_modules"
- "packages/contracts-governance/node_modules"
- "packages/contracts-periphery/node_modules"
- "packages/core-utils/node_modules"
- "packages/data-transport-layer/node_modules"
- "packages/drippie-mon/node_modules"
- "packages/fault-detector/node_modules"
- "packages/hardhat-deploy-config/node_modules"
- "packages/integration-tests-bedrock/node_modules"
- "packages/message-relayer/node_modules"
- "packages/migration-data/node_modules"
- "packages/replica-healthcheck/node_modules"
- "packages/sdk/node_modules"
- run: - run:
name: print forge version name: print forge version
command: forge --version command: forge --version
...@@ -78,8 +106,6 @@ jobs: ...@@ -78,8 +106,6 @@ jobs:
- persist_to_workspace: - persist_to_workspace:
root: "." root: "."
paths: paths:
- "node_modules"
- "packages/*/node_modules"
- "packages/*/dist" - "packages/*/dist"
- "packages/*/artifacts" - "packages/*/artifacts"
- "packages/contracts/src/contract-artifacts.ts" - "packages/contracts/src/contract-artifacts.ts"
...@@ -267,18 +293,12 @@ jobs: ...@@ -267,18 +293,12 @@ jobs:
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed: - check-changed:
patterns: contracts-bedrock,hardhat-deploy-config patterns: contracts-bedrock,hardhat-deploy-config
- run:
name: lint
command: yarn lint:check
working_directory: packages/contracts-bedrock
- run:
name: slither
command: |
slither --version
yarn slither || exit 0
working_directory: packages/contracts-bedrock
- run: - run:
name: print forge version name: print forge version
command: forge --version command: forge --version
...@@ -295,6 +315,29 @@ jobs: ...@@ -295,6 +315,29 @@ jobs:
command: codecov --verbose --clean --flags contracts-bedrock-tests command: codecov --verbose --clean --flags contracts-bedrock-tests
environment: environment:
FOUNDRY_PROFILE: ci FOUNDRY_PROFILE: ci
contracts-bedrock-checks:
docker:
- image: ethereumoptimism/ci-builder:latest
steps:
- checkout
- attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed:
patterns: contracts-bedrock,hardhat-deploy-config
- run:
name: lint
command: yarn lint:check
working_directory: packages/contracts-bedrock
- run:
name: slither
command: |
slither --version
yarn slither || exit 0
working_directory: packages/contracts-bedrock
- run: - run:
name: gas snapshot name: gas snapshot
command: | command: |
...@@ -303,10 +346,6 @@ jobs: ...@@ -303,10 +346,6 @@ jobs:
environment: environment:
FOUNDRY_PROFILE: ci FOUNDRY_PROFILE: ci
working_directory: packages/contracts-bedrock working_directory: packages/contracts-bedrock
- run:
name: validate spacers
command: yarn validate-spacers
working_directory: packages/contracts-bedrock
- run: - run:
name: storage snapshot name: storage snapshot
command: yarn storage-snapshot && git diff --exit-code .storage-layout command: yarn storage-snapshot && git diff --exit-code .storage-layout
...@@ -316,6 +355,23 @@ jobs: ...@@ -316,6 +355,23 @@ jobs:
command: yarn autogen:invariant-docs && git diff --exit-code ./invariant-docs/*.md command: yarn autogen:invariant-docs && git diff --exit-code ./invariant-docs/*.md
working_directory: packages/contracts-bedrock working_directory: packages/contracts-bedrock
contracts-bedrock-validate-spaces:
docker:
- image: ethereumoptimism/ci-builder:latest
steps:
- checkout
- attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed:
patterns: contracts-bedrock,hardhat-deploy-config
- run:
name: validate spacers
command: yarn validate-spacers
working_directory: packages/contracts-bedrock
bedrock-echidna-build: bedrock-echidna-build:
docker: docker:
- image: ethereumoptimism/ci-builder:latest - image: ethereumoptimism/ci-builder:latest
...@@ -349,6 +405,10 @@ jobs: ...@@ -349,6 +405,10 @@ jobs:
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed: - check-changed:
patterns: contracts-bedrock,contracts patterns: contracts-bedrock,contracts
- run: - run:
...@@ -364,6 +424,10 @@ jobs: ...@@ -364,6 +424,10 @@ jobs:
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed: - check-changed:
patterns: contracts-bedrock,op-bindings patterns: contracts-bedrock,op-bindings
- run: - run:
...@@ -389,6 +453,10 @@ jobs: ...@@ -389,6 +453,10 @@ jobs:
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed: - check-changed:
patterns: <<parameters.package_name>>,<<parameters.dependencies>> patterns: <<parameters.package_name>>,<<parameters.dependencies>>
- run: - run:
...@@ -442,6 +510,10 @@ jobs: ...@@ -442,6 +510,10 @@ jobs:
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: "." }
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- check-changed: - check-changed:
patterns: packages patterns: packages
# Note: The below needs to be manually configured whenever we # Note: The below needs to be manually configured whenever we
...@@ -470,10 +542,6 @@ jobs: ...@@ -470,10 +542,6 @@ jobs:
name: Check integration-tests name: Check integration-tests
command: npx depcheck command: npx depcheck
working_directory: integration-tests working_directory: integration-tests
- run:
name: Check two-step-monitor
command: npx depcheck
working_directory: packages/two-step-monitor
go-lint: go-lint:
parameters: parameters:
...@@ -538,7 +606,7 @@ jobs: ...@@ -538,7 +606,7 @@ jobs:
command: | command: |
# Note: We don't use circle CI test splits because we need to split by test name, not by package. There is an additional # Note: We don't use circle CI test splits because we need to split by test name, not by package. There is an additional
# constraint that gotestsum does not currently (nor likely will) accept files from different pacakges when building. # constraint that gotestsum does not currently (nor likely will) accept files from different pacakges when building.
OP_TESTLOG_DISABLE_COLOR=true OP_E2E_DISABLE_PARALLEL=false OP_E2E_USE_HTTP=<<parameters.use_http>> gotestsum \ OP_TESTLOG_DISABLE_COLOR=true OP_E2E_DISABLE_PARALLEL=true OP_E2E_USE_HTTP=<<parameters.use_http>> gotestsum \
--format=standard-verbose --junitfile=/tmp/test-results/<<parameters.module>>_http_<<parameters.use_http>>.xml \ --format=standard-verbose --junitfile=/tmp/test-results/<<parameters.module>>_http_<<parameters.use_http>>.xml \
-- -timeout=20m ./... -- -timeout=20m ./...
working_directory: <<parameters.module>> working_directory: <<parameters.module>>
...@@ -893,6 +961,12 @@ workflows: ...@@ -893,6 +961,12 @@ workflows:
- contracts-bedrock-tests: - contracts-bedrock-tests:
requires: requires:
- yarn-monorepo - yarn-monorepo
- contracts-bedrock-checks:
requires:
- yarn-monorepo
- contracts-bedrock-validate-spaces:
requires:
- yarn-monorepo
- op-bindings-build: - op-bindings-build:
requires: requires:
- yarn-monorepo - yarn-monorepo
......
(The MIT License) (The MIT License)
Copyright 2020-2022 Optimism Copyright 2020-2023 Optimism
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
......
...@@ -24,7 +24,7 @@ If you want to build Optimism, check out the [Protocol Specs](./specs/). ...@@ -24,7 +24,7 @@ If you want to build Optimism, check out the [Protocol Specs](./specs/).
## Community ## Community
General discussion happens most frequently on the [Optimism discord](https://discord.optimism.io). General discussion happens most frequently on the [Optimism discord](https://discord-gateway.optimism.io).
Governance discussion can also be found on the [Optimism Governance Forum](https://gov.optimism.io/). Governance discussion can also be found on the [Optimism Governance Forum](https://gov.optimism.io/).
## Contributing ## Contributing
...@@ -138,7 +138,7 @@ When merging commits to the `develop` branch you MUST include a changeset file i ...@@ -138,7 +138,7 @@ When merging commits to the `develop` branch you MUST include a changeset file i
To add a changeset, run the command `yarn changeset` in the root of this monorepo. To add a changeset, run the command `yarn changeset` in the root of this monorepo.
You will be presented with a small prompt to select the packages to be released, the scope of the release (major, minor, or patch), and the reason for the release. You will be presented with a small prompt to select the packages to be released, the scope of the release (major, minor, or patch), and the reason for the release.
Comments with in changeset files will be automatically included in the changelog of the package. Comments within changeset files will be automatically included in the changelog of the package.
### Triggering Releases ### Triggering Releases
......
# The OP Stack Docs
[![Discord](https://img.shields.io/discord/667044843901681675.svg?color=768AD4&label=discord&logo=https%3A%2F%2Fdiscordapp.com%2Fassets%2F8c9701b98ad4372b58f13fd9f65f966e.svg)](https://discord-gateway.optimism.io)
[![Twitter Follow](https://img.shields.io/twitter/follow/optimismPBC.svg?label=optimismPBC&style=social)](https://twitter.com/optimismPBC)
The OP Stack is an open, collectively maintained development stack for blockchain ecosystems.
This repository contains the source code for the [OP Stack Docs](https://stack.optimism.io).
## Development
### Serving docs locally
```sh
yarn dev
```
Then navigate to [http://localhost:8080](http://localhost:8080).
If that link doesn't work, double check the output of `yarn dev`.
You might already be serving something on port 8080 and the site may be on another port (e.g., 8081).
### Building docs for production
```sh
yarn build
```
You probably don't need to run this command, but now you know.
### Editing docs
Edit the markdown directly in [src/docs](./src/docs).
### Adding new docs
Add your markdown files to [src/docs](./src/docs).
You will also have to update [src/.vuepress/config.js](./src/.vuepress/config.js) if you want these docs to show up in the sidebar.
### Updating the theme
We currently use an ejected version of [vuepress-theme-hope](https://vuepress-theme-hope.github.io/).
Since the version we use was ejected from the original theme, you'll see a bunch of compiled JavaScript files instead of the original TypeScript files.
There's not much we can do about that right now, so you'll just need to make do and edit the raw JS if you need to make theme adjustments.
We're planning to move away from VuePress relatively soon anyway so we won't be fixing this.
{
"name": "@eth-optimism/op-stack-docs",
"version": "0.0.2",
"description": "The OP Stack Docs",
"main": "index.js",
"scripts": {
"dev": "vuepress dev src",
"build": "vuepress build src"
},
"license": "MIT",
"devDependencies": {
"@vuepress/plugin-medium-zoom": "^1.8.2",
"@vuepress/plugin-pwa": "^1.9.7",
"vuepress": "^1.8.2",
"vuepress-plugin-plausible-analytics": "^0.2.1",
"vuepress-theme-hope": "^1.22.0"
},
"dependencies": {
"check-md": "^1.0.2",
"dayjs": "^1.11.7"
}
}
const { description } = require('../../package')
const path = require('path')
module.exports = {
title: 'OP Stack Docs',
description: description,
head: [
['link', { rel: 'manifest', href: '/manifest.json' }],
['meta', { name: 'theme-color', content: '#3eaf7c' }],
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
['meta', { property: 'og:image', content: 'https://stack.optimism.io/assets/logos/twitter-logo.png' }],
['meta', { name: 'twitter:image', content: 'https://stack.optimism.io/assets/logos/twitter-logo.png' }],
['meta', { name: 'twitter:title', content: 'OP Stack Docs' }],
['meta', { property: 'og:title', content: 'OP Stack Docs' }],
['meta', { name: 'twitter:card', content: 'summary' } ],
['link', { rel: "icon", type: "image/png", sizes: "32x32", href: "/assets/logos/favicon.png"}],
],
theme: path.resolve(__dirname, './theme'),
themeConfig: {
"twitter:card": "summary",
contributor: false,
hostname: 'https://stack.optimism.io',
logo: '/assets/logos/logo.png',
docsDir: 'src',
docsRepo: 'https://github.com/ethereum-optimism/opstack-docs',
docsBranch: 'main',
lastUpdated: false,
darkmode: 'disable',
themeColor: false,
blog: false,
iconPrefix: 'far fa-',
pageInfo: false,
pwa: {
cacheHTML: false,
},
activeHash: {
offset: -200,
},
algolia: {
appId: 'O9WKE9RMCV',
apiKey: '00cf17cba30b374d08d7f7afead974be',
indexName: 'optimism'
},
nav: [
{
text: 'Home',
link: 'https://www.optimism.io/'
},
{
text: 'OP Stack Docs',
link: '/'
},
{
text: 'Optimism Docs',
link: 'https://community.optimism.io/'
},
{
text: 'Governance',
link: 'https://community.optimism.io/docs/governance/'
},
{
text: 'Community',
items: [
{
icon: 'discord',
iconPrefix: 'fab fa-',
iconClass: 'color-discord',
text: 'Discord',
link: 'https://discord.optimism.io',
},
{
icon: 'github',
iconPrefix: 'fab fa-',
iconClass: 'color-github',
text: 'GitHub',
link: 'https://github.com/ethereum-optimism/optimism',
},
{
icon: 'twitter',
iconPrefix: 'fab fa-',
iconClass: 'color-twitter',
text: 'Twitter',
link: 'https://twitter.com/optimismFND',
},
{
icon: 'twitch',
iconPrefix: 'fab fa-',
iconClass: 'color-twitch',
text: 'Twitch',
link: 'https://www.twitch.tv/optimismpbc'
},
{
icon: 'medium',
iconPrefix: 'fab fa-',
iconClass: 'color-medium',
text: 'Blog',
link: 'https://optimismpbc.medium.com/'
},
{
icon: 'computer-classic',
iconClass: 'color-ecosystem',
text: 'Ecosystem',
link: 'https://www.optimism.io/apps/all',
},
{
icon: 'globe',
iconClass: 'color-optimism',
text: 'optimism.io',
link: 'https://www.optimism.io/',
}
]
}
],
searchPlaceholder: 'Search the docs',
sidebar: [
{
title: "OP Stack",
collapsable: false,
children: [
'/',
[
'/docs/understand/design-principles.md',
'Design Principles'
],
'/docs/understand/landscape.md',
'/docs/understand/explainer.md'
]
},
{
title: "Releases",
collapsable: false,
children: [
'/docs/releases/',
{
title: "Bedrock",
collapsable: true,
children: [
'/docs/releases/bedrock/',
'/docs/releases/bedrock/explainer.md',
'/docs/releases/bedrock/differences.md'
]
}
]
},
{
title: "Building OP Stack Rollups",
collapsable: false,
children: [
'/docs/build/getting-started.md',
'/docs/build/conf.md',
'/docs/build/explorer.md',
{
title: "OP Stack Hacks",
collapsable: true,
children: [
'/docs/build/hacks.md',
'/docs/build/featured.md',
'/docs/build/data-avail.md',
'/docs/build/derivation.md',
'/docs/build/execution.md',
'/docs/build/settlement.md',
{
title: "Sample Hacks",
children: [
"/docs/build/tutorials/add-attr.md",
"/docs/build/tutorials/new-precomp.md",
]
} // End of tutorials
],
}, // End of OP Stack hacks
],
}, // End of Building OP Stack Rollups
{
title: "Contributing",
collapsable: false,
children: [
'/docs/contribute.md',
]
},
{
title: "Security",
collapsable: false,
children: [
'/docs/security/faq.md',
'/docs/security/policy.md',
]
},
], // end of sidebar
plugins: [
"@vuepress/pwa",
[
'@vuepress/plugin-medium-zoom',
{
selector: ':not(a) > img'
}
],
"plausible-analytics"
]
}
}
// module.exports.themeConfig.sidebar["/docs/useful-tools/"] = module.exports.themeConfig.sidebar["/docs/developers/"]
export default ({ router }) => {
router.addRoutes([
{ path: '/docs/', redirect: '/' },
])
}
{
"name": "OP Docs",
"short_name": "OP Docs",
"description": "The official OP Docs",
"icons": [
{
"src": "/assets/logos/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/logos/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/assets/logos/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/assets/logos/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/assets/logos/icon-1020x1020.png",
"sizes": "any",
"type": "image/png"
}
],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ff0420"
}
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,600;0,700;1,600;1,700&display=swap');
@import 'https://pro.fontawesome.com/releases/v5.15.4/css/all.css';
main, body, html {
font-family: 'Open Sans', sans-serif;
}
p {
font-size: 16px;
line-height: 24px;
}
aside.sidebar {
background-color: #F1F4F9;
border-right: none;
}
p.sidebar-heading {
color: #323A43 !important;
font-family: 'Open Sans', sans-serif;
font-weight: 600;
font-size: 14px !important;
line-height: 24px !important;
min-height: 36px;
margin-left: 32px;
padding: 8px 16px !important;
width: calc(100% - 64px) !important;
border-radius: 8px;
}
a.sidebar-link {
font-family: 'Open Sans', sans-serif;
font-size: 14px !important;
line-height: 24px !important;
min-height: 36px;
margin-left: 32px;
padding: 8px 16px !important;
width: calc(100% - 64px) !important;
border-radius: 8px;
}
section.sidebar-group a.sidebar-link {
margin-left: 44px;
width: calc(100% - 64px) !important;
}
.sidebar-links:not(.sidebar-group-items) > li > a.sidebar-link {
font-weight: 600 !important;
color: #323A43 !important;
}
.sidebar-links:not(.sidebar-group-items) > li > a.sidebar-link.active {
border-left-color: #F1F4F9 !important;
background-color: #FFDBDF !important;
color: #FF0420 !important;
}
a.sidebar-link.active {
border-left-color: #F1F4F9 !important;
background-color: #FFDBDF !important;
color: #FF0420 !important;
}
h1 {
font-size: 50px;
}
h2 {
font-size: 40px;
}
h3 {
font-size: 28px;
}
h4 {
font-size: 20px;
}
h1 {
font-family: 'Rubik', sans-serif;
font-weight: 700;
font-style: italic;
border-bottom: none;
color: #202327 !important;
}
h2, h3, h4 {
font-family: 'Rubik', sans-serif;
border-bottom: none;
font-weight: 400;
}
#search-form {
@media (min-width $MQNormal) {
margin-left: 2rem;
}
}
.search-box {
@media (min-width $MQNormal) {
order: 1;
margin-right: 0;
margin-left: 1rem;
.suggestions {
left: auto !important;
right: 0 !important;
}
}
input {
border-color: #CBD5E0 !important;
border-radius: 100px !important;
background-color: #FFFFFF !important;
}
}
header.navbar {
border-bottom: none;
box-shadow: 0px 6px 8px -6px rgba(20, 23, 26, 0.06), 0px 8px 16px -6px rgba(20, 23, 26, 0.04);
--bgcolor-blur: hsla(0,0%,100%,0.9);
}
span.site-name {
display: none !important;
}
a.nav-link,
div.nav-item span.title {
font-weight: 600;
}
a.nav-link:not(.router-link-active),
div.nav-item span.title {
color: #68778D;
}
.theme-default-content:not(.custom) > p {
text-align: inherit !important;
}
.sidebar {
box-shadow: none !important;
}
div.hero-info {
color: white;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
div.hero-info .description {
display: none;
}
div.hero-info #main-title {
color: white !important;
}
header.hero > img {
border-radius: 16px;
max-height: none !important;
}
header.hero {
margin-top: 20px;
position: relative;
justify-content: space-between !important;
}
.home .features {
border-top: none !important;
justify-content: space-between !important;
margin: 0 !important;
padding-top: 0.5rem !important;
}
.home .features h2 {
font-family: 'Open Sans', sans-serif;
font-style: normal;
font-size: 16px !important;
font-weight: 600 !important;
color: #202327 !important;
}
.home .features p {
font-family: 'Open Sans', sans-serif;
font-size: 14px !important;
color: #68778D !important;
}
.home .features .icon-container {
height: 44px;
width: 44px;
background-color: #FFF0F1;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
color: #FF0420;
}
.home .features .feature {
background-color: #FFFFFF !important;
box-shadow: 0px 6px 8px -6px rgba(20, 23, 26, 0.12), 0px 8px 16px -6px rgba(20, 23, 26, 0.08);
border-radius: 16px !important;
margin: 0 !important;
margin-bottom: 2rem !important;
padding: 1.5rem !important;
}
.features-header {
margin-top: 30px;
margin-bottom: 0px;
}
div.theme-container:not(.has-sidebar) {
background-color: #F1F4F9;
}
.anchor-header {
color: #202327;
font-weight: 600;
font-size: 14px;
font-height: 20px;
margin-bottom: 10px;
}
.anchor-support {
margin-top: 20px;
}
.anchor-support-links i {
width: 20px;
text-align: center;
margin-right: 5px;
}
.anchor-support-links a {
color: #68778D;
}
.anchor-support-links a div {
height: 30px;
font-size: 14px;
}
.anchor-support-links a:hover {
color: #FF0420;
}
#anchor {
width: 15rem !important;
}
#anchor .anchor {
line-height: 27.2px !important;
}
#anchor .anchor div {
color: #68778D !important;
}
#anchor .anchor.active div {
font-weight: 600 !important;
}
.theme-default-content code {
top: 1px;
line-height: 22.4px !important;
vertical-align: middle !important;
}
.nav-dropdown svg.icon.outbound {
display: none;
}
.nav-dropdown .dropdown-item i {
width: 20px;
}
.color-discord {
color: #5865F2;
}
.color-github {
color: #121212;
}
.color-twitter {
color: #1DA1F2;
}
.color-twitch {
color: #6441A5;
}
.color-medium {
color: #000000;
}
.color-optimism {
color: #FF0420;
}
.color-ecosystem {
color: #ea94db;
}
$textColor = #000000
$accentColor = #f01a37
$backgroundColor = #272934
$lightBackgroundColor = #f3f3f3
$sidebarWidth = 320px
import Vue from "vue";
export default Vue.extend({
name: "AlgoliaSearchDropdown",
props: {
options: { type: Object, required: true },
},
data: () => ({
placeholder: "",
}),
watch: {
$lang(newValue) {
this.update(this.options, newValue);
},
options(newValue) {
this.update(newValue, this.$lang);
},
},
mounted() {
this.initialize(this.options, this.$lang);
this.placeholder =
this.$site.themeConfig.searchPlaceholder || "";
},
methods: {
initialize(userOptions, lang) {
void Promise.all([
import(
/* webpackChunkName: "docsearch" */ "docsearch.js/dist/cdn/docsearch.min.js"),
import(
/* webpackChunkName: "docsearch" */ "docsearch.js/dist/cdn/docsearch.min.css"),
]).then(([docsearch]) => {
// eslint-disable-next-line
docsearch.default(Object.assign(Object.assign({}, userOptions), { inputSelector: "#algolia-search-input",
// #697 Make docsearch work well at i18n mode.
algoliaOptions: {
facetFilters: [`lang:${lang}`].concat(
// eslint-disable-next-line
userOptions.facetFilters || []),
}, handleSelected: (_input, _event, suggestion) => {
const { pathname, hash } = new URL(suggestion.url);
const routepath = pathname.replace(this.$site.base, "/");
if (this.$router.getRoutes().some((route) => route.path === routepath))
void this.$router.push(`${routepath}${decodeURIComponent(hash)}`);
else
window.open(suggestion.url);
} }));
});
},
update(options, lang) {
this.$el.innerHTML =
'<input id="algolia-search-input" class="search-query">';
this.initialize(options, lang);
},
},
});
//# sourceMappingURL=Dropdown.js.map
\ No newline at end of file
<template>
<form
id="search-form"
class="algolia-search-wrapper search-box"
role="search"
>
<label class="sr-only" for="algolia-search-input">Algolia search</label>
<input
id="algolia-search-input"
class="search-query"
:placeholder="placeholder"
/>
</form>
</template>
<script src="./Dropdown" />
<style lang="stylus">
.algolia-search-wrapper
& > span
vertical-align middle
.algolia-autocomplete
line-height normal
.ds-dropdown-menu
min-width 515px !important
margin 6px 0 0
padding 4px
border 1px solid var(--light-grey)
border-radius 4px
background var(--bgcolor)
font-size 16px
text-align left
@media (max-width $MQMobile)
min-width calc(100vw - 4rem) !important
max-width calc(100vw - 4rem) !important
&:before
border-color var(--light-grey)
[class*=ds-dataset-]
padding 0
border none
background var(--bgcolor)
.ds-suggestions
margin-top 0
.ds-suggestion
border-bottom 1px solid var(--border-color)
.algolia-docsearch-suggestion--highlight
color var(--accent-color)
.algolia-docsearch-suggestion
padding 0
border-color var(--border-color)
background var(--bgcolor)
color var(--text-color)
.algolia-docsearch-suggestion--category-header
padding 5px 10px
margin-top 0
background var(--accent-color)
color var(--white)
font-weight 600
.algolia-docsearch-suggestion--highlight
background rgba(255, 255, 255, 0.6)
.algolia-docsearch-suggestion--wrapper
padding 0
@media (max-width $MQMobile)
padding 5px 7px 5px 5px !important
.algolia-docsearch-suggestion--title
margin-bottom 0
color var(--text-color)
font-weight 600
.algolia-docsearch-suggestion--subcategory-column
vertical-align top
padding 5px 7px 5px 5px
border-color var(--border-color)
background var(--bgcolor)
color var(--text-color)
@media (min-width $MQMobile)
display table-cell
float none
width 150px
min-width 150px
@media (max-width $MQMobile)
padding 0 !important
background white !important
&:after
display none
.algolia-docsearch-suggestion--subcategory-column-text
color #555
&:after
@media (max-width $MQMobile)
display inline-block
vertical-align middle
content ' > '
width 5px
margin -3px 3px 0
font-size 10px
line-height 14.4px
.algolia-docsearch-suggestion--content
@media (min-width $MQMobile)
display table-cell
float none
vertical-align top
width 100%
.algolia-docsearch-footer
border-color var(--border-color)
.ds-cursor .algolia-docsearch-suggestion--content
background var(--grey3)
color var(--text-color)
</style>
import { createElement } from "preact";
import Vue from "vue";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: docsearch type issue
import docsearch from "@docsearch/js";
export default Vue.extend({
name: "AlgoliaSearchFull",
props: {
options: { type: Object, required: true },
},
watch: {
$lang(newValue) {
this.update(this.options, newValue);
},
options(newValue) {
this.update(newValue, this.$lang);
},
},
mounted() {
this.initialize(this.options, this.$lang);
},
methods: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
initialize(userOptions, _lang) {
// eslint-disable-next-line
docsearch(Object.assign(Object.assign({ container: "#docsearch", placeholder: this.$site.themeConfig.searchPlaceholder || "" }, userOptions), { searchParameters: userOptions.searchParameters || {},
// transform full url to route path
transformItems: (items) => items.map((item) => (Object.assign(Object.assign({}, item), {
// the `item.url` is full url with protocol and hostname
// so we have to transform it to vue-router path
url: this.resolveRoutePathFromUrl(item.url) }))),
// render the hit component with custom `onClick` handler
hitComponent: ({ hit, children }) => createElement("a", {
href: hit.url,
onClick: (event) => {
// We rely on the native link scrolling when user is
// already on the right anchor because Vue Router doesn’t
// support duplicated history entries.
if (this.$route.fullPath === hit.url)
return;
const fullPath = `${window.location.origin}${hit.url}`;
const { pathname: hitPathname } = new URL(fullPath);
// If the hits goes to another page, we prevent the native link behavior
// to leverage the Vue Router loading feature.
if (this.$route.path !== hitPathname)
event.preventDefault();
if (this.$router
.getRoutes()
.some((route) => route.path.replace(/index\.html$/, "") === hitPathname))
void this.$router.push(hit.url);
else
window.open(fullPath);
},
}, children), navigator: {
navigate: ({ itemUrl }) => {
const fullPath = `${window.location.origin}${itemUrl}`;
const { pathname: hitPathname } = new URL(fullPath);
// Vue Router doesn’t handle same-page navigation so we use
// the native browser location API for anchor navigation.
if (this.$route.path === hitPathname)
window.location.assign(fullPath);
else if (this.$router
.getRoutes()
.some((route) => route.path === hitPathname))
void this.$router.push(itemUrl);
else
window.open(fullPath);
},
navigateNewTab({ itemUrl }) {
window.open(itemUrl);
},
navigateNewWindow({ itemUrl }) {
window.open(itemUrl);
},
} }));
},
resolveRoutePathFromUrl(absoluteUrl) {
const { pathname, hash } = new URL(absoluteUrl);
return `${pathname.replace(this.$site.base, "/")}${hash}`;
},
update(options, lang) {
this.$el.innerHTML = '<div id="docsearch"></div>';
this.initialize(options, lang);
},
},
});
//# sourceMappingURL=Full.js.map
\ No newline at end of file
<template>
<div id="docsearch" />
</template>
<script src="./Full" />
<style lang="stylus">
@keyframes fade-in
0%
opacity 0
to
opacity 1
body
--docsearch-spacing 12px
--docsearch-icon-stroke-width 1.4
--docsearch-muted-color #969faf
--docsearch-container-background rgba(101, 108, 133, 0.8)
--docsearch-modal-width 560px
--docsearch-modal-height 600px
--docsearch-modal-shadow inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5), 0 3px 8px 0 #555a64
--docsearch-searchbox-height 56px
--docsearch-searchbox-background #ebedf0
--docsearch-searchbox-focus-background #efeef4
--docsearch-searchbox-shadow inset 0 0 0 2px var(--accent-color)
--docsearch-hit-height 56px
--docsearch-hit-color var(--dark-grey)
--docsearch-hit-shadow 0 1px 3px 0 #d4d9e1
--docsearch-key-gradient linear-gradient(-225deg, #d5dbe4, #f8f8f8)
--docsearch-key-shadow inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, 0.4)
--docsearch-footer-height 44px
--docsearch-footer-shadow 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgba(69, 98, 155, 0.12)
@media (max-width $MQMobile)
--docsearch-searchbox-height 44px
--docsearch-spacing 8px
--docsearch-footer-height 40px
body.theme-dark
--docsearch-container-background rgba(9, 10, 17, 0.8)
--docsearch-modal-shadow inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309
--docsearch-searchbox-background #090a11
--docsearch-searchbox-focus-background lighten($darkBgColor, 10%)
--docsearch-hit-shadow none
--docsearch-key-gradient linear-gradient(-26.5deg, #565862, #31353b)
--docsearch-key-shadow inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, 0.3)
--docsearch-footer-shadow inset 0 1px 0 0 rgba(73, 76, 106, 0.5), 0 -4px 8px 0 rgba(0, 0, 0, 0.2)
--docsearch-muted-color #7f8497
.DocSearch-Button
display inline-flex
justify-content space-between
align-items center
height 36px
margin 0 1rem 0 0.25rem
padding 0.5rem
background var(--docsearch-searchbox-background)
border-radius 40px
color var(--docsearch-muted-color)
font-weight 500
user-select none
outline none
@media (max-width $MQMobile)
margin-right 0
&:active, &:focus, &:hover
background var(--docsearch-searchbox-focus-background)
box-shadow var(--docsearch-searchbox-shadow)
color var(--dark-grey)
&:hover .DocSearch-Search-Icon
@media (max-width $MQMobile)
color var(--accent-color)
.DocSearch-Button-Container
.navbar &
align-items center
display flex
.DocSearch-Search-Icon
width 1rem
height 1rem
margin 0.1rem
color #aaa
stroke-width 3
position relative
.DocSearch-Search-Icon
stroke-width 1.6
.DocSearch-Button-Placeholder
padding 0 12px 0 6px
font-size 1rem
.DocSearch-Button-Keys
.navbar &
display flex
.DocSearch-Button-Key
position relative
top -1px
width 20px
height 18px
margin-right 0.4em
padding-bottom 2px
border-radius 3px
background var(--docsearch-key-gradient)
box-shadow var(--docsearch-key-shadow)
color var(--docsearch-muted-color)
display flex
justify-content center
align-items center
.navbar &
display flex
.navbar
@media (max-width $MQNarrow)
.DocSearch-Button-Key, .DocSearch-Button-KeySeparator, .DocSearch-Button-Placeholder
display none
.DocSearch--active
overflow hidden !important
.DocSearch-Container
position fixed
top 0
left 0
z-index 200
box-sizing border-box
width 100vw
height 100vh
background-color var(--docsearch-container-background)
*
box-sizing border-box
a
text-decoration none
.DocSearch-Link
margin 0
padding 0
border 0
background none
color var(--accent-color)
font inherit
appearance none
cursor pointer
.DocSearch-Modal
position relative
max-width var(--docsearch-modal-width)
margin 60px auto auto
border-radius 6px
background var(--bgcolor)
box-shadow var(--docsearch-modal-shadow)
flex-direction column
@media (max-width $MQNarrow)
width 100%
max-width 100%
height 100vh
margin 0
border-radius 0
box-shadow none
.DocSearch-SearchBar
display flex
padding var(--docsearch-spacing) var(--docsearch-spacing) 0
.DocSearch-Form
align-items center
background var(--docsearch-searchbox-focus-background)
border-radius 8px
display flex
height var(--docsearch-searchbox-height)
padding 0 var(--docsearch-spacing)
position relative
width 100%
.DocSearch-Input
width 80%
height 100%
padding 0 0 0 8px
border 0
background transparent
color var(--text-color)
font inherit
font-size 1.2rem
appearance none
outline none
flex 1
&::placeholder
color var(--docsearch-muted-color)
opacity 1
&::-webkit-search-cancel-button, &::-webkit-search-decoration, &::-webkit-search-results-button, &::-webkit-search-results-decoration
display none
@media (max-width $MQMobile)
font-size 1rem
.DocSearch-LoadingIndicator, .DocSearch-MagnifierLabel, .DocSearch-Reset
margin 0
padding 0
.DocSearch-MagnifierLabel, .DocSearch-Reset
align-items center
color var(--accent-color)
display flex
justify-content center
.DocSearch-Container--Stalled .DocSearch-MagnifierLabel, .DocSearch-LoadingIndicator
display none
.DocSearch-Container--Stalled .DocSearch-LoadingIndicator
align-items center
color var(--accent-color)
display flex
justify-content center
.DocSearch-Reset
animation fade-in 0.1s ease-in forwards
appearance none
background none
border 0
border-radius 50%
color var(--docsearch-icon-color)
cursor pointer
padding 2px
right 0
stroke-width var(--docsearch-icon-stroke-width)
@media screen and (prefers-reduced-motion reduce)
animation none
appearance none
background none
border 0
border-radius 50%
color var(--docsearch-icon-color)
cursor pointer
right 0
stroke-width var(--docsearch-icon-stroke-width)
&[hidden]
display none
&:focus
outline none
&:hover
color var(--accent-color)
.DocSearch-LoadingIndicator svg, .DocSearch-MagnifierLabel svg
height 24px
width 24px
.DocSearch-Cancel
display none
margin-left var(--docsearch-spacing)
padding 0
border 0
background none
color var(--accent-color)
font-family Arial, Helvetica, sans-serif
font-size 1em
font-weight 500
white-space nowrap
user-select none
appearance none
cursor pointer
flex none
outline none
overflow hidden
@media (max-width $MQNarrow)
display inline-block
.DocSearch-Dropdown
max-height calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))
min-height var(--docsearch-spacing)
overflow-y auto
overflow-y overlay
padding 0 var(--docsearch-spacing)
@media (max-width $MQNarrow)
height 100%
max-height unset
& ul
list-style none
margin 0
padding 0
.DocSearch-Label
font-size 0.75em
line-height 1.6em
color var(--docsearch-muted-color)
.DocSearch-Help
color var(--docsearch-muted-color)
font-size 0.9em
margin 0
user-select none
.DocSearch-Title
font-size 1.2em
.DocSearch-Logo
a
display flex
svg
margin-left 8px
color var(--accent-color)
.DocSearch-Hits
&:last-of-type
margin-bottom 24px
mark
background none
color var(--accent-color)
.DocSearch-HitsFooter
margin-bottom var(--docsearch-spacing)
padding var(--docsearch-spacing)
color var(--docsearch-muted-color)
font-size 0.85em
display flex
justify-content center
a
border-bottom 1px solid
color inherit
.DocSearch-Hit
position relative
padding-bottom 4px
border-radius 4px
display flex
a
width 100%
padding-left var(--docsearch-spacing)
border-radius 4px
background var(--bgcolor)
box-shadow var(--docsearch-hit-shadow)
display block
&[aria-selected='true'] a
background-color var(--accent-color)
&[aria-selected='true'] mark
text-decoration underline
.DocSearch-Hit--deleting
opacity 0
transition all 0.25s linear
@media screen and (prefers-reduced-motion reduce)
transition none
.DocSearch-Hit--favoriting
transform scale(0)
transform-origin top center
transition all 0.25s linear
transition-delay 0.25s
@media screen and (prefers-reduced-motion reduce)
transition none
.DocSearch-Hit-source
position sticky
top 0
z-index 10
margin 0 -4px
padding 8px 4px 0
background var(--bgcolor-light)
color var(--accent-color)
font-size 0.85em
font-weight 600
line-height 32px
.DocSearch-Hit-Tree
width 24px
height var(--docsearch-hit-height)
color var(--docsearch-muted-color)
opacity 0.5
stroke-width var(--docsearch-icon-stroke-width)
@media (max-width $MQNarrow)
display none
.DocSearch-Hit-Container
height var(--docsearch-hit-height)
padding 0 var(--docsearch-spacing) 0 0
color var(--docsearch-hit-color)
display flex
flex-direction row
align-items center
.DocSearch-Hit-icon
width 20px
height 20px
color var(--docsearch-muted-color)
stroke-width var(--docsearch-icon-stroke-width)
.DocSearch-Hit-action
width 22px
height 22px
color var(--docsearch-muted-color)
stroke-width var(--docsearch-icon-stroke-width)
display flex
align-items center
svg
display block
width 18px
height 18px
& + &
margin-left 6px
.DocSearch-Hit-action-button
padding 2px
border 0
border-radius 50%
background none
color inherit
cursor pointer
appearance none
&:focus, &:hover
background rgba(0, 0, 0, 0.2)
transition background-color 0.1s ease-in
@media screen and (prefers-reduced-motion reduce)
background rgba(0, 0, 0, 0.2)
transition none
path
fill #fff
svg.DocSearch-Hit-Select-Icon
display none
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-Select-Icon
display block
.DocSearch-Hit-content-wrapper
width 80%
position relative
margin 0 8px
font-weight 500
line-height 1.2em
text-overflow ellipsis
white-space nowrap
display flex
flex 1 1 auto
flex-direction column
justify-content center
overflow-x hidden
.DocSearch-Hit-title
font-size 0.9em
.DocSearch-Hit-path
color var(--docsearch-muted-color)
font-size 0.75em
.DocSearch-Hit[aria-selected='true']
.DocSearch-Hit-action, .DocSearch-Hit-icon, .DocSearch-Hit-path, .DocSearch-Hit-text, .DocSearch-Hit-title, .DocSearch-Hit-Tree, mark
color var(--bgcolor) !important
.DocSearch-ErrorScreen, .DocSearch-NoResults, .DocSearch-StartScreen
width 80%
margin 0 auto
padding 36px 0
font-size 0.9em
text-align center
.DocSearch-Screen-Icon
padding-bottom 12px
color var(--docsearch-muted-color)
.DocSearch-NoResults-Prefill-List
display inline-block
padding-bottom 24px
text-align left
ul
display inline-block
padding 8px 0 0
li
list-style-position inside
list-style-type '» '
.DocSearch-Prefill
appearance none
background none
border 0
border-radius 1em
color var(--accent-color)
cursor pointer
display inline-block
font-size 1em
font-weight 700
padding 0
&:focus, &:hover
outline none
text-decoration underline
.DocSearch-Footer
position relative
z-index 300
width 100%
height var(--docsearch-footer-height)
padding 0 var(--docsearch-spacing)
border-radius 0 0 8px 8px
box-shadow var(--docsearch-footer-shadow)
background var(--bgcolor)
display flex
flex-direction row-reverse
flex-shrink 0
align-items center
justify-content space-between
user-select none
@media (max-width $MQNarrow)
position absolute
bottom 0
border-radius 0
.DocSearch-Commands
margin 0
padding 0
color var(--docsearch-muted-color)
display flex
list-style none
@media (max-width $MQNarrow)
display none
li
display flex
align-items center
&:not(:last-of-type)
margin-right 0.8em
.DocSearch-Commands-Key
width 20px
height 18px
margin-right 0.4em
padding-bottom 2px
border-radius 2px
background var(--docsearch-key-gradient)
box-shadow var(--docsearch-key-shadow)
display flex
justify-content center
align-items center
</style>
import Vue from "vue";
import { isActive } from "@theme/utils/path";
const renderLink = (h, { text, link, level }) => h("RouterLink", {
props: {
to: link,
activeClass: "",
exactActiveClass: "",
},
class: {
"anchor-link": true,
[level ? `heading${level}` : ""]: level,
},
}, [h("div", {}, [text])]);
const renderChildren = (h, { children, route }) => h("ul", { class: "anchor-list" }, children.map((child) => {
const active = isActive(route, `${route.path}#${child.slug}`);
return h("li", { class: { anchor: true, active } }, [
renderLink(h, {
text: child.title,
link: `${route.path}#${child.slug}`,
level: child.level,
}),
]);
}));
export default Vue.extend({
name: "Anchor",
functional: true,
props: {
items: {
type: Array,
default: () => [],
},
},
render(h, { props, parent: { $page, $route } }) {
return h("div", { attrs: { class: "anchor-place-holder" } }, [
h("aside", { attrs: { id: "anchor" } }, [
($page.headers && $page.headers.length)
? h("div", { class: "anchor-header" }, [
"On this page"
])
: null,
h("div", { class: "anchor-wrapper" }, [
props.items.length
? renderChildren(h, {
children: props.items,
route: $route,
})
: $page.headers
? renderChildren(h, {
children: $page.headers,
route: $route,
})
: null,
]),
($page.headers && $page.headers.length)
? h("div", [
h("div", { class: "anchor-header anchor-support" }, [
"Support"
]),
h("div", { class: "anchor-support-links" }, [
h("a", { attrs: { href: "https://discord.optimism.io", target: "_blank" } }, [
h("div", [
h("i", { attrs: { class: "fab fa-discord" } }),
" Discord community "
])
]),
h("a", { attrs: { href: "https://forms.monday.com/forms/055862bfb7f4091be3db2567288296f8?r=use1", target: "_blank" } }, [
h("div", [
h("i", { attrs: { class: "far fa-comment-dots" } }),
" Join the Superchain "
])
]),
h("a", { attrs: { href: "https://github.com/ethereum-optimism/optimism/issues", target: "_blank" } }, [
h("div", [
h("i", { attrs: { class: "fab fa-github" } }),
" Make an issue on GitHub"
])
]),
h("a", { attrs: { href: "https://github.com/ethereum-optimism/optimism/contribute", target: "_blank" } }, [
h("div", [
h("i", { attrs: { class: "far fa-hands-helping" } }),
" Contribute to Optimism"
])
]),
])
])
: null
]),
]);
},
});
//# sourceMappingURL=Anchor.js.map
<script src="./Anchor" />
<style lang="stylus">
$headings = 2 3 4 5 6
.anchor-place-holder
position sticky
top: ($navbarHeight + 2rem)
max-width $contentWidth
margin 0 auto
padding 0 2.5rem
z-index 99
@media (max-width $MQNarrow)
padding 0 1.5rem
& + .theme-default-content:not(.custom)
padding-top 0
#anchor
display none
position absolute
left calc(100% + 0.5rem)
min-width 10rem
max-width 15rem
max-height 85vh
overflow-y scroll
@media (min-width $MQWide)
.has-anchor &
display block
&::-webkit-scrollbar-track-piece
background transparent
&::-webkit-scrollbar
width 3px
&::-webkit-scrollbar-thumb:vertical
background #ddd
.theme-dark &
background #333
.anchor-wrapper
position relative
padding-left 8px
&::before
content ' '
position absolute
top 0
left 0px
bottom 0
width 2px
background var(--border-color)
z-index -1
> .anchor-list
margin 0
.anchor-list
padding-left 0
.anchor
position relative
box-sizing border-box
padding 1px 8px
list-style none
line-height 1.5
&::before
content ' '
position absolute
z-index 2
top 0
bottom 0
left -8px
width 2px
background transparent
&:hover
.anchor-link
color var(--accent-color)
&.active
.anchor-link
color var(--accent-color)
&::before
background var(--accent-color)
.anchor-link
display inline-block
vertical-align middle
position relative
max-width 100%
color var(--light-grey)
> div
text-overflow ellipsis
white-space nowrap
overflow hidden
for $heading in $headings
&.heading{$heading}
padding-left ($heading * 8 - 16) px
font-size: (16 - $heading)px
</style>
import Vue from "vue";
export default Vue.extend({
name: "Clipboard",
props: {
html: { type: String, default: "" },
lang: { type: String, default: "en-US" },
},
data: () => ({
location: "",
}),
computed: {
copyright() {
const { author } = this.$themeConfig;
const content = {
"zh-CN": `${this.html}\n-----\n${author ? `著作权归${author}所有。\n` : ""}链接: ${this.location}`,
"en-US": `${this.html}\n-----\n${author ? `Copyright by ${author}.\n` : ""}Link: ${this.location}`,
"vi-VN": `${this.html}\n-----\n${author ? `bản quyền bởi ${author}.\n` : ""}Liên kết: ${this.location}`,
};
return content[this.lang];
},
},
created() {
if (typeof window !== "undefined")
this.location = window.location.toString();
},
});
//# sourceMappingURL=Clipboard.js.map
\ No newline at end of file
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="copyright" />
</template>
<script src="./Clipboard" />
import Vue from "vue";
import { getSidebarItems } from "@theme/utils/sidebar";
import Navbar from "@theme/components/Navbar/Navbar.vue";
import Sidebar from "@theme/components/Sidebar/Sidebar.vue";
import throttle from "lodash.throttle";
export default Vue.extend({
name: "Common",
components: {
Navbar,
Sidebar,
},
props: {
navbar: { type: Boolean, default: true },
sidebar: { type: Boolean, default: true },
},
data: () => ({
isSidebarOpen: false,
hideNavbar: false,
touchStart: {
clientX: 0,
clientY: 0,
},
}),
computed: {
enableNavbar() {
if (this.navbar === false)
return false;
const { frontmatter } = this.$page;
if (frontmatter.navbar === false || this.$themeConfig.navbar === false)
return false;
return Boolean(this.$title ||
this.$themeConfig.logo ||
this.$themeConfig.repo ||
this.$themeConfig.nav ||
this.$themeLocaleConfig.nav);
},
enableSidebar() {
if (this.sidebar === false)
return false;
return (!this.$frontmatter.home &&
this.$frontmatter.sidebar !== false &&
this.sidebarItems.length !== 0);
},
sidebarItems() {
if (this.sidebar === false)
return [];
return getSidebarItems(this.$page, this.$site, this.$localePath);
},
pageClasses() {
const userPageClass = this.$page.frontmatter.pageClass;
return [
{
"has-navbar": this.enableNavbar,
"has-sidebar": this.enableSidebar,
"has-anchor": this.enableAnchor,
"hide-navbar": this.hideNavbar,
"sidebar-open": this.isSidebarOpen,
},
userPageClass,
];
},
headers() {
return this.getHeader(this.sidebarItems);
},
enableAnchor() {
return (this.$frontmatter.anchorDisplay ||
(this.$themeConfig.anchorDisplay !== false &&
this.$frontmatter.anchorDisplay !== false));
},
},
mounted() {
let lastDistance = 0;
this.$router.afterEach(() => {
this.isSidebarOpen = false;
});
window.addEventListener("scroll", throttle(() => {
const distance = this.getScrollTop();
// scroll down
if (lastDistance < distance && distance > 58) {
if (!this.isSidebarOpen)
this.hideNavbar = true;
// scroll up
}
else
this.hideNavbar = false;
lastDistance = distance;
}, 300));
},
methods: {
/** Get scroll distance */
getScrollTop() {
return (window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0);
},
toggleSidebar(to) {
this.isSidebarOpen = typeof to === "boolean" ? to : !this.isSidebarOpen;
this.$emit("toggle-sidebar", this.isSidebarOpen);
},
// Side swipe
onTouchStart(event) {
this.touchStart = {
clientX: event.changedTouches[0].clientX,
clientY: event.changedTouches[0].clientY,
};
},
onTouchEnd(event) {
const dx = event.changedTouches[0].clientX - this.touchStart.clientX;
const dy = event.changedTouches[0].clientY - this.touchStart.clientY;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40)
if (dx > 0 && this.touchStart.clientX <= 80)
this.toggleSidebar(true);
else
this.toggleSidebar(false);
},
getHeader(items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type === "group") {
const matching = this.getHeader(item.children);
if (matching.length !== 0)
return matching;
}
else if (item.type === "page" &&
item.headers &&
item.path === this.$route.path)
return item.headers;
}
return [];
},
},
});
//# sourceMappingURL=Common.js.map
<template>
<div
class="theme-container"
:class="pageClasses"
@touchstart="onTouchStart"
@touchend="onTouchEnd"
>
<!-- Content -->
<template>
<Navbar v-if="enableNavbar" @toggle-sidebar="toggleSidebar">
<template #start>
<slot name="navbar-start" />
</template>
<template #center>
<slot name="navbar-center" />
</template>
<template #end>
<slot name="navbar-end" />
</template>
</Navbar>
<div class="sidebar-mask" @click="toggleSidebar(false)" />
<Sidebar :items="sidebarItems" @toggle-sidebar="toggleSidebar">
<template #top>
<slot name="sidebar-top" />
</template>
<template #center>
<slot name="sidebar-center" />
</template>
<template #bottom>
<slot name="sidebar-bottom" />
</template>
</Sidebar>
<slot :sidebar-items="sidebarItems" :headers="headers" />
</template>
</div>
</template>
<script src="./Common" />
<style lang="stylus">
.theme-container
min-height 100vh
.sidebar-mask
position fixed
z-index 9
top 0
left 0
width 100vw
height 100vh
display none
.theme-container.sidebar-open &
display block
</style>
import Vue from "vue";
import MyTransition from "@theme/components/MyTransition.vue";
import NavLink from "@theme/components/Navbar/NavLink.vue";
import { navigate } from "@theme/utils/navigate";
export default Vue.extend({
name: "Home",
components: { MyTransition, NavLink },
computed: {
actionLinks() {
const { action } = this.$frontmatter;
if (Array.isArray(action))
return action;
return [action];
},
},
methods: {
navigate(link) {
navigate(link, this.$router, this.$route);
},
},
});
//# sourceMappingURL=Home.js.map
\ No newline at end of file
<template>
<main
:aria-labelledby="$frontmatter.heroText !== null ? 'main-title' : null"
class="home"
>
<header class="hero">
<MyTransition>
<img
v-if="$frontmatter.heroImage"
key="light"
:class="{ light: Boolean($frontmatter.darkHeroImage) }"
:src="$withBase($frontmatter.heroImage)"
:alt="$frontmatter.heroAlt || 'HomeLogo'"
/>
</MyTransition>
<MyTransition>
<img
v-if="$frontmatter.darkHeroImage"
key="dark"
class="dark"
:src="$withBase($frontmatter.darkHeroImage)"
:alt="$frontmatter.heroAlt || 'HomeLogo'"
/>
</MyTransition>
<div class="hero-info">
<MyTransition :delay="0.04">
<h1
v-if="$frontmatter.heroText !== false"
id="main-title"
v-text="$frontmatter.heroText || $title || 'Hello'"
/>
</MyTransition>
<MyTransition :delay="0.08">
<p
class="description"
v-text="
$frontmatter.tagline ||
$description ||
'Welcome to your VuePress site'
"
/>
</MyTransition>
<MyTransition :delay="0.12">
<p v-if="$frontmatter.action" class="action">
<NavLink
v-for="action in actionLinks"
:key="action.text"
:item="action"
class="action-button"
:class="action.type || ''"
/>
</p>
</MyTransition>
</div>
</header>
<MyTransition :delay="0.16">
<div>
<h2 class="features-header">Resources</h2>
<div
v-if="$frontmatter.features && $frontmatter.features.length"
class="features"
>
<template v-for="(feature, index) in $frontmatter.features">
<div
v-if="feature.link"
:key="index"
class="feature link"
:class="`feature${index % 9}`"
tabindex="0"
role="navigation"
@click="navigate(feature.link)"
>
<div class="icon-container">
<i :class="`far fa-${feature.icon}`"></i>
</div>
<h2>{{ feature.title }}</h2>
<p>{{ feature.details }}</p>
</div>
<div
v-else
:key="index"
class="feature"
:class="`feature${index % 9}`"
>
<h2>{{ feature.title }}</h2>
<p>{{ feature.details }}</p>
</div>
</template>
</div>
</div>
</MyTransition>
<MyTransition :delay="0.24">
<Content class="theme-default-content custom" />
</MyTransition>
</main>
</template>
<script src="./Home" />
<style lang="stylus">
.home
display block
max-width $homePageWidth
min-height 100vh - $navbarHeight
padding $navbarHeight 2rem 0
margin 0px auto
overflow-x hidden
@media (max-width $MQNarrow)
min-height 100vh - $navbarMobileHeight
padding-top $navbarMobileHeight
@media (max-width $MQMobileNarrow)
padding-left 1.5rem
padding-right 1.5rem
.hero
text-align center
@media (min-width $MQNarrow)
display flex
justify-content space-evenly
align-items center
text-align left
img
display block
max-width 100%
max-height 320px
margin 0
@media (max-width $MQNarrow)
max-height 280px
margin 3rem auto 1.5rem
@media (max-width $MQMobile)
max-height 240px
margin 2rem auto 1.2rem
@media (max-width $MQMobileNarrow)
max-height 210px
margin 1.5rem auto 1rem
.theme-light &
&.light
display block
&.dark
display none
.theme-dark &
&.light
display none
&.dark
display block
h1
font-size 3rem
@media (max-width $MQMobile)
font-size 2.5rem
@media (max-width $MQMobileNarrow)
font-size 2rem
h1, .description, .action
margin 1.8rem auto
@media (max-width $MQMobile)
margin 1.5rem auto
@media (max-width $MQMobileNarrow)
margin 1.2rem auto
.description
max-width 35rem
color var(--text-color-l40)
font-size 1.6rem
line-height 1.3
@media (max-width $MQMobile)
font-size 1.4rem
@media (max-width $MQMobileNarrow)
font-size 1.2rem
.action-button
display inline-block
margin 0.6rem 0.8rem
padding 1rem 1.5rem
border 2px solid var(--accent-color)
border-radius 2rem
color var(--accent-color)
font-size 1.2rem
transition background 0.1s ease
overflow hidden
@media (max-width $MQMobile)
padding 0.8rem 1.2rem
font-size 1.1rem
@media (max-width $MQMobileNarrow)
font-size 1rem
&:hover
color var(--white)
background-color var(--accent-color)
&.primary
color var(--white)
background-color var(--accent-color)
&:hover
border-color var(--accent-color-l10)
background-color var(--accent-color-l10)
.theme-dark &
&:hover
border-color var(--accent-color-d10)
background-color var(--accent-color-d10)
.features
display flex
flex-wrap wrap
justify-content center
align-items stretch
align-content stretch
margin 0 -2rem
padding 1.2rem 0
border-top 1px solid var(--border-color)
@media (max-width $MQMobileNarrow)
margin 0 -1.5rem
.feature
display flex
flex-direction column
justify-content center
flex-basis calc(33% - 4rem)
margin 0.5rem
padding 0 1.5rem
border-radius 0.5rem
transition transform 0.3s, box-shadow 0.3s
overflow hidden
@media (max-width $MQNarrow)
flex-basis calc(50% - 4rem)
@media (max-width $MQMobile)
font-size 0.95rem
@media (max-width $MQMobileNarrow)
flex-basis calc(100%)
font-size 0.9rem
margin 0.5rem 0
border-radius 0
&.link
cursor pointer
&:hover
box-shadow 0 2px 12px 0 var(--card-shadow-color)
h2
margin-bottom 0.25rem
border-bottom none
color var(--text-color-l10)
font-size 1.25rem
font-weight 500
@media (max-width $MQMobileNarrow)
font-size 1.2rem
p
margin-top 0
color var(--text-color-l25)
{$contentClass}
padding-bottom 1.5rem
@require '~@mr-hope/vuepress-shared/styles/colors.styl'
for $color, $index in $colors
.home .features .feature{$index}
&, .theme-light &
background lighten($color, 90%)
.theme-dark &
background darken($color, 75%)
</style>
import Vue from "vue";
export default Vue.extend({
name: "MyTransition",
props: {
delay: { type: Number, default: 0 },
duration: { type: Number, default: 0.25 },
disable: { type: Boolean, default: false },
},
methods: {
setStyle(items) {
if (this.disable) {
return;
}
items.style.transition = `transform ${this.duration}s ease-in-out ${this.delay}s, opacity ${this.duration}s ease-in-out ${this.delay}s`;
items.style.transform = "translateY(-20px)";
items.style.opacity = "0";
},
unsetStyle(items) {
if (this.disable) {
return;
}
items.style.transform = "translateY(0)";
items.style.opacity = "1";
},
},
});
//# sourceMappingURL=MyTransition.js.map
\ No newline at end of file
<template>
<transition
name="drop"
appear
@appear="setStyle"
@after-appear="unsetStyle"
@enter="setStyle"
@after-enter="unsetStyle"
@before-leave="setStyle"
>
<slot />
</transition>
</template>
<script src="./MyTransition" />
<style lang="stylus">
.drop-enter, .drop-leave-to
opacity 0
transform translateY(-20px)
</style>
import Vue from "vue";
import NavLink from "@theme/components/Navbar/NavLink.vue";
export default Vue.extend({
name: "DropdownLink",
components: { NavLink },
props: {
item: { type: Object, required: true },
},
data: () => ({
open: false,
}),
computed: {
dropdownAriaLabel() {
return this.item.ariaLabel || this.item.text;
},
iconPrefix() {
const { iconPrefix } = this.$themeConfig;
return iconPrefix === "" ? "" : iconPrefix || "icon-";
},
},
watch: {
$route() {
this.open = false;
},
},
methods: {
setOpen(value) {
this.open = value;
},
handleDropdown(event) {
const isTriggerByTab = event.detail === 0;
if (isTriggerByTab)
this.setOpen(!this.open);
},
isLastItemOfArray(item, array) {
if (Array.isArray(array))
return item === array[array.length - 1];
return false;
},
},
});
//# sourceMappingURL=DropdownLink.js.map
\ No newline at end of file
<template>
<div class="dropdown-wrapper" :class="{ open }">
<button
class="dropdown-title"
type="button"
:aria-label="dropdownAriaLabel"
@click="handleDropdown"
>
<slot name="title">
<span class="title">
<i v-if="item.icon" :class="`iconfont ${iconPrefix}${item.icon}`" />
{{ item.text }}
</span>
</slot>
<span class="arrow" />
</button>
<ul class="nav-dropdown">
<li
v-for="(child, index) in item.items"
:key="child.link || index"
class="dropdown-item"
>
<template v-if="child.type === 'links'">
<h4 class="dropdown-subtitle">
<NavLink
v-if="child.link"
:item="child"
@focusout="
isLastItemOfArray(child, item.children) &&
child.children.length === 0 &&
setOpen(false)
"
/>
<span v-else>{{ child.text }}</span>
</h4>
<ul class="dropdown-subitem-wrapper">
<li
v-for="grandchild in child.items"
:key="grandchild.link"
class="dropdown-subitem"
>
<NavLink
:item="grandchild"
@focusout="
isLastItemOfArray(grandchild, child.items) &&
isLastItemOfArray(child, item.items) &&
setOpen(false)
"
/>
</li>
</ul>
</template>
<NavLink
v-else
:item="child"
@focusout="isLastItemOfArray(child, item.items) && setOpen(false)"
/>
</li>
</ul>
</div>
</template>
<script src="./DropdownLink" />
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/arrow'
@require '~@mr-hope/vuepress-shared/styles/reset'
.dropdown-wrapper
height 1.8rem
cursor pointer
&:not(:hover)
.arrow
transform rotate(-180deg)
&:hover, &.open
.nav-dropdown
z-index 2
transform scale(1)
visibility visible
opacity 1
.dropdown-title
button()
cursor inherit
padding inherit
color var(--dark-grey)
font-family inherit
font-size 0.9rem
font-weight 500
line-height 1.4rem
&::after
border-left 5px solid var(--accent-color)
&:hover
border-color transparent
.arrow
arrow()
font-size 1.2em
.nav-dropdown
box-sizing border-box
position absolute
top 100%
right 0
max-height 100vh - $navbarHeight
margin 0
padding 0.6rem 0
border 1px solid var(--grey14)
border-radius 0.25rem
background var(--bgcolor)
box-shadow 2px 2px 10px var(--card-shadow-color)
text-align left
white-space nowrap
overflow-y auto
transform scale(0.8)
opacity 0
visibility hidden
transition all 0.18s ease-out
.dropdown-item
color inherit
line-height 1.7rem
h4
margin 0
padding 0.75rem 1rem 0.25rem 0.75rem
border-top 1px solid var(--grey14)
color var(--dark-grey)
font-size 0.9rem
.nav-link
padding 0
&:before
display none
&:first-child h4
padding-top 0
border-top 0
.nav-link
display block
position relative
margin-bottom 0
padding 0 1.5rem 0 1.25rem
border-bottom none
color var(--dark-grey)
font-weight 400
line-height 1.7rem
&:hover
color var(--accent-color)
&.active
color var(--accent-color)
&::before
content ''
position absolute
top calc(50% - 3px)
left 9px
width 0
height 0
border-top 3px solid transparent
border-left 5px solid var(--accent-color)
border-bottom 3px solid transparent
.dropdown-subitem-wrapper
padding 0
list-style none
.dropdown-subitem
font-size 0.9em
</style>
import Vue from "vue";
import DropdownLink from "@theme/components/Navbar/DropdownLink.vue";
import I18nIcon from "@theme/icons/I18nIcon.vue";
import NavLink from "@theme/components/Navbar/NavLink.vue";
import { getNavLinkItem } from "@theme/utils/navbar";
export default Vue.extend({
name: "LanguageDropdown",
components: { NavLink, DropdownLink },
computed: {
dropdown() {
const { locales } = this.$site;
if (locales && Object.keys(locales).length > 1) {
const currentLink = this.$page.path;
const { routes } = this.$router.options;
const themeLocales = this.$themeConfig.locales || {};
const languageDropdown = {
text: this.$themeLocaleConfig.selectText || "Languages",
ariaLabel: this.$themeLocaleConfig.ariaLabel || "Select language",
items: Object.keys(locales).map((path) => {
const locale = locales[path];
const text = (themeLocales[path] && themeLocales[path].label) ||
locale.lang ||
"Unknown Language";
let link;
// Stay on the current page
if (locale.lang === this.$lang)
link = currentLink;
else {
// Try to stay on the same page
link = currentLink.replace(this.$localeConfig.path, path);
// Fallback to homepage
if (!(routes || []).some((route) => route.path === link))
link = path;
}
return { text, link };
}),
};
return getNavLinkItem(languageDropdown);
}
return false;
},
},
render(h) {
return this.dropdown
? h("div", { class: "nav-links" }, [
h("div", { class: "nav-item" }, [
h(DropdownLink, { props: { item: this.dropdown } }, [
h(I18nIcon, {
slot: "title",
style: {
width: "1rem",
height: "1rem",
verticalAlign: "middle",
marginLeft: "1rem",
},
}),
]),
]),
])
: null;
},
});
//# sourceMappingURL=LanguageDropdown.js.map
\ No newline at end of file
import Vue from "vue";
import { ensureExt, isExternal, isMailto, isTel } from "@theme/utils/path";
export default Vue.extend({
name: "NavLink",
props: {
item: { type: Object, required: true },
},
computed: {
link() {
return ensureExt(this.item.link);
},
iconPrefix() {
const { iconPrefix } = this.$themeConfig;
return iconPrefix === "" ? "" : iconPrefix || "icon-";
},
active() {
// link is home path
if ((this.$site.locales &&
Object.keys(this.$site.locales).some((rootLink) => rootLink === this.link)) ||
this.link === "/")
// exact match
return this.$route.path === this.link;
// inclusive match
return this.$route.path.startsWith(this.link);
},
isNonHttpURI() {
return isMailto(this.link) || isTel(this.link);
},
isBlankTarget() {
return this.target === "_blank";
},
isInternal() {
return !isExternal(this.link) && !this.isBlankTarget;
},
target() {
if (this.isNonHttpURI)
return null;
if (this.item.target)
return this.item.target;
return isExternal(this.link) ? "_blank" : "";
},
rel() {
if (this.isNonHttpURI)
return null;
if (this.item.rel === false)
return null;
if (this.item.rel)
return this.item.rel;
return this.isBlankTarget ? "noopener noreferrer" : null;
},
},
methods: {
focusoutAction() {
// eslint-disable-next-line vue/require-explicit-emits
this.$emit("focusout");
},
},
});
//# sourceMappingURL=NavLink.js.map
\ No newline at end of file
<template>
<!-- eslint-disable vue/no-deprecated-v-on-native-modifier -->
<RouterLink
v-if="isInternal"
class="nav-link"
:class="{ active }"
:to="link"
@focusout.native="focusoutAction"
>
<i v-if="item.icon" :class="`iconfont ${item.iconPrefix || iconPrefix}${item.icon} ${item.iconClass}`" />
{{ item.text }}
</RouterLink>
<a
v-else
class="nav-link external"
:href="link"
:target="target"
:rel="rel"
@focusout="focusoutAction"
>
<i v-if="item.icon" :class="`iconfont ${item.iconPrefix || iconPrefix}${item.icon} ${item.iconClass}`" />
{{ item.text }}
<OutboundLink v-if="isBlankTarget" />
</a>
</template>
<script src="./NavLink" />
<style lang="stylus">
.nav-link
line-height 1.4rem
.navbar &
color var(--dark-grey)
&.active
color var(--accent-color)
.sidebar &
color var(--text-color)
&:hover, &.active
color var(--accent-color)
</style>
import Vue from "vue";
import DropdownLink from "@theme/components/Navbar/DropdownLink.vue";
import NavLink from "@theme/components/Navbar/NavLink.vue";
import { getNavLinkItem } from "@theme/utils/navbar";
export default Vue.extend({
name: "NavLinks",
components: {
DropdownLink,
NavLink,
},
computed: {
navLinks() {
const navbar = this.$themeLocaleConfig.nav || this.$themeConfig.nav || [];
return navbar.map((link) => getNavLinkItem(link));
},
},
});
//# sourceMappingURL=NavLinks.js.map
\ No newline at end of file
<template>
<nav class="nav-links">
<!-- user links -->
<div v-for="item in navLinks" :key="item.link" class="nav-item">
<DropdownLink v-if="item.type === 'links'" :item="item" />
<NavLink v-else :item="item" />
</div>
</nav>
</template>
<script src="./NavLinks" />
<style lang="stylus">
.nav-links
display inline-block
.nav-item
position relative
display inline-block
line-height 2rem
margin-left 1.5rem
&:first-child
margin-left 0
> .nav-link
color var(--dark-grey)
&::after
position absolute
content ' '
left 50%
right 50%
bottom 0px
height 2px
background var(--accent-color-l10)
border-radius 1px
visibility hidden
transition left 0.2s ease-in-out, right 0.2s ease-in-out
&.active
color var(--accent-color)
&:hover, &.active
&::after
left 0
right 0
visibility visible
</style>
import Vue from "vue";
import AlgoliaSearchBox from "@AlgoliaSearchBox";
import LanguageDropdown from "@theme/components/Navbar/LanguageDropdown";
import NavLinks from "@theme/components/Navbar/NavLinks.vue";
import RepoLink from "@theme/components/Navbar/RepoLink.vue";
import SearchBox from "@SearchBox";
import SidebarButton from "@theme/components/Navbar/SidebarButton.vue";
import ThemeColor from "@ThemeColor";
let handler;
const css = (el, property) => {
// NOTE: Known bug, will return 'auto' if style value is 'auto'
const window = el.ownerDocument.defaultView;
// `null` means not to return pseudo styles
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return window.getComputedStyle(el, null)[property];
};
export default Vue.extend({
name: "Navbar",
components: {
AlgoliaSearchBox,
LanguageDropdown,
NavLinks,
RepoLink,
SearchBox,
SidebarButton,
ThemeColor,
},
data: () => ({
linksWrapMaxWidth: 0,
isMobile: false,
}),
computed: {
siteBrandTitle() {
return this.$site.title;
},
canHideSiteBrandTitle() {
return (Boolean(this.siteBrandTitle) &&
this.$themeConfig.hideSiteTitleonMobile !== false);
},
siteBrandLogo() {
const { logo } = this.$themeConfig;
return logo ? this.$withBase(logo) : "";
},
siteBrandDarkLogo() {
const { darkLogo } = this.$themeConfig;
return darkLogo ? this.$withBase(darkLogo) : "";
},
algoliaConfig() {
return (this.$themeLocaleConfig.algolia || this.$themeConfig.algolia || false);
},
isAlgoliaSearch() {
return Boolean(this.algoliaConfig &&
this.algoliaConfig.apiKey &&
this.algoliaConfig.indexName);
},
canHide() {
const autoHide = this.$themeConfig.navAutoHide;
return autoHide !== "none" && (autoHide === "always" || this.isMobile);
},
},
mounted() {
// Refer to config.styl
const MOBILE_DESKTOP_BREAKPOINT = 719;
const NAVBAR_HORIZONTAL_PADDING = parseInt(css(this.$el, "paddingLeft")) +
parseInt(css(this.$el, "paddingRight"));
handler = () => {
if (document.documentElement.clientWidth < MOBILE_DESKTOP_BREAKPOINT) {
this.isMobile = true;
this.linksWrapMaxWidth = 0;
}
else {
this.isMobile = false;
this.linksWrapMaxWidth =
this.$el.offsetWidth -
NAVBAR_HORIZONTAL_PADDING -
((this.$refs.siteInfo &&
this.$refs.siteInfo.$el &&
this.$refs.siteInfo.$el.offsetWidth) ||
0);
}
};
handler();
window.addEventListener("resize", handler);
window.addEventListener("orientationchange", handler);
},
// eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle
beforeDestroy() {
window.removeEventListener("resize", handler);
window.removeEventListener("orientationchange", handler);
},
});
//# sourceMappingURL=Navbar.js.map
\ No newline at end of file
<template>
<header class="navbar" :class="{ 'can-hide': canHide }">
<slot name="start" />
<SidebarButton @toggle-sidebar="$emit('toggle-sidebar')" />
<RouterLink ref="siteInfo" :to="$localePath" class="home-link">
<img
v-if="siteBrandLogo"
class="logo"
:class="{ light: Boolean(siteBrandDarkLogo) }"
:src="siteBrandLogo"
:alt="siteBrandTitle"
/>
<img
v-if="siteBrandDarkLogo"
class="logo dark"
:src="siteBrandDarkLogo"
:alt="siteBrandTitle"
/>
<span
v-if="siteBrandTitle"
class="site-name"
:class="{ 'can-hide': canHideSiteBrandTitle }"
>{{ siteBrandTitle }}</span
>
</RouterLink>
<slot name="center" />
<div
:style="
linksWrapMaxWidth ? { 'max-width': `${linksWrapMaxWidth}px` } : {}
"
class="links"
>
<ThemeColor />
<AlgoliaSearchBox v-if="isAlgoliaSearch" :options="algoliaConfig" />
<SearchBox
v-else-if="
$themeConfig.search !== false && $page.frontmatter.search !== false
"
/>
<NavLinks class="can-hide" />
<LanguageDropdown />
<RepoLink class="can-hide" />
<slot name="end" />
</div>
</header>
</template>
<script src="./Navbar" />
<style lang="stylus">
.navbar
position fixed
z-index 200
top 0
left 0
right 0
height $navbarHeight
padding $navbarVerticalPadding $navbarHorizontalPadding
background var(--bgcolor-blur)
box-sizing border-box
box-shadow 0 2px 8px var(--card-shadow-color)
backdrop-filter saturate(200%) blur(20px)
line-height: $navbarHeight - $navbarVerticalPadding * 2
transition transform 0.3s ease-in-out
@media (max-width $MQMedium)
height $navbarMobileHeight
padding $navbarMobileVerticalPadding $navbarMobileHorizontalPadding
padding-left: $navbarMobileHorizontalPadding + 2.4rem
line-height: $navbarMobileHeight - $navbarMobileVerticalPadding * 2
.hide-navbar &.can-hide
transform translateY(-100%)
a, span, img
display inline-block
.logo
min-width: 200px
height: $navbarHeight - $navbarVerticalPadding * 2
margin-right 0.8rem
vertical-align top
@media (max-width $MQMedium)
min-width: $navbarMobileHeight - $navbarMobileVerticalPadding * 2
height: $navbarMobileHeight - $navbarMobileVerticalPadding * 2
.theme-light &
&.light
display inline-block
&.dark
display none
.theme-dark &
&.light
display none
&.dark
display inline-block
.can-hide
@media (max-width $MQMedium)
display none
.site-name
font-size 1.5rem
color var(--text-color)
position relative
@media (max-width $MQMedium)
width calc(100vw - 9.4rem)
overflow hidden
white-space nowrap
text-overflow ellipsis
.links
position absolute
top $navbarVerticalPadding
right $navbarHorizontalPadding
display flex
box-sizing border-box
padding-left 1.5rem
font-size 0.9rem
white-space nowrap
@media (max-width $MQMedium)
padding-left 0
top $navbarMobileVerticalPadding
right $navbarMobileHorizontalPadding
</style>
import Vue from "vue";
export default Vue.extend({
name: "RepoLink",
computed: {
repoLink() {
const { repo } = this.$themeConfig;
if (repo)
return /^https?:/u.test(repo) ? repo : `https://github.com/${repo}`;
return "";
},
repoLabel() {
if (!this.repoLink)
return "";
if (this.$themeConfig.repoLabel)
return this.$themeConfig.repoLabel;
const [repoHost] = /^https?:\/\/[^/]+/u.exec(this.repoLink) || [""];
const platforms = ["GitHub", "GitLab", "Bitbucket"];
for (let index = 0; index < platforms.length; index++) {
const platform = platforms[index];
if (new RegExp(platform, "iu").test(repoHost))
return platform;
}
return "Source";
},
},
});
//# sourceMappingURL=RepoLink.js.map
\ No newline at end of file
<template>
<!-- repo link -->
<a
v-if="repoLink && $themeConfig.repoDisplay !== false"
class="repo-link"
rel="noopener noreferrer"
:href="repoLink"
target="_blank"
>
{{ repoLabel }}
<OutboundLink />
</a>
</template>
<script src="./RepoLink" />
<style lang="stylus">
.repo-link
.navbar &
color var(--dark-grey)
margin-left 1rem
.sidebar-nav-links &
display block
padding 0.5rem 0 0.5rem 1.5rem
font-size 1.1em
line-height 1.25rem
</style>
<template>
<button
class="sidebar-button"
title="Sidebar Button"
@click="$emit('toggle-sidebar')"
>
<span class="icon" />
</button>
</template>
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/reset'
.sidebar-button
button()
display none
box-sizing content-box
position absolute
top calc(50% - 1.075rem)
left 1rem
width 1.25rem
height 1.25rem
padding 0.45rem
font unset
vertical-align middle
transition transform 0.2s ease-in-out
&::before
content ' '
margin-top 0.125em
&::after
content ' '
margin-bottom 0.125em
.icon
margin 0.2em 0
&::before, &::after, .icon
display block
width 100%
height 0.2em
transition transform 0.2s ease-in-out
border-radius 0.05em
background var(--text-color)
.sidebar-open .sidebar-button
&::before
transform translateY(0.4em) rotate(135deg)
.icon
transform scale(0)
&::after
transform translateY(-0.4em) rotate(-135deg)
@media (max-width $MQMedium)
.sidebar-button
display block
</style>
import Vue from "vue";
import Anchor from "@theme/components/Anchor.vue";
import Comment from "@Comment";
import MyTransition from "@theme/components/MyTransition.vue";
import PageInfo from "@mr-hope/vuepress-plugin-comment/lib/client/PageInfo.vue";
import PageMeta from "@theme/components/PageMeta.vue";
import PageNav from "@theme/components/PageNav.vue";
export default Vue.extend({
name: "Page",
components: {
Anchor,
Comment,
MyTransition,
PageInfo,
PageMeta,
PageNav,
},
props: {
sidebarItems: {
type: Array,
default: () => [],
},
headers: {
type: Array,
default: () => [],
},
},
});
//# sourceMappingURL=Page.js.map
<template>
<main class="page">
<BreadCrumb :key="$route.path" />
<slot name="top" />
<PageInfo :key="$route.path" />
<template>
<MyTransition :delay="0.12" :disable="true">
<Anchor :key="$route.path" />
</MyTransition>
<slot name="content-top" />
<MyTransition :delay="0.08" :disable="true">
<Content :key="$route.path" class="theme-default-content" />
</MyTransition>
<slot name="content-bottom" />
<MyTransition :delay="0.12" :disable="true">
<PageMeta :key="$route.path" />
</MyTransition>
<MyTransition :delay="0.14" :disable="true">
<PageNav :key="$route.path" v-bind="{ sidebarItems }" />
</MyTransition>
<MyTransition :delay="0.16" :disable="true">
<Comment :key="$route.path" />
</MyTransition>
</template>
<slot name="bottom" />
<!-- Google tag (gtag.js) -->
<!-- put here because the plugin didn't work -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-22HX148PZF">
</script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-9KLVB8X0ME');
</script>
</main>
</template>
<script src="./Page" />
<style lang="stylus">
.page
display block
box-sizing border-box
min-height 100vh
padding-left $sidebarWidth
padding-bottom 2rem
background var(--bgcolor)
@media (max-width $MQMobile)
min-height 100vh
// narrow desktop / iPad
@media (max-width $MQNarrow)
padding-left $mobileSidebarWidth
// wide mobile
@media (max-width $MQMobile)
padding-left 0
@media (min-width $MQMobile)
.theme-container:not(.has-sidebar) &
padding-left 0
@media (min-width $MQWide)
.has-anchor &:not(.blog)
padding-right 16rem
</style>
import Vue from "vue";
import EditIcon from "@theme/icons/EditIcon.vue";
import { endingSlashRE, outboundRE } from "@theme/utils/path";
export default Vue.extend({
name: "PageMeta",
components: { EditIcon },
computed: {
i18n() {
return (this.$themeLocaleConfig.meta || {
contributor: "Contributors",
editLink: "Edit this page",
updateTime: "Last Updated",
});
},
contributors() {
return this.$page.frontmatter.contributor === false ||
(this.$themeConfig.contributor === false &&
!this.$page.frontmatter.contributor)
? []
: this.$page.contributors || [];
},
contributorsText() {
return this.i18n.contributor;
},
updateTime() {
return this.$page.frontmatter.contributor === false ||
(this.$themeConfig.updateTime === false &&
!this.$page.frontmatter.updateTime)
? ""
: this.$page.updateTime || "";
},
updateTimeText() {
return this.i18n.updateTime;
},
editLink() {
const showEditLink = this.$page.frontmatter.editLink ||
(this.$themeConfig.editLinks !== false &&
this.$page.frontmatter.editLink !== false);
const { repo, docsRepo } = this.$site.themeConfig;
if (showEditLink && (repo || docsRepo) && this.$page.relativePath)
return this.createEditLink();
return false;
},
editLinkText() {
return this.i18n.editLink;
},
},
methods: {
createEditLink() {
const { repo = "", docsRepo = repo, docsDir = "", docsBranch = "main", } = this.$themeConfig;
const bitbucket = /bitbucket.org/u;
if (bitbucket.test(docsRepo))
return `${docsRepo.replace(endingSlashRE, "")}/src/${docsBranch}/${docsDir ? `${docsDir.replace(endingSlashRE, "")}/` : ""}${this.$page.relativePath}?mode=edit&spa=0&at=${docsBranch}&fileviewer=file-view-default`;
const gitlab = /gitlab.com/u;
if (gitlab.test(docsRepo))
return `${docsRepo.replace(endingSlashRE, "")}/-/edit/${docsBranch}/${docsDir ? `${docsDir.replace(endingSlashRE, "")}/` : ""}${this.$page.relativePath}`;
const base = outboundRE.test(docsRepo)
? docsRepo
: `https://github.com/${docsRepo}`;
return `${base.replace(endingSlashRE, "")}/edit/${docsBranch}/${docsDir ? `${docsDir.replace(endingSlashRE, "")}/` : ""}${this.$page.relativePath}`;
},
},
});
//# sourceMappingURL=PageMeta.js.map
\ No newline at end of file
<template>
<footer class="page-meta">
<div class="footer-box">
<div class="footer-box-area">
<span class="footer-section-header">About this page</span>
<ul>
<li>Updated: {{ updateTime.slice(0, updateTime.length - 6) }}</li>
</ul>
</div>
<div class="footer-box-area">
<span class="footer-section-header">Contribute</span>
<ul>
<li>
<a :href="editLink" target="_blank" rel="noopener noreferrer">
<i class="far fa-pencil"></i> Edit this page
</a>
</li>
<li>
<a href="https://github.com/ethereum-optimism/optimism/contribute" target="_blank" rel="noopener noreferrer">
<i class="far fa-hands-helping"></i> Contribute to Optimism
</a>
</li>
</ul>
</div>
<div class="footer-box-area">
<span class="footer-section-header">Still need help?</span>
<ul>
<li>
<a href="https://discord.optimism.io" target="_blank" rel="noopener noreferrer">
<i class="fab fa-discord"></i> Discord community
</a>
</li>
</ul>
</div>
</div>
</footer>
</template>
<script src="./PageMeta" />
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/wrapper'
.page-meta
@extend $wrapper
padding-top 12px
padding-bottom 12px
font-family Arial, Helvetica, sans-serif
overflow auto
.meta-item
.label
font-weight 500
color var(--text-color-l25)
.info
font-weight 400
color var(--dark-grey)
.edit-link
display inline-block
font-size 14px
.icon
position relative
bottom -0.125em
width 1em
height 1em
color var(--accent-color)
@media (max-width $MQMobile)
margin-bottom 8px
a
color var(--accent-color-l10)
.update-time
float right
font-size 14px
@media (max-width $MQMobile)
float none
font-size 13px
text-align left
.contributors
font-size 14px
text-align right
@media (max-width $MQMobile)
font-size 13px
text-align left
.footer-box
display flex
flex-direction row
justify-content space-between
background-color #F1F4F9
padding 32px
padding-left: 40px;
padding-right: 40px;
border-radius 16px
@media (max-width $MQNarrow)
flex-direction column
.footer-box-area
@media (max-width $MQNarrow)
margin-bottom 32px
span.footer-section-header
font-family 'Open Sans', sans-serif
font-weight 600
font-size 14px
line-height 20px
ul
list-style-type none
padding-left 0
font-size 14px
line-height 20px
margin-top 10px
margin-bottom 0px
color #68778D
li
margin-top 15px
margin-bottom 5px
a
color #68778D
font-family 'Open Sans', sans-serif
&:hover
color #FF0420
i
font-size 14px
width 20px
margin-right 3px
text-align center
</style>
import Vue from "vue";
import NextIcon from "@theme/icons/NextIcon.vue";
import PrevIcon from "@theme/icons/PrevIcon.vue";
import { resolvePath } from "@theme/utils/path";
import { resolvePageforSidebar } from "@theme/utils/sidebar";
const getSidebarItems = (items, result) => {
for (const item of items)
if (item.type === "group")
getSidebarItems((item.children || []), result);
else
result.push(item);
};
const find = (page, items, offset) => {
const result = [];
getSidebarItems(items, result);
for (let i = 0; i < result.length; i++) {
const cur = result[i];
if (cur.type === "page" && cur.path === decodeURIComponent(page.path))
return result[i + offset];
}
return false;
};
const resolvePageLink = (linkType, { themeConfig, page, route, site, sidebarItems }) => {
const themeLinkConfig =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
themeConfig[`${linkType}Links`];
const pageLinkConfig = page.frontmatter[linkType];
if (themeLinkConfig === false || pageLinkConfig === false)
return false;
if (typeof pageLinkConfig === "string")
return resolvePageforSidebar(site.pages, resolvePath(pageLinkConfig, route.path));
return find(page, sidebarItems, linkType === "prev" ? -1 : 1);
};
export default Vue.extend({
name: "PageNav",
components: { NextIcon, PrevIcon },
props: {
sidebarItems: {
type: Array,
default: () => [],
},
},
computed: {
prev() {
return resolvePageLink("prev", {
sidebarItems: this.sidebarItems,
themeConfig: this.$themeConfig,
page: this.$page,
route: this.$route,
site: this.$site,
});
},
next() {
return resolvePageLink("next", {
sidebarItems: this.sidebarItems,
themeConfig: this.$themeConfig,
page: this.$page,
route: this.$route,
site: this.$site,
});
},
},
});
//# sourceMappingURL=PageNav.js.map
\ No newline at end of file
<template>
<div v-if="prev || next" class="page-nav">
<p class="inner">
<span v-if="prev" class="prev">
<a
v-if="prev.type === 'external'"
class="prev"
:href="prev.path"
target="_blank"
rel="noopener noreferrer"
>
<PrevIcon />
{{ prev.title || prev.path }}
<OutboundLink />
</a>
<RouterLink v-else class="prev" :to="prev.path">
<PrevIcon />
{{ prev.title || prev.path }}
</RouterLink>
</span>
<span v-if="next" class="next">
<a
v-if="next.type === 'external'"
:href="next.path"
target="_blank"
rel="noopener noreferrer"
>
{{ next.title || next.path }}
<OutboundLink />
<NextIcon />
</a>
<RouterLink v-else :to="next.path">
{{ next.title || next.path }}
<NextIcon />
</RouterLink>
</span>
</p>
</div>
</template>
<script src="./PageNav" />
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/wrapper'
.page-nav
@extend $wrapper
padding-top 12px
padding-bottom 0
font-family Arial, Helvetica, sans-serif
.inner
min-height 32px
margin-top 0
border-top 1px solid var(--border-color)
padding-top 16px
overflow auto // clear float
.prev .icon, .next .icon
position relative
top 0.125em
width 1em
height 1em
color var(--accent-color)
.next
float right
</style>
import Vue from "vue";
export default Vue.extend({
name: "DropdownTransition",
methods: {
setHeight(items) {
// explicitly set height so that it can be transitioned
items.style.height = `${items.scrollHeight}px`;
},
unsetHeight(items) {
items.style.height = "";
},
},
});
//# sourceMappingURL=DropdownTransition.js.map
\ No newline at end of file
<template>
<transition
name="dropdown"
@enter="setHeight"
@after-enter="unsetHeight"
@before-leave="setHeight"
>
<slot />
</transition>
</template>
<script src="./DropdownTransition" />
<style lang="stylus">
.dropdown-enter, .dropdown-leave-to
height 0 !important
</style>
import Vue from "vue";
import SidebarNavLinks from "@theme/components/Sidebar/SidebarNavLinks.vue";
import SidebarLinks from "@theme/components/Sidebar/SidebarLinks.vue";
export default Vue.extend({
name: "Sidebar",
components: {
SidebarLinks,
SidebarNavLinks,
},
props: {
items: { type: Array, required: true },
},
computed: {
blogConfig() {
return this.$themeConfig.blog || {};
},
sidebarDisplay() {
return this.blogConfig.sidebarDisplay || "none";
},
},
});
//# sourceMappingURL=Sidebar.js.map
<template>
<aside class="sidebar">
<slot name="top" />
<SidebarNavLinks />
<slot name="center" />
<SidebarLinks :depth="0" :items="items" />
<slot name="bottom" />
</aside>
</template>
<script src="./Sidebar" />
<style lang="stylus">
.sidebar
position fixed
z-index 150
top $navbarHeight
left 0
bottom 0
box-sizing border-box
width $sidebarWidth
margin 0
background var(--bgcolor-blur)
box-shadow 2px 0 4px var(--card-shadow-color)
backdrop-filter saturate(200%) blur(20px)
font-size 16px
overflow-y auto
@media (max-width $MQMedium)
top $navbarMobileHeight
.theme-container.hide-navbar &
top 0
.theme-container:not(.has-navbar) &
top 0
a
display inline-block
color var(--text-color)
.blogger-info.mobile
display none
.blogger-info.mobile + hr
display none
& > .sidebar-links
padding 1.5rem 0
& > li
& > a.sidebar-link
font-size 1.1em
line-height 1.7
&:not(:first-child)
margin-top 0.75rem
// narrow desktop / iPad
@media (max-width $MQNarrow)
width $mobileSidebarWidth
font-size 15px
@media (min-width $MQMedium)
.theme-container:not(.has-sidebar) &
display none
// wide mobile
@media (max-width $MQMedium)
transform translateX(-100%)
transition transform 0.2s ease
box-shadow none
.theme-container.sidebar-open &
transform translateX(0)
box-shadow 2px 0 8px var(--card-shadow-color)
.theme-container:not(.has-navbar) &
top 0
.blogger-info.mobile
display block
.blogger-info.mobile + hr
display block
margin-top 16px
& > .sidebar-links
padding 1rem 0
</style>
import Vue from "vue";
import DropdownTransition from "@theme/components/Sidebar/DropdownTransition.vue";
import NavLink from "@theme/components/Navbar/NavLink.vue";
export default Vue.extend({
name: "SidebarDropdownLink",
components: { NavLink, DropdownTransition },
props: {
item: { type: Object, required: true },
},
data: () => ({
open: false,
}),
computed: {
dropdownAriaLabel() {
return this.item.ariaLabel || this.item.text;
},
iconPrefix() {
const { iconPrefix } = this.$themeConfig;
return iconPrefix === "" ? "" : iconPrefix || "icon-";
},
},
watch: {
$route() {
this.open = false;
},
},
methods: {
setOpen(value) {
this.open = value;
},
isLastItemOfArray(item, array) {
if (Array.isArray(array))
return item === array[array.length - 1];
return false;
},
},
});
//# sourceMappingURL=SidebarDropdownLink.js.map
\ No newline at end of file
<template>
<div class="mobile-dropdown-wrapper" :class="{ open }">
<button
class="dropdown-title"
type="button"
:aria-label="dropdownAriaLabel"
@click="setOpen(!open)"
>
<span class="title">
<i v-if="item.icon" :class="`iconfont ${iconPrefix}${item.icon}`" />
{{ item.text }}
</span>
<span class="arrow" :class="open ? 'down' : 'right'" />
</button>
<DropdownTransition>
<ul v-show="open" class="nav-dropdown">
<li
v-for="(child, index) in item.items"
:key="child.link || index"
class="dropdown-item"
>
<h4 v-if="child.type === 'links'" class="dropdown-subtitle">
<NavLink
v-if="child.link"
:item="child"
@focusout="
isLastItemOfArray(child, item.children) &&
child.children.length === 0 &&
setOpen(false)
"
/>
<span v-else>{{ child.text }}</span>
</h4>
<ul v-if="child.type === 'links'" class="dropdown-subitem-wrapper">
<li
v-for="grandchild in child.items"
:key="grandchild.link"
class="dropdown-subitem"
>
<NavLink
:item="grandchild"
@focusout="
isLastItemOfArray(grandchild, child.items) &&
isLastItemOfArray(child, item.items) &&
setOpen(false)
"
/>
</li>
</ul>
<NavLink
v-else
:item="child"
@focusout="isLastItemOfArray(child, item.items) && setOpen(false)"
/>
</li>
</ul>
</DropdownTransition>
</div>
</template>
<script src="./SidebarDropdownLink" />
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/arrow'
@require '~@mr-hope/vuepress-shared/styles/reset'
.mobile-dropdown-wrapper
cursor pointer
.dropdown-title
button()
cursor inherit
padding inherit
color var(--text-color)
font-family inherit
font-size inherit
line-height 1.4rem
&:hover
color var(--accent-color)
.arrow
arrow()
.nav-dropdown
margin-top 0.25rem
transition height 0.1s ease-out
overflow hidden
.dropdown-item
color inherit
line-height 1.7rem
h4
margin 0
padding-left 1.25rem
font-size 15px
line-height 1.7
.nav-link
padding 0
&:before
display none
.nav-link
display block
position relative
margin-bottom 0
padding 0 1.5rem 0 1.25rem
border-bottom none
font-weight 400
line-height 1.7rem
&:hover
color var(--accent-color)
&.active
color var(--accent-color)
&::before
content ''
position absolute
top calc(50% - 3px)
left 9px
width 0
height 0
border-top 3px solid transparent
border-left 5px solid var(--accent-color)
border-bottom 3px solid transparent
& > .nav-link
font-size 15px
line-height 2rem
.dropdown-subitem-wrapper
padding 0
list-style none
.dropdown-subitem
font-size 0.9em
padding-left 0.5rem
</style>
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import Vue from "vue";
import DropdownTransition from "@theme/components/Sidebar/DropdownTransition.vue";
import { isActive } from "@theme/utils/path";
export default Vue.extend({
name: "SidebarGroup",
components: { DropdownTransition },
props: {
item: {
type: Object,
required: true,
},
open: { type: Boolean },
depth: { type: Number, required: true },
},
beforeCreate() {
// eslint-disable-next-line
this.$options.components.SidebarLinks =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("@theme/components/Sidebar/SidebarLinks.vue").default;
},
methods: {
getIcon(icon) {
const { iconPrefix } = this.$themeConfig;
return this.$themeConfig.sidebarIcon !== false && icon
? `${iconPrefix === "" ? "" : iconPrefix || "icon-"}${icon}`
: "";
},
isActive,
},
});
//# sourceMappingURL=SidebarGroup.js.map
\ No newline at end of file
<template>
<section
:class="[
{
collapsable: item.collapsable,
'is-sub-group': depth !== 0,
},
`depth-${depth}`,
]"
class="sidebar-group"
>
<RouterLink
v-if="item.path"
:class="{
open,
active: isActive($route, item.path),
}"
class="sidebar-heading clickable"
:to="item.path"
@click="$emit('toggle')"
>
<i v-if="item.icon" :class="`iconfont ${getIcon(item.icon)}`" />
<span class="title">{{ item.title }}</span>
<span
v-if="item.collapsable"
:class="open ? 'down' : 'right'"
class="arrow"
/>
</RouterLink>
<p
v-else
:class="{ clickable: item.collapsable, open }"
class="sidebar-heading"
@click="$emit('toggle')"
>
<i v-if="item.icon" :class="`iconfont ${getIcon(item.icon)}`" />
<span class="title">{{ item.title }}</span>
<span
v-if="item.collapsable"
:class="open ? 'down' : 'right'"
class="arrow"
/>
</p>
<DropdownTransition>
<SidebarLinks
v-if="open || !item.collapsable"
class="sidebar-group-items"
:depth="depth + 1"
:items="item.children"
/>
</DropdownTransition>
</section>
</template>
<script src="./SidebarGroup" />
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/arrow'
.sidebar-group
.sidebar-group
padding-left 0.5em
&:not(.collapsable)
.sidebar-heading:not(.clickable)
color inherit
cursor auto
// refine styles of nested sidebar groups
&.is-sub-group
padding-left 0
& > .sidebar-heading
padding-left 1.75rem
font-weight normal
line-height 1.5
&:not(.clickable)
opacity 0.8
& > .sidebar-group-items
padding-left 1rem
& > li > .sidebar-link
border-left none
font-size 0.95em
&.depth-2
& > .sidebar-heading
border-left none
.sidebar-heading
display flex
box-sizing border-box
width 100%
margin 0
padding 0.35rem 1rem 0.35rem 1.25rem
border-left 0.25rem solid transparent
border-top-right-radius 0.25rem
border-bottom-right-radius 0.25rem
color var(--text-color)
font-size 1.1em
cursor pointer
transition color 0.15s ease
user-select none
&.open
color inherit
&.clickable
&:hover
background-color var(--bgcolor-light)
&.active
border-left-color var(--accent-color)
color var(--accent-color)
font-weight 500
.iconfont
margin-right 0.25em
.title
flex 1
.arrow
arrow()
font-size 1.5em
float right
.sidebar-group-items
font-size 0.95em
transition height 0.1s ease-out
overflow hidden
</style>
import Vue from "vue";
import { hashRE, isActive } from "@theme/utils/path";
import { groupSidebarHeaders } from "@theme/utils/sidebar";
const renderIcon = (h, icon) => icon
? h("i", {
class: ["iconfont", icon],
})
: null;
const renderLink = (h, { icon = "", text, link, level, active }) => h("RouterLink", {
props: {
to: link,
activeClass: "",
exactActiveClass: "",
},
class: {
active,
"sidebar-link": true,
[level ? `heading${level}` : ""]: level && level !== 2,
},
}, [renderIcon(h, icon), text]);
const renderExternalLink = (h, { path, title = path }) => h("a", {
attrs: {
href: path,
target: "_blank",
rel: "noopener noreferrer",
},
class: { "sidebar-link": true },
}, [title, h("OutboundLink")]);
const renderChildren = (h, { children, path, route, maxDepth, depth = 1 }) => {
if (!children || depth > maxDepth)
return null;
return h("ul", { class: "sidebar-sub-headers" }, children.map((child) => {
const active = isActive(route, `${path}#${child.slug}`);
return h("li", { class: "sidebar-sub-header" }, [
renderLink(h, {
text: child.title,
link: `${path}#${child.slug}`,
level: child.level,
active,
}),
renderChildren(h, {
children: child.children || false,
path,
route,
maxDepth,
depth: depth + 1,
}),
]);
}));
};
export default Vue.extend({
name: "SidebarLink",
functional: true,
props: {
item: {
type: Object,
required: true,
},
},
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
render(h, { parent: { $page, $route, $themeConfig, $themeLocaleConfig }, props }) {
const { item } = props;
// if the item can not be resolved
if (item.type === "error")
return null;
// external link
if (item.type === "external")
return renderExternalLink(h, item);
/*
* Use custom active class matching logic
* Due to edge case of paths ending with / + hash
*/
const selfActive = isActive($route, item.path);
/** whether the item is active */
const active =
// if the item is a heading, then one of the children needs to be active
item.type === "header"
? selfActive ||
(item.children || []).some((child) => isActive($route, `${item.basePath}#${child.slug}`))
: selfActive;
const pageMaxDepth = $page.frontmatter.sidebarDepth;
const localesMaxDepth = $themeLocaleConfig.sidebarDepth;
const themeMaxDepth = $themeConfig.sidebarDepth;
const maxDepth = typeof pageMaxDepth === "number"
? pageMaxDepth
: typeof localesMaxDepth === "number"
? localesMaxDepth
: typeof themeMaxDepth === "number"
? themeMaxDepth
: 2;
// the item is a heading
if (item.type === "header")
return [
renderLink(h, {
text: item.title || item.path,
link: item.path,
level: item.level,
active,
}),
renderChildren(h, {
children: item.children || false,
path: item.basePath,
route: $route,
maxDepth,
}),
];
const displayAllHeaders = $themeLocaleConfig.displayAllHeaders ||
$themeConfig.displayAllHeaders;
const link = renderLink(h, {
icon: $themeConfig.sidebarIcon !== false && item.frontmatter.icon
? `${$themeConfig.iconPrefix === ""
? ""
: $themeConfig.iconPrefix || "icon-"}${item.frontmatter.icon}`
: "",
text: item.title || item.path,
link: item.path,
active,
});
if ((active || displayAllHeaders) &&
item.headers &&
!hashRE.test(item.path)) {
const children = groupSidebarHeaders(item.headers);
return [
link,
renderChildren(h, {
children,
path: item.path,
route: $route,
maxDepth,
}),
];
}
return link;
},
});
//# sourceMappingURL=SidebarLink.js.map
\ No newline at end of file
<script src="./SidebarLink" />
<style lang="stylus">
$headings = 3 4 5 6
.sidebar-links
ul
padding 0
margin 0
list-style-type none
&.sidebar-sub-headers
padding-left 1rem
font-size 0.95em
.has-anchor &
@media (min-width $MQWide)
display none
a.sidebar-link
display inline-block
box-sizing border-box
width 100%
padding 0.35rem 1rem 0.35rem 1.25rem
border-left 0.2rem solid transparent
color var(--text-color)
font-size 1em
line-height 1.5
font-weight 400
for $heading in $headings
&.heading{$heading}
padding-left ($heading - 0.5) * 0.5rem !important
.iconfont
margin-right 0.25em
&:hover
background-color var(--bgcolor-light)
&.active
border-left-color var(--accent-color)
background-color var(--accent-color-a15)
color var(--accent-color)
font-weight 500
.iconfont
color var(--accent-color)
.sidebar-group &
padding-left 1.75rem
.sidebar-sub-headers &
padding-top 0.25rem
padding-bottom 0.25rem
border-left none
&.active
background-color transparent
font-weight 500
</style>
import Vue from "vue";
import SidebarGroup from "@theme/components/Sidebar/SidebarGroup.vue";
import SidebarLink from "@theme/components/Sidebar/SidebarLink.vue";
import { isActive } from "@theme/utils/path";
const descendantIsActive = (route, item) => {
if (item.type === "group")
return item.children.some((child) => {
if (child.type === "group")
return descendantIsActive(route, child);
return child.type === "page" && isActive(route, child.path);
});
return false;
};
const resolveOpenGroupIndex = (route, items) => {
for (let i = 0; i < items.length; i++)
if (descendantIsActive(route, items[i]))
return i;
return -1;
};
export default Vue.extend({
name: "SidebarLinks",
components: { SidebarGroup, SidebarLink },
props: {
items: {
type: Array,
required: true,
},
depth: { type: Number, required: true },
},
data: () => ({
openGroupIndex: 0,
}),
watch: {
$route() {
this.refreshIndex();
},
},
created() {
this.refreshIndex();
},
methods: {
refreshIndex() {
const index = resolveOpenGroupIndex(this.$route, this.items);
if (index > -1)
this.openGroupIndex = index;
},
toggleGroup(index) {
this.openGroupIndex = index === this.openGroupIndex ? -1 : index;
},
isActive(page) {
return isActive(this.$route, page.regularPath);
},
},
});
//# sourceMappingURL=SidebarLinks.js.map
\ No newline at end of file
<template>
<ul v-if="items.length" class="sidebar-links">
<li v-for="(item, index) in items" :key="index">
<SidebarGroup
v-if="item.type === 'group'"
:item="item"
:open="index === openGroupIndex"
:depth="depth"
@toggle="toggleGroup(index)"
/>
<SidebarLink v-else :item="item" />
</li>
</ul>
</template>
<script src="./SidebarLinks" />
import Vue from "vue";
import RepoLink from "@theme/components/Navbar/RepoLink.vue";
import SidebarDropdownLink from "@theme/components/Sidebar/SidebarDropdownLink.vue";
import NavLink from "@theme/components/Navbar/NavLink.vue";
import { getNavLinkItem } from "@theme/utils/navbar";
export default Vue.extend({
name: "SidebarNavLinks",
components: {
RepoLink,
SidebarDropdownLink,
NavLink,
},
computed: {
navLinks() {
const navbar = this.$themeLocaleConfig.nav || this.$themeConfig.nav || [];
return navbar.map((link) => getNavLinkItem(link));
},
},
});
//# sourceMappingURL=SidebarNavLinks.js.map
\ No newline at end of file
<template>
<nav class="sidebar-nav-links">
<!-- user links -->
<div v-for="item in navLinks" :key="item.link" class="nav-item">
<SidebarDropdownLink v-if="item.type === 'links'" :item="item" />
<NavLink v-else :item="item" />
</div>
<RepoLink />
</nav>
</template>
<script src="./SidebarNavLinks" />
<style lang="stylus">
.sidebar-nav-links
display none
padding 0.5rem 0 0.75rem 0
border-bottom 1px solid var(--border-color)
@media (max-width $MQMedium)
display block
ul
padding 0
margin 0.25rem 0 0
list-style-type none
.nav-item
position relative
display block
padding 0.5rem 0 0.5rem 1.5rem
font-size 1.1em
line-height 1.25rem
&:first-child
margin-left 0
</style>
import Vue from "vue";
import AutoIcon from "@theme/icons/AutoIcon.vue";
import DarkIcon from "@theme/icons/DarkIcon.vue";
import LightIcon from "@theme/icons/LightIcon.vue";
import { changeClass } from "@theme/utils/dom";
export default Vue.extend({
name: "DarkmodeSwitch",
components: { AutoIcon, DarkIcon, LightIcon },
data: () => ({
darkmode: "auto",
}),
computed: {
darkmodeConfig() {
return this.$themeConfig.darkmode || "auto-switch";
},
},
mounted() {
this.darkmode =
localStorage.getItem("darkmode") ||
"auto";
if (this.darkmodeConfig === "auto-switch")
if (this.darkmode === "auto")
this.setDarkmode("auto");
else
this.setDarkmode(this.darkmode);
else if (this.darkmodeConfig === "auto")
this.setDarkmode("auto");
else if (this.darkmodeConfig === "switch")
this.setDarkmode(this.darkmode);
// disabled
else
this.setDarkmode("off");
},
methods: {
setDarkmode(status) {
if (status === "on")
this.toggleDarkmode(true);
else if (status === "off")
this.toggleDarkmode(false);
else {
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
const isLightMode = window.matchMedia("(prefers-color-scheme: light)").matches;
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
if (event.matches)
this.toggleDarkmode(true);
});
window
.matchMedia("(prefers-color-scheme: light)")
.addEventListener("change", (event) => {
if (event.matches)
this.toggleDarkmode(false);
});
if (isDarkMode)
this.toggleDarkmode(true);
else if (isLightMode)
this.toggleDarkmode(false);
else {
const timeHour = new Date().getHours();
this.toggleDarkmode(timeHour < 6 || timeHour >= 18);
}
}
this.darkmode = status;
localStorage.setItem("darkmode", status);
},
toggleDarkmode(isDarkmode) {
const classes = document.body.classList;
if (isDarkmode)
changeClass(classes, ["theme-dark"], ["theme-light"]);
else
changeClass(classes, ["theme-light"], ["theme-dark"]);
},
},
});
//# sourceMappingURL=DarkmodeSwitch.js.map
\ No newline at end of file
<template>
<div class="darkmode-switch">
<template v-if="darkmodeConfig === 'auto-switch'">
<div
class="item day"
:class="{ active: darkmode === 'off' }"
@click="setDarkmode('off')"
>
<LightIcon />
</div>
<div
class="item auto"
:class="{ active: darkmode === 'auto' }"
@click="setDarkmode('auto')"
>
<AutoIcon />
</div>
<div
class="item night"
:class="{ active: darkmode === 'on' }"
@click="setDarkmode('on')"
>
<DarkIcon />
</div>
</template>
<div v-else-if="darkmodeConfig === 'switch'" class="switch">
<input
id="switch"
class="switch-input"
type="checkbox"
:checked="darkmode !== 'on'"
@click="setDarkmode(darkmode === 'on' ? 'off' : 'on')"
/>
<label class="label" for="switch">
<span class="label-content" />
</label>
</div>
</div>
</template>
<script src="./DarkmodeSwitch" />
<style lang="stylus">
@keyframes starry_star
50%
background rgba(255, 255, 255, 0.1)
box-shadow #fff 7.5px -0.75px 0 0, #fff 3px 2.5px 0 -0.25px, rgba(255, 255, 255, 0.1) 9.5px 4.5px 0 0.25px, #fff 8px 8.5px 0 0, rgba(255, 255, 255, 0.1) 5px 6px 0 -0.375px, #fff 1.25px 9.5px 0 0.25px
@keyframes bounceIn
0%
opacity 0
transform scale(0.3)
50%
opacity 100
transform scale(1.1)
55%
transform scale(1.1)
75%
transform scale(0.9)
100%
opacity 100
transform scale(1)
.darkmode-switch
display flex
height 22px
&:hover
cursor pointer
.item
padding 2px
border 1px solid var(--accent-color)
border-left none
line-height 1
&:first-child
border-left 1px solid var(--accent-color)
&.day
border-radius 4px 0 0 4px
&.night
border-radius 0 4px 4px 0
.icon
width 16px
height 16px
color var(--accent-color)
&.active
background var(--accent-color)
&:hover
cursor default
.icon
color var(--white)
.switch
display block
text-align center
user-select none
.label
display block
position relative
width 31.25px
height 17.5px
margin 0 auto
border-radius 17.5px
border 1px solid #1c1c1c
background #3c4145
font-size 1.4em
transition all 250ms ease-in
&:hover
cursor pointer
&:before
content ''
display block
position absolute
top 0.5px
left 1px
width 14px
height 14px
border 1.25px solid #e3e3c7
border-radius 50%
background #fff
transition all 250ms ease-in
&:after
content ''
display block
position absolute
top 62%
left 9.75px
z-index 10
width 2.8px
height 2.8px
opacity 0
background #fff
border-radius 50%
box-shadow #fff 0 0, #fff 0.75px 0, #fff 1.5px 0, #fff 2.25px 0, #fff 2.75px 0, #fff 3.5px 0, #fff 4px 0, #fff 5.25px -0.25px 0 0.25px, #fff 4px -1.75px 0 -0.5px, #fff 1.75px -1.75px 0 0.25px, #d3d3d3 0 0 0 1px, #d3d3d3 1.5px 0 0 1px, #d3d3d3 2.75px 0 0 1px, #d3d3d3 4px 0 0 1px, #d3d3d3 5.25px -0.25px 0 1.25px, #d3d3d3 4px -1.75px 0 0.25px, #d3d3d3 1.75px -1.75px 0 1.25px
transition opacity 100ms ease-in
.label-content
display block
position absolute
top 2.25px
left 52.5%
z-index 20
width 1px
height 1px
border-radius 50%
background #fff
box-shadow rgba(255, 255, 255, 0.1) 7.5px -0.75px 0 0, rgba(255, 255, 255, 0.1) 3px 2.5px 0 -0.25px, #fff 9.5px 4.5px 0 0.25px, rgba(255, 255, 255, 0.1) 8px 8.5px 0 0, #fff 5px 6px 0 0.375px, rgba(255, 255, 255, 0.1) 1.25px 9.5px 0 0.25px
animation starry_star 5s ease-in-out infinite
transition all 250ms ease-in
&:before
content ''
display block
position absolute
top -0.5px
left -6.25px
width 4.5px
height 4.5px
background #fff
border-radius 50%
border 1.25px solid #e3e3c7
box-shadow #e3e3c7 -7px 0 0 -0.75px, #e3e3c7 -2px 6px 0 -0.5px
transform-origin -1.5px 130%
transition all 250ms ease-in
.switch-input
display none
transition all 250ms ease-in
&:checked + .label
background #9ee3fb
border 1px solid #86c3d7
&:before
left 13.75px
background #ffdf6d
border 1.25px solid #e1c348
&:after
opacity 100
animation bounceIn 0.6s ease-in-out 0.1s
animation-fill-mode backwards
& > .label-content
opacity 0
box-shadow rgba(255, 255, 255, 0.1) 7.5px -0.75px 0 -1px, rgba(255, 255, 255, 0.1) 3px 2.5px 0 -1.25px, #fff 9.5px 4.5px 0 -0.75px, rgba(255, 255, 255, 0.1) 8px 8.5px 0 -1px, #fff 5px 6px 0 -1.375px, rgba(255, 255, 255, 0.1) 1.25px 9.5px 0 -0.75px
animation none
&:before
left 6.25px
transform rotate(70deg)
</style>
import Vue from "vue";
import ClickOutside from "@theme/utils/click-outside";
import ThemeOptions from "@theme/components/Theme/ThemeOptions.vue";
export default Vue.extend({
name: "ThemeColor",
directives: { "click-outside": ClickOutside },
components: { ThemeOptions },
data: () => ({
showMenu: false,
}),
methods: {
clickOutside() {
this.showMenu = false;
},
},
});
//# sourceMappingURL=ThemeColor.js.map
\ No newline at end of file
<template>
<button
v-click-outside="clickOutside"
class="color-button"
:class="{ select: showMenu }"
tabindex="-1"
aria-hidden="true"
@click="showMenu = !showMenu"
>
<svg
class="skin-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M224 800c0 9.6 3.2 44.8 6.4 54.4 6.4 48-48 76.8-48 76.8s80 41.6 147.2 0 134.4-134.4
38.4-195.2c-22.4-12.8-41.6-19.2-57.6-19.2C259.2 716.8 227.2 761.6 224 800zM560 675.2l-32
51.2c-51.2 51.2-83.2 32-83.2 32 25.6 67.2 0 112-12.8 128 25.6 6.4 51.2 9.6 80 9.6 54.4 0
102.4-9.6 150.4-32l0 0c3.2 0 3.2-3.2 3.2-3.2 22.4-16 12.8-35.2
6.4-44.8-9.6-12.8-12.8-25.6-12.8-41.6 0-54.4 60.8-99.2 137.6-99.2 6.4 0 12.8 0 22.4
0 12.8 0 38.4 9.6 48-25.6 0-3.2 0-3.2 3.2-6.4 0-3.2 3.2-6.4 3.2-6.4 6.4-16 6.4-16 6.4-19.2
9.6-35.2 16-73.6 16-115.2 0-105.6-41.6-198.4-108.8-268.8C704 396.8 560 675.2 560 675.2zM224
419.2c0-28.8 22.4-51.2 51.2-51.2 28.8 0 51.2 22.4 51.2 51.2 0 28.8-22.4 51.2-51.2 51.2C246.4
470.4 224 448 224 419.2zM320 284.8c0-22.4 19.2-41.6 41.6-41.6 22.4 0 41.6 19.2 41.6 41.6 0
22.4-19.2 41.6-41.6 41.6C339.2 326.4 320 307.2 320 284.8zM457.6 208c0-12.8 12.8-25.6 25.6-25.6
12.8 0 25.6 12.8 25.6 25.6 0 12.8-12.8 25.6-25.6 25.6C470.4 233.6 457.6 220.8 457.6 208zM128
505.6C128 592 153.6 672 201.6 736c28.8-60.8 112-60.8 124.8-60.8-16-51.2 16-99.2
16-99.2l316.8-422.4c-48-19.2-99.2-32-150.4-32C297.6 118.4 128 291.2 128 505.6zM764.8
86.4c-22.4 19.2-390.4 518.4-390.4 518.4-22.4 28.8-12.8 76.8 22.4 99.2l9.6 6.4c35.2 22.4
80 12.8 99.2-25.6 0 0 6.4-12.8 9.6-19.2 54.4-105.6 275.2-524.8 288-553.6
6.4-19.2-3.2-32-19.2-32C777.6 76.8 771.2 80 764.8 86.4z"
/>
</svg>
<transition mode="out-in" name="menu-transition">
<div v-show="showMenu" class="color-picker-menu">
<ThemeOptions />
</div>
</transition>
</button>
</template>
<script src="./ThemeColor" />
<style lang="stylus">
@require '~@mr-hope/vuepress-shared/styles/reset'
.color-button
button()
position relative
width 2.25rem
height 2.25rem
margin 0 0.25rem
padding 0.5rem
outline none
color #aaa
flex-shrink 0
&:hover, &.select
color var(--accent-color)
&.select:hover
color #aaa
.skin-icon
width 100%
height 100%
fill currentcolor
.color-picker-menu
position absolute
top: $navbarHeight - $navbarVerticalPadding
left 50%
min-width 100px
margin 0
padding 0.5em 0.75em
background var(--bgcolor)
box-shadow 2px 2px 10px var(--card-shadow-color)
color var(--dark-grey)
border-radius 0.25em
transform translateX(-50%)
z-index 250
@media (max-width $MQMobile)
top: $navbarMobileHeight - $navbarMobileVerticalPadding
transform translateX(-80%)
&::before
content ''
position absolute
top -7px
left 50%
border-style solid
border-color transparent transparent var(--bgcolor)
border-width 0 7px 7px
transform translateX(-50%)
@media (max-width $MQMobile)
left 80%
&.menu-transition-enter-active, &.menu-transition-leave-active
transition all 0.25s ease-in-out
&.menu-transition-enter, &.menu-transition-leave-to
top 30px
opacity 0
ul
list-style-type none
margin 0
padding 0
@media (max-width $MQMobile)
.color-picker
.color-picker-menu
left calc(50% - 35px)
&::before
left calc(50% + 35px)
</style>
import Vue from "vue";
import { getDefaultLocale } from "@mr-hope/vuepress-shared";
import DarkmodeSwitch from "@theme/components/Theme/DarkmodeSwitch.vue";
const defaultColorPicker = {
red: "#e74c3c",
blue: "#3498db",
green: "#3eaf7c",
orange: "#f39c12",
purple: "#8e44ad",
};
export default Vue.extend({
name: "ThemeOptions",
components: { DarkmodeSwitch },
data: () => ({
themeColor: {},
isDarkmode: false,
}),
computed: {
text() {
return (this.$themeLocaleConfig.themeColor || getDefaultLocale().themeColor);
},
themeColorEnabled() {
return this.$themeConfig.themeColor !== false;
},
switchEnabled() {
return (this.$themeConfig.darkmode !== "disable" &&
this.$themeConfig.darkmode !== "auto");
},
},
mounted() {
const theme = localStorage.getItem("theme");
this.themeColor = {
list: this.$themeConfig.themeColor
? Object.keys(this.$themeConfig.themeColor)
: Object.keys(defaultColorPicker),
picker: this.$themeConfig.themeColor || defaultColorPicker,
};
if (theme)
this.setTheme(theme);
},
methods: {
setTheme(theme) {
const classes = document.body.classList;
const themes = this.themeColor.list.map((colorTheme) => `theme-${colorTheme}`);
if (!theme) {
localStorage.removeItem("theme");
classes.remove(...themes);
return;
}
classes.remove(...themes.filter((themeclass) => themeclass !== `theme-${theme}`));
classes.add(`theme-${theme}`);
localStorage.setItem("theme", theme);
},
},
});
//# sourceMappingURL=ThemeOptions.js.map
\ No newline at end of file
<template>
<div class="theme-options">
<ul v-if="themeColorEnabled" class="themecolor-select">
<label for="themecolor-select" v-text="`${text.themeColor}:`" />
<li>
<span class="default-theme" @click="setTheme()" />
</li>
<li v-for="color in themeColor.list" :key="color">
<span
:style="{ background: themeColor.picker[color] }"
@click="setTheme(color)"
/>
</li>
</ul>
<div v-if="switchEnabled" class="darkmode-toggle">
<label class="desc" for="darkmode-toggle" v-text="`${text.themeMode}:`" />
<DarkmodeSwitch />
<ScreenFull />
</div>
</div>
</template>
<script src="./ThemeOptions" />
<style lang="stylus">
.theme-options
font-size 14px
.themecolor-select
display flex
justify-content space-around
label
padding-right 8px
li
&:first-child
margin-right 8px
span
vertical-align middle
width 15px
height 15px
margin 0 2px
border-radius 2px
&.default-theme
background $accentColor // must be fixed to the original accent-color
.darkmode-toggle
display flex
align-items center
margin-top 8px
.desc
padding-right 8px
line-height 1.5
.full-screen, .cancel-full-screen
margin-left 0.5em
</style>
<template>
<svg
class="icon auto-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M460.864 539.072H564.8L510.592 376l-49.728 163.072zM872 362.368V149.504H659.648L510.528 0l-149.12 149.504H149.12v212.928L0 511.872l149.12 149.504v212.928h212.352l149.12 149.504 149.12-149.504h212.352V661.376l149.12-149.504L872 362.368zM614.464 693.12l-31.616-90.624H438.272l-31.616 90.624h-85.888l144.576-407.68h90.368l144.576 407.68h-85.824zm0 0"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg
class="icon dark-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M935.539 630.402c-11.43-11.432-28.674-14.739-43.531-8.354-46.734 20.103-96.363 30.297-147.508 30.297-99.59 0-193.221-38.784-263.64-109.203-108.637-108.637-139.61-270.022-78.908-411.148a39.497 39.497 0 0 0-51.886-51.887c-52.637 22.64-100.017 54.81-140.826 95.616-85.346 85.346-132.346 198.821-132.346 319.52 0 120.7 47.001 234.172 132.347 319.519S408.063 947.11 528.76 947.11c120.7 0 234.172-47.003 319.52-132.351 40.809-40.81 72.978-88.19 95.616-140.826a39.497 39.497 0 0 0-8.356-43.532z"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg
class="icon edit-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M117.953 696.992 64.306 959.696l265.931-49.336 450.204-452.505-212.284-213.376-450.204 452.513zm496.384-296.326L219.039 797.993l-46.108-46.34L568.233 354.33l46.104 46.335zm345.357-122.99-114.45 115.04-212.288-213.377 114.45-115.035 212.288 213.371zm0 0"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg
class="icon i18n-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M639.981 344.075c14.805 44.45 34.542 79.023 69.084 113.596 29.603-29.634 49.34-69.146 64.145-113.596H639.981zM314.33 591.024h128.29l-64.145-172.865-64.145 172.865z"
fill="currentColor"
/>
<path
d="M807.746 116.882H215.643c-54.274 0-98.681 44.45-98.681 98.78v592.677c0 54.329 44.407 98.78 98.68 98.78h592.104c54.273 0 98.681-44.451 98.681-98.78V215.66c0-54.329-39.475-98.78-98.68-98.78zM565.971 754.01c-9.866 9.878-19.738 9.878-29.603 9.878-4.94 0-14.805 0-19.738-4.939-4.939-4.939-9.872 0-9.872-4.939s-4.932-9.878-9.865-19.756c-4.94-9.878-4.94-14.817-9.872-24.695L467.29 655.23H294.592l-19.737 54.33c-9.866 19.755-14.805 34.572-19.738 44.45-4.939 9.878-14.804 9.878-29.603 9.878-9.871 0-19.737-4.939-29.609-9.878-9.865-9.878-14.798-14.817-14.798-24.695 0-4.939 0-9.878 4.933-19.756 4.939-9.878 4.939-14.817 9.865-24.695l108.553-276.583c4.939-9.878 4.939-19.756 9.872-29.633 4.932-9.878 9.865-19.756 14.798-24.695 4.939-4.94 9.872-14.817 19.737-19.756 9.872-4.94 19.738-4.94 29.61-4.94 9.865 0 19.73 0 29.603 4.94 9.865 4.939 14.804 9.878 19.737 19.756 4.933 4.939 9.866 14.817 14.798 24.695 4.94 9.877 9.872 19.755 14.805 34.572l108.553 271.644c9.865 19.756 14.804 34.573 14.804 44.451-4.939 4.94-9.872 14.817-14.804 24.695zm271.378-192.62c-54.273-19.756-93.748-44.451-128.29-74.085-34.536 34.573-78.943 59.268-133.223 74.085l-14.798-24.695c54.273-14.817 98.68-34.573 133.223-69.146-34.542-34.573-64.145-79.024-74.017-128.413h-49.34V319.38h133.228c-9.877-14.817-19.743-34.573-29.609-49.39l14.799-4.94c9.871 14.818 24.676 34.574 34.542 54.33h123.35v24.695h-49.34c-14.798 49.39-39.468 93.84-69.077 123.474 34.541 29.634 74.01 54.329 128.29 69.146l-19.738 24.695z"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg
class="icon light-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M512 256a42.667 42.667 0 0 0 42.667-42.667V128a42.667 42.667 0 0 0-85.334 0v85.333A42.667 42.667 0 0 0 512 256zm384 213.333h-85.333a42.667 42.667 0 0 0 0 85.334H896a42.667 42.667 0 0 0 0-85.334zM256 512a42.667 42.667 0 0 0-42.667-42.667H128a42.667 42.667 0 0 0 0 85.334h85.333A42.667 42.667 0 0 0 256 512zm9.387-298.667a42.667 42.667 0 0 0-59.307 62.72l61.44 59.307a42.667 42.667 0 0 0 31.147 11.947 42.667 42.667 0 0 0 30.72-13.227 42.667 42.667 0 0 0 0-60.16zm459.946 133.974a42.667 42.667 0 0 0 29.44-11.947l61.44-59.307a42.667 42.667 0 0 0-57.6-62.72l-61.44 60.587a42.667 42.667 0 0 0 0 60.16 42.667 42.667 0 0 0 28.16 13.227zM512 768a42.667 42.667 0 0 0-42.667 42.667V896a42.667 42.667 0 0 0 85.334 0v-85.333A42.667 42.667 0 0 0 512 768zm244.48-79.36a42.667 42.667 0 0 0-59.307 61.44l61.44 60.587a42.667 42.667 0 0 0 29.44 11.946 42.667 42.667 0 0 0 30.72-12.8 42.667 42.667 0 0 0 0-60.586zm-488.96 0-61.44 59.307a42.667 42.667 0 0 0 0 60.586 42.667 42.667 0 0 0 30.72 12.8 42.667 42.667 0 0 0 28.587-10.666l61.44-59.307a42.667 42.667 0 0 0-59.307-61.44zM512 341.333A170.667 170.667 0 1 0 682.667 512 170.667 170.667 0 0 0 512 341.333z"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg
class="icon next-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M906.772 512c0 4.772-2.211 9.267-5.99 12.175L524.257 813.66a15.37 15.37 0 0 1-18.616.092 15.368 15.368 0 0 1-5.038-17.91l75.714-191.672h-443.73c-8.488 0-15.36-6.881-15.36-15.36v-153.6c0-8.489 6.872-15.36 15.36-15.36h443.73l-75.714-191.682a15.358 15.358 0 0 1 5.048-17.91c5.51-4.158 13.128-4.137 18.606.092l376.525 289.485a15.323 15.323 0 0 1 5.99 12.165z"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="page-404-icon"
viewBox="0 0 178 130"
>
<defs>
<linearGradient
id="b"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#e9e9e9" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<linearGradient
id="c"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#dcdcdc" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<linearGradient
id="d"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#f1f1f1" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<linearGradient
id="e"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#dedede" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<linearGradient
id="f"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#e8e8e8" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<linearGradient
id="g"
x1=".213"
y1="1.265"
x2=".846"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#fff" />
<stop offset="1" stop-color="#f5f5f5" />
</linearGradient>
<linearGradient
id="h"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#c5c5c5" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<linearGradient
id="i"
x1=".5"
x2=".5"
y2="1"
gradientUnits="objectBoundingBox"
>
<stop offset="0" stop-color="#acacac" />
<stop offset="1" stop-color="#f2f2f2" stop-opacity=".388" />
</linearGradient>
<clipPath id="a">
<path transform="translate(744 1111)" fill="none" d="M0 0h178v130H0z" />
</clipPath>
</defs>
<g transform="translate(-744 -1111)" clip-path="url(#a)">
<g>
<path
d="M0 10.795 36.6 0v93.779L0 104.574z"
transform="translate(772.466 1122.142)"
fill="url(#b)"
/>
<path
d="M-8.492 10.642-26.361-.469v93.78l17.868 11.111z"
transform="translate(780.958 1122.293)"
fill="url(#c)"
/>
<path
d="M-8.5 5.55 28.106-5.3 10.228-16.437l-36.6 10.845z"
transform="translate(780.963 1127.438)"
fill="url(#d)"
/>
<path
d="M0 10.539 35.741 0v91.56L0 102.1z"
transform="translate(870.158 1123.617)"
fill="url(#d)"
/>
<path
d="M-8.913 10.38-26.361-.469v91.562l17.448 10.848z"
transform="translate(879.071 1123.775)"
fill="url(#e)"
/>
<path
d="m-8.918 5.032 35.741-10.59L9.366-16.437-26.375-5.848z"
transform="translate(879.076 1129.175)"
fill="url(#d)"
/>
<path
d="M0 9.137 30.839 0v79.381L0 88.519z"
transform="translate(799.678 1151.579)"
fill="url(#f)"
/>
<path
d="m-11.306 8.936-15.054-9.4v79.377l15.054 9.4z"
transform="translate(810.985 1151.78)"
fill="url(#c)"
/>
<path
d="M-11.313 2.087 19.526-7.05 4.464-16.437-26.375-7.3z"
transform="translate(810.991 1158.63)"
fill="url(#g)"
/>
<path
d="M178 53H0a51.361 51.361 0 0 1 10.453-20.952 74.532 74.532 0 0 1 19.742-16.811A103.3 103.3 0 0 1 57.089 4.058a127.515 127.515 0 0 1 63.823 0 103.3 103.3 0 0 1 26.894 11.179 74.532 74.532 0 0 1 19.741 16.811A51.363 51.363 0 0 1 178 53z"
transform="translate(744 1187.549)"
fill="url(#h)"
/>
<path d="m814.529 1199.586-1.272 1.212h2.3l1.2-1.212z" fill="#cbcbcb" />
<path
d="m816.725 1194.909-1.272 1.212h2.3l1.263-1.212z"
fill="#cbcbcb"
/>
<path d="m863.284 1199.585-1.272 1.212h2.3l1.2-1.212z" fill="#cbcbcb" />
<path d="m865.519 1194.9-1.272 1.212h2.3l1.263-1.212z" fill="#cbcbcb" />
<path
d="m799.527 1191.21 10.182-21.97h4.381l-9.931 21.719h14.876v3.941h-19.508zm13.081-9.493h4.152v17.859h-4.152zm20.728 18.151q-4.256 0-6.457-2.274a8.74 8.74 0 0 1-2.2-6.343v-13.791a8.708 8.708 0 0 1 2.21-6.353q2.212-2.264 6.447-2.264 4.256 0 6.457 2.253a8.726 8.726 0 0 1 2.2 6.363v13.792a8.708 8.708 0 0 1-2.21 6.349q-2.211 2.268-6.447 2.268zm0-4.048a4.29 4.29 0 0 0 3.328-1.178 4.862 4.862 0 0 0 1.074-3.39v-13.792a4.893 4.893 0 0 0-1.064-3.39 4.285 4.285 0 0 0-3.338-1.179 4.285 4.285 0 0 0-3.338 1.179 4.893 4.893 0 0 0-1.064 3.39v13.791a4.862 4.862 0 0 0 1.075 3.391 4.29 4.29 0 0 0 3.327 1.178zm14.928-4.61 10.181-21.97h4.381l-9.931 21.719h14.876v3.941h-19.507zm13.081-9.493h4.152v17.859h-4.152z"
fill="#c6c6c6"
/>
<path
d="m798.306 1192.431 10.182-21.97h4.381l-9.931 21.719h14.876v3.941h-19.508zm13.081-9.493h4.152v17.859h-4.152zm20.728 18.151q-4.256 0-6.457-2.274a8.74 8.74 0 0 1-2.2-6.343v-13.791a8.708 8.708 0 0 1 2.21-6.353q2.212-2.264 6.447-2.264 4.256 0 6.457 2.253a8.726 8.726 0 0 1 2.2 6.363v13.792a8.708 8.708 0 0 1-2.21 6.349q-2.211 2.268-6.447 2.268zm0-4.048a4.29 4.29 0 0 0 3.328-1.178 4.862 4.862 0 0 0 1.074-3.39v-13.792a4.893 4.893 0 0 0-1.064-3.39 4.285 4.285 0 0 0-3.338-1.179 4.285 4.285 0 0 0-3.338 1.179 4.893 4.893 0 0 0-1.064 3.39v13.791a4.862 4.862 0 0 0 1.075 3.391 4.29 4.29 0 0 0 3.327 1.178zm14.928-4.61 10.181-21.97h4.381l-9.931 21.719h14.876v3.941h-19.507zm13.081-9.493h4.152v17.859h-4.152z"
fill="#b2b2b2"
/>
<path
d="m-27.694-19.435 10.182 14.517h4.381l-9.931-14.352h14.876v-2.606h-19.508zm13.081 6.273h4.152v-11.8h-4.152zM6.115-25.156q-4.256 0-6.457 1.5a4.8 4.8 0 0 0-2.2 4.191v9.113a4.784 4.784 0 0 0 2.212 4.2 11.511 11.511 0 0 0 6.447 1.5q4.256 0 6.457-1.489a4.786 4.786 0 0 0 2.2-4.2v-9.113a4.784 4.784 0 0 0-2.212-4.2 11.511 11.511 0 0 0-6.447-1.502zm0 2.675a5.705 5.705 0 0 1 3.328.779 2.6 2.6 0 0 1 1.074 2.24v9.113a2.607 2.607 0 0 1-1.064 2.24 5.7 5.7 0 0 1-3.338.779 5.7 5.7 0 0 1-3.338-.779 2.607 2.607 0 0 1-1.064-2.24v-9.113A2.6 2.6 0 0 1 2.788-21.7a5.705 5.705 0 0 1 3.327-.782zm14.927 3.047L31.224-4.918h4.381l-9.931-14.351H40.55v-2.606H21.043zm13.081 6.273h4.152v-11.8h-4.151z"
transform="translate(826 1226.245)"
opacity=".32"
fill="url(#i)"
/>
<g fill="#e6e6e6">
<path d="m858.428 1169.23-1.2 1.259h4.388l1.178-1.259z" />
<path
d="m802.944 1192.187 1.288-1.375h7.143v1.375zm8.415-9.25 1.273-1.234h4.15l-1.235 1.234zm-2.855-12.469 1.198-1.259h4.367l-1.178 1.259z"
/>
<path d="m861.362 1181.678-1.27 1.3h4.188l1.236-1.3z" />
<path d="m865.519 1190.9-1.27 1.3h2.3l1.162-1.3z" />
<path d="m852.838 1190.791-1.207 1.508h8.447v-1.508z" />
</g>
</g>
</g>
</svg>
</template>
<style lang="stylus">
.page-404-icon
.theme-dark &
filter invert(70%)
</style>
<template>
<svg
class="icon prev-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M906.783 588.79c-.02 8.499-6.882 15.36-15.38 15.37l-443.7-.01 75.704 191.682c2.52 6.42.482 13.763-5.038 17.91-5.52 4.168-13.138 4.147-18.616-.092L123.228 524.175a15.362 15.362 0 0 1-6-12.165c0-4.782 2.222-9.277 6-12.185L499.753 210.35a15.388 15.388 0 0 1 9.38-3.195c3.236 0 6.502 1.034 9.236 3.103 5.52 4.147 7.578 11.49 5.038 17.91L447.683 419.84l443.72-.01c8.498.01 15.36 6.881 15.36 15.36l.02 153.6z"
fill="currentColor"
/>
</svg>
</template>
"use strict";
const alias_1 = require("./node/alias");
const config_1 = require("./node/config");
const plugins_1 = require("./node/plugins");
// Theme API.
const themeAPI = (themeConfig, ctx) => ({
alias: (0, alias_1.getAlias)(themeConfig, ctx),
plugins: (0, plugins_1.getPluginConfig)(themeConfig),
additionalPages: [],
});
themeAPI.config = config_1.config;
// helper functions
themeAPI.themeConfig = (themeConfig) => themeConfig;
themeAPI.navbarConfig = (navbarConfig) => navbarConfig;
themeAPI.sidebarConfig = (sidebarConfig) => sidebarConfig;
module.exports = themeAPI;
//# sourceMappingURL=index.js.map
import Vue from "vue";
import Common from "@theme/components/Common.vue";
import Page404Icon from "@theme/icons/Page404Icon.vue";
import { getDefaultLocale } from "@mr-hope/vuepress-shared";
export default Vue.extend({
name: "NotFound",
components: {
Common,
Page404Icon,
},
computed: {
i18n() {
return this.$themeLocaleConfig.error404 || getDefaultLocale().error404;
},
msg() {
return this.i18n.hint[Math.floor(Math.random() * this.i18n.hint.length)];
},
},
methods: {
back() {
window.history.go(-1);
},
},
});
//# sourceMappingURL=404.js.map
\ No newline at end of file
<template>
<Common :sidebar="false">
<main class="page not-found">
<Page404Icon />
<blockquote v-text="msg" />
<button class="action-button" @click="back">{{ i18n.back }}</button>
<RouterLink class="action-button" to="/">{{ i18n.home }}</RouterLink>
</main>
</Common>
</template>
<script src="./404" />
<style lang="stylus">
.page.not-found
display block
max-width $homePageWidth
margin 0px auto
padding ($navbarHeight + 1rem) 2rem 2rem 2rem !important
text-align center
.page-404-icon
width 50%
margin 0 auto
.action-button
display inline-block
box-sizing border-box
margin 0 0.25rem
padding 0.5rem 1rem
border-width 0
border-bottom 1px solid var(--accent-color-d10)
border-radius 0.25rem
background var(--accent-color)
color var(--white)
font-size 1rem
outline none
transition background 0.1s ease
&:hover
cursor pointer
background var(--accent-color-l10)
</style>
import Vue from "vue";
import ContentBottom from "@ContentBottom";
import ContentTop from "@ContentTop";
import NavbarStart from "@NavbarStart";
import NavbarCenter from "@NavbarCenter";
import NavbarEnd from "@NavbarEnd";
import PageBottom from "@PageBottom";
import PageTop from "@PageTop";
import SidebarBottom from "@SidebarBottom";
import SidebarCenter from "@SidebarCenter";
import SidebarTop from "@SidebarTop";
import Common from "@theme/components/Common.vue";
import Home from "@theme/components/Home.vue";
import Page from "@theme/components/Page.vue";
export default Vue.extend({
name: "Layout",
components: {
Common,
ContentBottom,
ContentTop,
Home,
NavbarCenter,
NavbarEnd,
NavbarStart,
Page,
PageBottom,
PageTop,
SidebarBottom,
SidebarCenter,
SidebarTop,
},
});
//# sourceMappingURL=Layout.js.map
<template>
<Common :sidebar="true">
<template #navbar-start>
<slot name="navbar-start">
<NavbarStart />
<Content slot-key="navbar-start" />
</slot>
</template>
<template #navbar-center>
<slot name="navbar-center">
<NavbarCenter />
<Content slot-key="navbar-center" />
</slot>
</template>
<template #navbar-end>
<slot name="navbar-end">
<NavbarEnd />
<Content slot-key="navbar-end" />
</slot>
</template>
<template #sidebar-top>
<slot name="sidebar-top">
<SidebarTop />
<Content slot-key="sidebar-top" />
</slot>
</template>
<template #sidebar-center>
<slot name="sidebar-center">
<SidebarCenter />
<Content slot-key="sidebar-center" />
</slot>
</template>
<template #sidebar-bottom>
<slot name="sidebar-bottom">
<SidebarBottom />
<Content slot-key="sidebar-bottom" />
</slot>
</template>
<template #default="slotProps">
<Home v-if="$frontmatter.home" />
<Page
v-else
:headers="slotProps.headers"
:sidebar-items="slotProps.sidebarItems"
>
<template #top>
<slot name="page-top">
<PageTop />
<Content slot-key="page-top" />
</slot>
</template>
<template #content-top>
<slot name="content-top">
<ContentTop />
<Content slot-key="content-top" />
</slot>
</template>
<template #content-bottom>
<slot name="content-bottom">
<ContentBottom />
<Content slot-key="content-bottom" />
</slot>
</template>
<template #bottom>
<slot name="page-bottom">
<PageBottom />
<Content slot-key="page-bottom" />
</slot>
</template>
</Page>
</template>
</Common>
</template>
<script src="./Layout" />
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAlias = void 0;
const path_1 = require("path");
const getAlias = (themeConfig, ctx) => {
const { siteConfig } = ctx;
// Resolve algolia
const isAlgoliaSearch = Boolean(themeConfig.algolia) ||
Object.keys((siteConfig.locales && themeConfig.locales) || {}).some((base) => themeConfig.locales[base].algolia);
const commentEnabled = themeConfig.comment &&
themeConfig.comment.type &&
themeConfig.comment.type !== "disable";
const themeColorEnabled = !(themeConfig.themeColor === false && themeConfig.darkmode === "disable");
const { custom = {} } = themeConfig;
const noopModule = "@mr-hope/vuepress-shared/lib/esm/noopModule";
return {
"@AlgoliaSearchBox": isAlgoliaSearch
? themeConfig.algoliaType === "full"
? (0, path_1.resolve)(__dirname, "../components/AlgoliaSearch/Full.vue")
: (0, path_1.resolve)(__dirname, "../components/AlgoliaSearch/Dropdown.vue")
: noopModule,
"@ContentTop": custom.contentTop
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.contentTop)
: noopModule,
"@ContentBottom": custom.contentBottom
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.contentBottom)
: noopModule,
"@PageTop": custom.pageTop
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.pageTop)
: noopModule,
"@PageBottom": custom.pageBottom
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.pageBottom)
: noopModule,
"@Comment": commentEnabled
? "@mr-hope/vuepress-plugin-comment/lib/client/Comment.vue"
: noopModule,
"@NavbarStart": custom.navbarStart
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.navbarStart)
: noopModule,
"@NavbarCenter": custom.navbarCenter
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.navbarCenter)
: noopModule,
"@NavbarEnd": custom.navbarEnd
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.navbarEnd)
: noopModule,
"@ThemeColor": themeColorEnabled
? (0, path_1.resolve)(__dirname, "../components/Theme/ThemeColor.vue")
: noopModule,
"@SidebarTop": custom.sidebarTop
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.sidebarTop)
: noopModule,
"@SidebarCenter": custom.sidebarCenter
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.sidebarCenter)
: noopModule,
"@SidebarBottom": custom.sidebarBottom
? (0, path_1.resolve)(ctx.sourceDir, ".vuepress", custom.sidebarBottom)
: noopModule,
};
};
exports.getAlias = getAlias;
//# sourceMappingURL=alias.js.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.chunkRenamePlugin = void 0;
const chunkRenamePlugin = ({ pageChunkName = ({ title = "", key }) => {
const chunkTitle = (title || "").replace(/[.&*?#\\/:"<>| ]/gu, "");
return chunkTitle ? `page-${chunkTitle}` : `page-${key.slice(1)}`;
}, layoutChunkName = (layout) => `layout-${layout.componentName}`, }, context) => {
// override internal plugins
const plugins = [];
if (pageChunkName) {
plugins.push({
name: "@vuepress/internal-page-components",
extendPageData(page) {
page._chunkName = pageChunkName(page);
},
clientDynamicModules() {
const content = `export default {\n${context.pages
.filter(({ _filePath }) => _filePath)
.map((page) => {
const key = JSON.stringify(page.key);
const filePath = JSON.stringify(page._filePath);
const comment = page._chunkName
? `/* webpackChunkName: ${JSON.stringify(page._chunkName)} */`
: "";
return ` ${key}: () => import(${comment}${filePath})`;
})
.join(",\n")} \n}`;
return {
dirname: "internal",
name: "page-components.js",
content,
};
},
});
}
if (layoutChunkName) {
const { layoutComponentMap } = context.themeAPI;
for (const key in layoutComponentMap) {
const component = layoutComponentMap[key];
component._chunkName = layoutChunkName(component);
}
plugins.push({
name: "@vuepress/internal-layout-components",
clientDynamicModules() {
const { layoutComponentMap } = context.themeAPI;
const content = `export default {\n${Object.keys(layoutComponentMap)
.map((name) => {
const component = layoutComponentMap[name];
const key = JSON.stringify(name);
const filePath = JSON.stringify(component.path);
const comment = component._chunkName
? `/* webpackChunkName: ${JSON.stringify(component._chunkName)} */`
: "";
return ` ${key}: () => import(${comment}${filePath})`;
})
.join(",\n")} \n}`;
return {
dirname: "internal",
name: "layout-components.js",
content,
};
},
});
}
return {
name: "chunk-rename",
plugins,
};
};
exports.chunkRenamePlugin = chunkRenamePlugin;
//# sourceMappingURL=chunk-rename.js.map
\ No newline at end of file
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cleanUrlPlugin = void 0;
const cleanUrlPlugin = ({ normalSuffix = "", indexSuffix = "/", notFoundPath = "/404.html", }) => ({
name: "clean-url",
extendPageData(page) {
const { regularPath, frontmatter = {} } = page;
if (!frontmatter.permalink) {
if (regularPath === "/404.html")
// path for 404 page
page.path = notFoundPath;
else if (regularPath.endsWith(".html"))
// normal path
// e.g. foo/bar.md -> foo/bar.html
page.path = `${regularPath.slice(0, -5)}${normalSuffix}`;
else if (regularPath.endsWith("/"))
// index path
// e.g. foo/index.md -> foo/
page.path = `${regularPath.slice(0, -1)}${indexSuffix}`;
}
},
});
exports.cleanUrlPlugin = cleanUrlPlugin;
//# sourceMappingURL=clean-url.js.map
\ No newline at end of file
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.config = void 0;
const vuepress_shared_1 = require("@mr-hope/vuepress-shared");
const locales_1 = require("./locales");
const themeConfig_1 = require("./themeConfig");
const defaultConfig = {
base: process.env.VuePress_BASE || "/",
temp: "./node_modules/.temp",
theme: "hope",
themeConfig: { locales: {} },
evergreen: true,
};
const getRootLang = (config) => {
var _a, _b;
// infer from siteLocale
const siteLocales = config.locales;
if ((siteLocales === null || siteLocales === void 0 ? void 0 : siteLocales["/"]) && (0, vuepress_shared_1.checkLang)((_a = siteLocales["/"]) === null || _a === void 0 ? void 0 : _a.lang))
return siteLocales["/"].lang;
// infer from themeLocale
const themeLocales = config.locales;
if ((themeLocales === null || themeLocales === void 0 ? void 0 : themeLocales["/"]) && (0, vuepress_shared_1.checkLang)((_b = themeLocales["/"]) === null || _b === void 0 ? void 0 : _b.lang))
return themeLocales["/"].lang;
(0, vuepress_shared_1.showLangError)("root");
return "en-US";
};
const config = (config) => {
// merge default config
(0, vuepress_shared_1.deepAssignReverse)(defaultConfig, config);
const resolvedConfig = config;
const rootLang = getRootLang(resolvedConfig);
(0, themeConfig_1.resolveThemeConfig)(resolvedConfig.themeConfig, rootLang);
(0, locales_1.resolveLocales)(resolvedConfig, rootLang);
return resolvedConfig;
};
exports.config = config;
//# sourceMappingURL=config.js.map
\ No newline at end of file
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveLocales = void 0;
const vuepress_shared_1 = require("@mr-hope/vuepress-shared");
const resolveLocales = (config, rootLang) => {
// ensure locales config
if (!config.locales)
config.locales = {};
const { locales } = config;
// set locate for base
locales["/"] = Object.assign({ lang: rootLang }, (locales["/"] || {}));
// handle other languages
Object.keys(config.themeConfig.locales).forEach((path) => {
if (path === "/")
return;
locales[path] = Object.assign({ lang: (0, vuepress_shared_1.path2Lang)(path) }, (locales[path] || {}));
});
};
exports.resolveLocales = resolveLocales;
//# sourceMappingURL=locales.js.map
\ No newline at end of file
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPluginConfig = void 0;
const path_1 = require("path");
const clean_url_1 = require("./clean-url");
const chunk_rename_1 = require("./chunk-rename");
const getPluginConfig = (themeConfig) => {
// set author for comment plugin
if (themeConfig.comment && themeConfig.author)
themeConfig.comment.author = themeConfig.author;
return [
["@mr-hope/comment", themeConfig.comment || true],
["@mr-hope/components"],
["@mr-hope/feed", themeConfig.feed],
["@mr-hope/git", themeConfig.git],
["@mr-hope/pwa", themeConfig.pwa],
["@mr-hope/seo", themeConfig.seo],
["@mr-hope/sitemap", themeConfig.sitemap],
[
"@mr-hope/smooth-scroll",
themeConfig.smoothScroll === false
? false
: typeof themeConfig.smoothScroll === "number"
? { delay: themeConfig.smoothScroll }
: themeConfig.smoothScroll || { delay: 500 },
],
["@vuepress/last-updated", false],
"@vuepress/nprogress",
[
"@vuepress/search",
{
searchMaxSuggestions: themeConfig.searchMaxSuggestions || 10,
},
],
["active-hash", themeConfig.activeHash],
["add-this", typeof themeConfig.addThis === "string"],
[
"copyright",
typeof themeConfig.copyright === "object"
? Object.assign({ minLength: 100, disable: themeConfig.copyright.status === "local", clipboardComponent: (0, path_1.resolve)(__dirname, "../components/Clipboard.vue") }, themeConfig.copyright) : false,
],
["md-enhance", themeConfig.mdEnhance || {}],
["@mr-hope/copy-code", themeConfig.copyCode],
["photo-swipe", themeConfig.photoSwipe],
[
"typescript",
themeConfig.typescript
? {
tsLoaderOptions: typeof themeConfig.typescript === "object"
? themeConfig.typescript
: {},
}
: false,
],
[
clean_url_1.cleanUrlPlugin,
themeConfig.cleanUrl === false
? false
: themeConfig.cleanUrl || { normalSuffix: "/" },
],
[
chunk_rename_1.chunkRenamePlugin,
themeConfig.chunkRename === false ? false : themeConfig.chunkRename,
],
];
};
exports.getPluginConfig = getPluginConfig;
//# sourceMappingURL=plugins.js.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveThemeConfig = void 0;
const vuepress_shared_1 = require("@mr-hope/vuepress-shared");
const setThemeLocales = (themeConfig, rootLang) => {
const rootLangPath = (0, vuepress_shared_1.lang2Path)(rootLang);
// set locate for base
themeConfig.locales["/"] = Object.assign(Object.assign(Object.assign({}, (0, vuepress_shared_1.getLocale)(rootLang)), (themeConfig.locales[rootLangPath] || {})), (themeConfig.locales["/"] || {}));
// handle other languages
Object.keys(themeConfig.locales).forEach((path) => {
if (path === "/")
return;
const lang = (0, vuepress_shared_1.path2Lang)(path);
themeConfig.locales[path] = Object.assign(Object.assign({}, (0, vuepress_shared_1.getLocale)(lang)), themeConfig.locales[path]);
});
};
const resolveThemeConfig = (themeConfig, rootLang) => {
setThemeLocales(themeConfig, rootLang);
};
exports.resolveThemeConfig = resolveThemeConfig;
//# sourceMappingURL=themeConfig.js.map
{$contentClass}
code
color lighten($textColor, 20%)
padding 0.25rem 0.5rem
margin 0
font-size 0.85em
background-color rgba(27,31,35,0.05)
border-radius 3px
.token
&.deleted
color #EC5975
&.inserted
color $accentColor
{$contentClass}
pre, pre[class*="language-"]
line-height 1.4
padding 1.25rem 1.5rem
margin 0.85rem 0
background-color $codeBgColor
border-radius 6px
overflow auto
code
color #fff
padding 0
background-color transparent
border-radius 0
div[class*="language-"]
position relative
background-color $codeBgColor
border-radius 6px
.highlight-lines
user-select none
padding-top 1.3rem
position absolute
top 0
left 0
width 100%
line-height 1.4
.highlighted
background-color rgba(0, 0, 0, 66%)
pre, pre[class*="language-"]
background transparent
position relative
z-index 1
&::before
position absolute
z-index 3
top 0.8em
right 1em
font-size 0.75rem
color rgba(255, 255, 255, 0.4)
&:not(.line-numbers-mode)
.line-numbers-wrapper
display none
&.line-numbers-mode
.highlight-lines .highlighted
position relative
&:before
content ' '
position absolute
z-index 3
left 0
top 0
display block
width $lineNumbersWrapperWidth
height 100%
background-color rgba(0, 0, 0, 66%)
pre
padding-left $lineNumbersWrapperWidth + 1 rem
vertical-align middle
.line-numbers-wrapper
position absolute
top 0
width $lineNumbersWrapperWidth
text-align center
color rgba(255, 255, 255, 0.3)
padding 1.25rem 0
line-height 1.4
br
user-select none
.line-number
position relative
z-index 4
user-select none
font-size 0.85em
&::after
content ''
position absolute
z-index 2
top 0
left 0
width $lineNumbersWrapperWidth
height 100%
border-radius 6px 0 0 6px
border-right 1px solid rgba(0, 0, 0, 66%)
background-color $codeBgColor
for lang in $codeLang
div{'[class~="language-' + lang + '"]'}
&:before
content ('' + lang)
div[class~="language-javascript"]
&:before
content "js"
div[class~="language-typescript"]
&:before
content "ts"
div[class~="language-markup"]
&:before
content "html"
div[class~="language-markdown"]
&:before
content "md"
div[class~="language-json"]:before
content "json"
div[class~="language-ruby"]:before
content "rb"
div[class~="language-python"]:before
content "py"
div[class~="language-bash"]:before
content "sh"
div[class~="language-php"]:before
content "php"
@import '~prismjs/themes/prism-tomorrow.css'
@require './normalize'
@require './prefer-color-scheme-config'
@require './theme'
@require './plugins/index'
@require './theme-color'
@require '~balloon-css/balloon.min.css'
@require '~@mr-hope/vuepress-shared/styles/wrapper'
html, body
padding 0
margin 0
background var(--bgcolor)
body
font-family Georgia Pro, Georgia, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', STHeiti, 'Microsoft YaHei', SimSun, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
font-display optional
font-size 16px
color var(--text-color)
min-height 100vh
-webkit-tap-highlight-color transparent
{$contentClass}:not(.custom)
@extend $wrapper
> *:first-child
margin-top $navbarHeight
a:hover
text-decoration underline
// unknow container
p.demo
padding 1rem 1.5rem
border 1px solid #ddd
border-radius 4px
img
max-width 100%
{$contentClass}.custom
padding 0
margin 0
img
max-width 100%
a
font-weight 500
color var(--accent-color)
text-decoration none
overflow-wrap break-word
p a code
font-weight 400
color var(--accent-color)
kbd
background #eee
border solid 0.15rem #ddd
border-bottom solid 0.25rem #ddd
border-radius 0.15rem
padding 0 0.15em
blockquote
font-size 1rem
color var(--light-grey)
border-left 0.2rem solid #dfe2e5
margin 1rem 0
padding 0.25rem 0 0.25rem 1rem
& > p
margin 0
ul, ol
padding-left 1.2em
strong
font-weight 600
h1, h2, h3, h4, h5, h6
font-weight 500
line-height 1.25
{$contentClass}:not(.custom) > &
margin-top 0.5rem - $navbarHeight
padding-top: ($navbarHeight + 1rem)
margin-bottom 0.5rem
&:first-child
margin-top -3rem
margin-bottom 1rem
+ p, + pre, + .custom-block
margin-top 2rem
&:hover .header-anchor
opacity 1
p
{$contentClass}:not(.custom) > &, {$contentClass}:not(.custom) > ul &, {$contentClass}:not(.custom) > ol &
text-align justify
word-break break-word
hyphens auto
overflow-wrap break-word
@media (max-width $MQMobileNarrow)
text-align left
h1
font-size 2rem
h2
font-size 1.65rem
padding-bottom 0.3rem
border-bottom 1px solid var(--border-color)
h3
font-size 1.35rem
a.header-anchor
font-size 0.85em
float left
margin-left -0.87em
padding-right 0.23em
margin-top 0.125em
opacity 0
transition opacity 0.2s
&:hover
text-decoration none
code, kbd, .line-number
font-family source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace
p, ul, ol
line-height 1.7
hr
border 0
border-top 1px solid var(--border-color)
table
border-collapse collapse
margin 1rem 0
display block
overflow-x auto
tr
border-top 1px solid #dfe2e5
&:nth-child(2n)
background #f6f8fa
th, td
border 1px solid var(--grey14)
padding 0.6em 1em
.theme-dark
tr:nth-child(2n)
background #252322
th, td
border 1px solid var(--grey12)
/* basic color */
$accentColor ?= #3eaf7c
$textColor ?= #242424
$darkTextColor ?= #9e9e9e
$bgColor ?= #fff
$darkBgColor ?= #1e1e1e
$bgColorLight ?= #f8f8f8
$darkBgColorLight ?= #272727
$bgColorBlur ?= rgba(255, 255, 255, 0.9)
$darkBgColorBlur ?= rgba(30, 30, 30, 0.9)
$borderColor ?= #eaecef
$darkBorderColor ?= #302d28
$codeBgColor ?= #282c34
$darkCodeBgColor ?= #282c34
$arrowBgColor ?= #ccc
$darkArrowBgColor ?= #333
/* colors provided by theme */
$boxShadowColor ?= #f0f1f2
$darkBoxShadowColor ?= #0f0e0d
$cardShadowColor ?= rgba(0, 0, 0, 0.15)
$darkCardShadowColor ?= rgba(0, 0, 0, 0.3)
/* * theme-color */
$colorPicker ?= {
red: #e74c3c,
blue: #3498db,
green: #3eaf7c,
orange: #f39c12,
purple: #8e44ad
}
/* badge color */
$badgeTipColor ?= #42b983
$badgeWarningColor ?= darken(#ffe564, 35%)
$badgeErrorColor ?= #DA5961
/* layout */
$navbarHeight ?= 3.6rem
$navbarMobileHeight ?= 3.25rem
$navbarHorizontalPadding ?= 1.5rem
$navbarMobileHorizontalPadding ?= 1rem
$navbarVerticalPadding ?= 0.7rem
$navbarMobileVerticalPadding ?= 0.5rem
$sidebarWidth ?= 18rem
$mobileSidebarWidth ?= $sidebarWidth
$contentWidth ?= 820px
$homePageWidth ?= 960px
/* responsive breakpoints */
$MQWide ?= 1440px // wide screen
$MQMedium ?= 1350px // medium screen
$MQNormal ?= 1280px // desktop
$MQNarrow ?= 959px // narrow desktop / iPad
$MQMobile ?= 719px // wide mobile
$MQMobileNarrow ?= 419px // narrow mobile
/* code block */
$lineNumbersWrapperWidth ?= 2.5rem
$codeLang ?= js ts html md vue css sass scss less stylus go java c sh yaml py docker dockerfile makefile
/* content class */
$contentClass ?= '.theme-default-content'
@require './nprogress'
@require './search'
for $themeColorName, $themeColor in $colorPicker
.theme-{$themeColorName}
#nprogress
.bar
background $themeColor
.peg
box-shadow 0 0 10px $themeColor, 0 0 5px $themeColor
.spinner-icon
border-color $themeColor
.navbar
.search-box
height calc(2rem + 4px)
margin-left 0.25rem
input
margin-top 1px
margin-bottom 1px
border-color transparent
border-radius 0.25em
@media (max-width $MQMobile)
left 0
background-color transparent
@media (min-width $MQNarrow)
background-color #efeef4
&:focus
width 15rem
&:focus
background-color var(--bgcolor)
border-color var(--accent-color)
.theme-dark &
color var(--text-color)
background-color transparent
@media (min-width $MQNarrow)
background-color lighten($darkBgColor, 10%) !important
border-color var(--border-color)
&:focus
background-color lighten($darkBgColor, 10%) !important
.theme-dark &
.suggestion
a
color darken($darkTextColor, 35%)
&.focused
background #0c0b0a
a
color var(--accent-color)
border-color var(--accent-color)
.suggestions
border-color var(--border-color)
background var(--white)
#docsearch button
background-color transparent
border-color transparent
border-radius 0.25em
.mobile &
left 0
background-color transparent
@media (min-width $MQNarrow)
background-color #efeef4
&:focus
background-color var(--bgcolor)
border-color var(--accent-color)
.theme-dark &
color var(--text-color)
@media (min-width $MQNarrow)
background-color lighten($darkBgColor, 10%) !important
border-color var(--border-color)
&:focus
background-color lighten($darkBgColor, 10%) !important
body
--text-color $textColor
--bgcolor $bgColor
--bgcolor-light $bgColorLight
--border-color $borderColor
--code-bgcolor $codeBgColor
--arrow-bgcolor $arrowBgColor
--box-shadow-color $boxShadowColor
--card-shadow-color $cardShadowColor
--text-color-l10 lighten($textColor, 10%)
--text-color-l20 lighten($textColor, 20%)
--text-color-l25 lighten($textColor, 25%)
--text-color-l40 lighten($textColor, 40%)
--black #000
--dark-grey #666
--light-grey #999
--white #fff
--grey3 #333
--grey12 #bbb
--grey14 #eee
body.theme-light
--text-color $textColor
--bgcolor $bgColor
--bgcolor-light $bgColorLight
--bgcolor-blur $bgColorBlur
--border-color $borderColor
--code-bgcolor $codeBgColor
--arrow-bgcolor $arrowBgColor
--box-shadow-color $boxShadowColor
--card-shadow-color $cardShadowColor
--text-color-l10 lighten($textColor, 10%)
--text-color-l20 lighten($textColor, 20%)
--text-color-l25 lighten($textColor, 25%)
--text-color-l40 lighten($textColor, 40%)
--black #000
--dark-grey #666
--light-grey #999
--white #fff
--grey3 #333
--grey12 #bbb
--grey14 #eee
body.theme-dark
--text-color $darkTextColor
--bgcolor $darkBgColor
--bgcolor-light $darkBgColorLight
--bgcolor-blur $darkBgColorBlur
--border-color $darkBorderColor
--code-bgcolor $darkCodeBgColor
--arrow-bgcolor $darkArrowBgColor
--box-shadow-color $darkBoxShadowColor
--card-shadow-color $darkCardShadowColor
--text-color-l10 lighten($darkTextColor, 10%)
--text-color-l20 lighten($darkTextColor, 20%)
--text-color-l25 lighten($darkTextColor, 25%)
--text-color-l40 lighten($darkTextColor, 40%)
--black #fff
--dark-grey #999
--light-grey #666
--white #000
--grey3 #ccc
--grey12 #333
--grey14 #111
body
--accent-color $accentColor
--accent-color-l10 lighten($accentColor, 10%)
--accent-color-d10 darken($accentColor, 10%)
--accent-color-a15 alpha($accentColor, 15%)
theme-color($themeColor, $colorName)
body.theme-{$colorName}
--accent-color $themeColor
--accent-color-l10 lighten($themeColor, 10%)
--accent-color-d10 darken($themeColor, 10%)
--accent-color-a15 alpha($themeColor, 15%)
for key, value in $colorPicker
theme-color(value, key)
@require './code/index'
.theme-container:not(.has-navbar)
{$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6
margin-top 1.5rem
padding-top 0
// narrow mobile
@media (max-width $MQMobileNarrow)
h1
font-size 1.7rem
h2
font-size 1.5rem
h3
font-size 1.3rem
{$contentClass}
div[class*='language-']
margin 0.85rem -1.5rem
border-radius 0
.iconfont
font-weight normal
[aria-label][data-balloon-pos]
cursor help
import * as dayjs from "dayjs";
export const getDate = (date) => {
const time = dayjs(date instanceof Date || typeof date === "number" ? date : date.trim());
if (time.isValid()) {
const year = time.year();
const month = time.month() + 1;
const date = time.date();
const hour = time.hour();
const minute = time.minute();
const second = time.second();
const millisecond = time.millisecond();
if ((hour === 8 || hour === 0) &&
minute === 0 &&
second === 0 &&
millisecond === 0)
return [year, month, date, undefined, undefined, undefined];
return [year, month, date, hour, minute, second];
}
const pattern = /(?:(\d+)[/-](\d+)[/-](\d+))?\s*(?:(\d+):(\d+)(?::(\d+))?)?/u;
const [, year, month, day, hour, minute, second] = pattern.exec(date.trim()) || [];
const getNumber = (a) => typeof a === "undefined" ? undefined : Number(a);
const getYear = (yearNumber) => yearNumber && yearNumber < 100 ? yearNumber + 2000 : yearNumber;
const getSecond = (secondNumber) => hour && minute && !second ? 0 : secondNumber;
return [
getYear(getNumber(year)),
getNumber(month),
getNumber(day),
getNumber(hour),
getNumber(minute),
getSecond(getNumber(second)),
];
};
export const compareDate = (dataA, dataB) => {
if (!dataA)
return 1;
if (!dataB)
return -1;
const compare = (a, b) => {
if (a.length === 0)
return 0;
if (typeof b[0] === "undefined")
return typeof a[0] === "undefined" || a[0] === 0 ? 0 : -1;
if (typeof a[0] === "undefined")
return b[0] === 0 ? 0 : 1;
if (b[0] - a[0] === 0) {
a.shift();
b.shift();
return compare(a, b);
}
return b[0] - a[0];
};
return compare(getDate(dataA), getDate(dataB));
};
export const filterArticle = (pages, filterFunc) => pages.filter((page) => {
const { frontmatter: { article, blogpage, home }, title, } = page;
return (typeof title !== "undefined" &&
blogpage !== true &&
home !== true &&
article !== false &&
(!filterFunc || filterFunc(page)));
});
export const sortArticle = (pages, compareKey) => pages.slice(0).sort((prev, next) => {
if (compareKey) {
const prevKey = prev.frontmatter[compareKey];
const nextKey = next.frontmatter[compareKey];
if (prevKey && nextKey && prevKey !== nextKey)
return Number(nextKey) - Number(prevKey);
if (prevKey && !nextKey)
return -1;
if (!prevKey && nextKey)
return 1;
}
const prevTime = prev.frontmatter.time || prev.frontmatter.date || prev.createTimeStamp;
const nextTime = next.frontmatter.time || next.frontmatter.date || next.createTimeStamp;
return compareDate(prevTime, nextTime);
});
export const generatePagination = (pages, perPage = 10) => {
const result = [];
let index = 0;
while (index < pages.length) {
const paginationPage = [];
for (let i = 0; i < perPage; i++)
if (index < pages.length) {
paginationPage.push(pages[index]);
index += 1;
}
result.push(paginationPage);
}
return result;
};
//# sourceMappingURL=article.js.map
\ No newline at end of file
const validate = (binding) => {
if (typeof binding.value !== "function") {
console.warn("[Vue-click-outside:] provided expression", binding.expression, "is not a function.");
return false;
}
return true;
};
const isPopup = (popupItem, elements) => {
if (!popupItem || !elements)
return false;
for (let i = 0, len = elements.length; i < len; i++)
try {
if (popupItem.contains(elements[i]))
return true;
if (elements[i].contains(popupItem))
return false;
}
catch (err) {
return false;
}
return false;
};
const isServer = (vNode) => typeof vNode.componentInstance !== "undefined" &&
vNode.componentInstance.$isServer;
export const bind = (el, binding, vNode) => {
if (!validate(binding))
return;
// Define Handler and cache it on the element
const handler = (event) => {
if (!vNode.context)
return;
// Some components may have related popup item, on which we shall prevent the click outside event handler.
// eslint-disable-next-line
const elements =
// eslint-disable-next-line
event.path ||
(event.composedPath ? event.composedPath() : []);
if (elements && elements.length > 0)
elements.unshift(event.target);
if (el.contains(event.target) ||
// eslint-disable-next-line
isPopup(vNode.context.popupItem, elements))
return;
if (el.$vueClickOutside)
el.$vueClickOutside.callback(event);
};
// Add Event Listeners
el.$vueClickOutside = {
handler,
callback: binding.value,
};
const clickHandler = "ontouchstart" in document.documentElement ? "touchstart" : "click";
if (!isServer(vNode))
document.addEventListener(clickHandler, handler);
};
export const update = (el, binding) => {
if (validate(binding) && el.$vueClickOutside)
el.$vueClickOutside.callback = binding.value;
};
export const unbind = (el, _binding, vNode) => {
// Remove Event Listeners
const clickHandler = "ontouchstart" in document.documentElement ? "touchstart" : "click";
if (!isServer(vNode) && el.$vueClickOutside)
document.removeEventListener(clickHandler, el.$vueClickOutside.handler);
delete el.$vueClickOutside;
};
export default {
bind,
update,
unbind,
};
//# sourceMappingURL=click-outside.js.map
\ No newline at end of file
export default class Color {
constructor(type, red, green, blue, alpha = 1) {
this.type = type;
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
}
static fromHex(color) {
const parseHex = (colorString) => parseInt(colorString, 16);
const parseAlpha = (colorString, total) => Math.round((parseHex(colorString) * 100) / total) / 100;
if (color.length === 4)
return new Color("hex", parseHex(color[1]) * 17, parseHex(color[2]) * 17, parseHex(color[3]) * 17);
if (color.length === 5)
return new Color("hex", parseHex(color[1]) * 17, parseHex(color[2]) * 17, parseHex(color[3]) * 17, parseAlpha(color[4], 15));
if (color.length === 7)
return new Color("hex", parseHex(color.substring(1, 3)), parseHex(color.substring(3, 5)), parseHex(color.substring(5, 7)));
return new Color("hex", parseHex(color.substring(1, 3)), parseHex(color.substring(3, 5)), parseHex(color.substring(5, 7)), parseAlpha(color.substring(7, 9), 255));
}
// From RGB or RGBA
static fromRGB(color) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const RGBAPattern = /rgba\((.+)?,(.+)?,(.+)?,(.+)?\)/u;
// eslint-disable-next-line @typescript-eslint/naming-convention
const RGBPattern = /rgb\((.+)?,(.+)?,(.+)?\)/u;
const fromRGB = (colorString) => colorString.includes("%")
? (Number(colorString.trim().substring(0, colorString.trim().length - 1)) /
100) *
256 -
1
: Number(colorString.trim());
const rgbaResult = RGBAPattern.exec(color);
if (rgbaResult)
return new Color("rgb", fromRGB(rgbaResult[1]), fromRGB(rgbaResult[2]), fromRGB(rgbaResult[3]), Number(rgbaResult[4] || 1));
const rgbResult = RGBPattern.exec(color);
if (rgbResult)
return new Color("rgb", fromRGB(rgbResult[1]), fromRGB(rgbResult[2]), fromRGB(rgbResult[3]));
throw new Error(`Can not handle color: ${color}`);
}
static getColor(colorString) {
if (colorString.startsWith("#"))
return this.fromHex(colorString);
return this.fromRGB(colorString);
}
toString() {
if (this.type === "hex" && this.alpha === 1) {
const toHex = (color) => color < 10
? color.toString()
: color === 10
? "a"
: color === 11
? "b"
: color === 12
? "c"
: color === 13
? "d"
: color === 14
? "e"
: "f";
if (this.red % 17 === 0 && this.green % 17 === 0 && this.blue % 17 === 0)
return `#${toHex(this.red / 17)}${toHex(this.green / 17)}${toHex(this.blue / 17)}`;
const getHex = (color) => toHex((color - (color % 16)) / 16) + toHex(color % 16);
return `#${getHex(this.red)}${getHex(this.green)}${getHex(this.blue)}`;
}
return this.alpha === 1
? `rgb(${this.red},${this.green},${this.blue})`
: `rgba(${this.red},${this.green},${this.blue},${this.alpha})`;
}
adjust(item, amount) {
const result = Math.round(this[item] * amount);
if (item === "alpha")
this.alpha = result < 0 ? 0 : result > 1 ? 1 : result;
else
this[item] = result < 0 ? 0 : result > 255 ? 255 : result;
}
darken(amount) {
this.adjust("red", 1 - amount);
this.adjust("green", 1 - amount);
this.adjust("blue", 1 - amount);
return this;
}
lighten(amount) {
this.adjust("red", 1 + amount);
this.adjust("green", 1 + amount);
this.adjust("blue", 1 + amount);
return this;
}
}
//# sourceMappingURL=color.js.map
\ No newline at end of file
/**
* Change DOM classes
*
* @param domClass DOM classlist
* @param insert class to insert
* @param remove class to remove
*/
export const changeClass = (domClass, insert, remove) => {
const oldClasses = [];
domClass.remove(...remove);
domClass.forEach((classname) => {
oldClasses.push(classname);
});
domClass.value = "";
domClass.add(...insert, ...oldClasses);
};
//# sourceMappingURL=dom.js.map
\ No newline at end of file
/** Group lower level headings under h2 children */
export const groupHeaders = (headers) => {
const headerscopy = headers.map((header) => (Object.assign({}, header)));
let lastH2;
// group other headings under h2 headings
headerscopy.forEach((header) => {
if (header.level === 2)
lastH2 = header;
else if (lastH2) {
if (!lastH2.children)
lastH2.children = [];
lastH2.children.push(header);
}
});
// filter only h2 headings
return headerscopy.filter((header) => header.level === 2);
};
//# sourceMappingURL=groupHeader.js.map
\ No newline at end of file
export const getNavLinkItem = (navbarLink, beforeprefix = "") => {
var _a;
const prefix = beforeprefix + (navbarLink.prefix || "");
const navbarItem = Object.assign({}, navbarLink);
if (prefix) {
if (navbarItem.link !== undefined)
navbarItem.link = prefix + navbarItem.link;
delete navbarItem.prefix;
}
if ((_a = navbarItem.items) === null || _a === void 0 ? void 0 : _a.length)
Object.assign(navbarItem, {
type: "links",
items: navbarItem.items.map((item) => getNavLinkItem(item, prefix)),
});
else
navbarItem.type = "link";
return navbarItem;
};
//# sourceMappingURL=navbar.js.map
\ No newline at end of file
/**
* @param url navigate link
* @param router router
* @param route current route
*/
export const navigate = (url, router, route) => {
if (url)
if (url.startsWith("/")) {
// Inner absolute path
if (route.path !== url)
void router.push(url);
}
else if (url.startsWith("http://") ||
url.startsWith("https://") ||
url.startsWith("mailto:")) {
// Outter url
if (window)
window.open(url);
}
else {
// Inner relative path
const base = route.path.slice(0, route.path.lastIndexOf("/"));
void router.push(`${base}/${encodeURI(url)}`);
}
};
//# sourceMappingURL=navigate.js.map
\ No newline at end of file
export const hashRE = /#.*$/u;
export const extRE = /\.(md|html)$/u;
export const endingSlashRE = /\/$/u;
export const outboundRE = /^[a-z]+:/iu;
/** Remove hash and ext in a link */
export const normalize = (path) => decodeURI(path).replace(hashRE, "").replace(extRE, "");
export const getHash = (path) => {
const match = hashRE.exec(path);
if (match)
return match[0];
return "";
};
/** Judge whether a path is external */
export const isExternal = (path) => outboundRE.test(path);
/** Judge whether a path is `mailto:` link */
export const isMailto = (path) => path.startsWith("mailto:");
/** Judge whether a path is `tel:` link */
export const isTel = (path) => path.startsWith("tel:");
export const ensureExt = (path) => {
// do not resolve external links
if (isExternal(path))
return path;
const hashMatch = hashRE.exec(path);
const hash = hashMatch ? hashMatch[0] : "";
const normalized = normalize(path);
// do not resolve links ending with `/`
if (normalized.endsWith("/"))
return path;
// add `.html` ext
return `${normalized}.html${hash}`;
};
export const ensureEndingSlash = (path) => /(\.html|\/)$/u.test(path) ? path : `${path}/`;
/** Judge whether a route match a link */
export const isActive = (route, path) => {
const routeHash = decodeURIComponent(route.hash);
const linkHash = getHash(path);
// compare the hash only if the link has a hash
if (linkHash && routeHash !== linkHash)
return false;
const routePath = normalize(route.path);
const pagePath = normalize(path);
return routePath === pagePath;
};
/**
* @param path links being resolved
* @param base deploy base
* @param append whether append directly
*/
export const resolvePath = (path, base, append) => {
// do not resolve external links
if (isExternal(path))
return path;
const firstChar = path.charAt(0);
// do not resolve absolute links
if (firstChar === "/")
return path;
// if link is hash or query string, add with base
if (firstChar === "?" || firstChar === "#")
return `${base}${path}`;
// base links stack
const stack = base.split("/");
/*
* remove trailing segment if:
* - not appending
* - appending to trailing slash (last segment is empty)
*/
if (!append || !stack[stack.length - 1])
stack.pop();
// resolve relative path
const segments = path.replace(/^\//u, "").split("/");
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (segment === "..")
stack.pop();
else if (segment !== ".")
stack.push(segment);
}
// ensure leading slash
if (stack[0] !== "")
stack.unshift("");
return stack.join("/");
};
//# sourceMappingURL=path.js.map
\ No newline at end of file
import { groupHeaders } from "./groupHeader";
import { ensureEndingSlash, ensureExt, isExternal, normalize, resolvePath, } from "./path";
export const groupSidebarHeaders = groupHeaders;
const resolveSidebarHeaders = (page) => {
const headers = page.headers ? groupSidebarHeaders(page.headers) : [];
return [
{
type: "group",
collapsable: false,
title: page.title,
icon: page.frontmatter.icon,
path: "",
children: headers.map((header) => (Object.assign(Object.assign({}, header), { type: "header", basePath: page.path, path: `${page.path}#${header.slug}`, children: header.children }))),
},
];
};
const findMatchingSidebarConfig = (regularPath, config) => {
// return directly as array-type config is the moest simple config
if (Array.isArray(config))
return {
base: "/",
config,
};
// find matching config
for (const base in config)
if (ensureEndingSlash(regularPath).startsWith(encodeURI(base)))
return {
base,
config: config[base],
};
console.warn(`${regularPath} do not have valid sidebar config`);
return false;
};
/** sidebarConfig merged with pageObject */
export const resolvePageforSidebar = (pages, path) => {
// if it is external link
if (isExternal(path))
return {
type: "external",
path,
};
const realPath = normalize(path);
// find matches in all pages
for (const page of pages)
if (normalize(page.regularPath) === realPath)
// return sidebarConfig merged with pageObject
return Object.assign(Object.assign({}, page), { type: "page", path: ensureExt(page.path) });
console.error(`Sidebar: "${realPath}" has no matching page`);
return { type: "error", path: realPath };
};
const resolve = (prefix, path, base) => resolvePath(`${prefix}${path}`, base);
/**
* @param sidebarConfigItem config item being resolved
* @param pages pages Object
* @param base sidebar base
*/
const resolveSidebarItem = (sidebarConfigItem, pages, base, prefix = "") => {
// resolve and return directly
if (typeof sidebarConfigItem === "string")
return resolvePageforSidebar(pages, resolve(prefix, sidebarConfigItem, base));
// custom title with format `['path', 'customTitle']`
if (Array.isArray(sidebarConfigItem))
return Object.assign(resolvePageforSidebar(pages, resolve(prefix, sidebarConfigItem[0], base)), { title: sidebarConfigItem[1] });
const children = sidebarConfigItem.children || [];
// item do not have children
if (children.length === 0 && sidebarConfigItem.path)
// cover title
return Object.assign(resolvePageforSidebar(pages, resolve(prefix, sidebarConfigItem.path, base)), { title: sidebarConfigItem.title });
// resolve children recursively then return
return Object.assign(Object.assign({}, sidebarConfigItem), { type: "group", path: sidebarConfigItem.path
? resolve(prefix, sidebarConfigItem.path, base)
: "", children: children.map((child) => resolveSidebarItem(child, pages, base, `${prefix}${sidebarConfigItem.prefix || ""}`)), collapsable: sidebarConfigItem.collapsable !== false });
};
export const getSidebarItems = (page, site, localePath) => {
const { themeConfig, pages } = site;
const localeConfig = localePath && themeConfig.locales
? themeConfig.locales[localePath] || themeConfig
: themeConfig;
const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar;
// auto generate sidebar through headings
if (page.frontmatter.sidebar === "auto" || sidebarConfig === "auto")
return resolveSidebarHeaders(page);
// sidebar is disabled
if (!sidebarConfig)
return [];
const result = findMatchingSidebarConfig(page.regularPath, sidebarConfig);
return result
? result.config.map((item) => resolveSidebarItem(item, pages, result.base))
: [];
};
//# sourceMappingURL=sidebar.js.map
\ No newline at end of file
---
title: Welcome to the OP Stack
lang: en-US
---
**The OP Stack is the standardized, shared, and open-source development stack that powers Optimism, maintained by the Optimism Collective.**
::: tip Staying up to date
[Stay up to date on the Superchain and the OP Stack by subscribing to the Optimism newsletter](https://optimism.us6.list-manage.com/subscribe/post?u=9727fa8bec4011400e57cafcb&id=ca91042234&f_id=002a19e3f0).
:::
The OP Stack consists of the many different software components managed and maintained by the Optimism Collective that, together, form the backbone of Optimism.
The OP Stack is built as a public good for the Ethereum and Optimism ecosystems.
## The OP Stack powers Optimism
The OP Stack is the set of software that powers Optimism — currently in the form of the software behind Optimism Mainnet and eventually in the form of the Optimism Superchain and its governance.
With the advent of the Superchain concept, it has become increasingly important for Optimism to easily support the secure creation of new chains that can interoperate within the proposed Superchain ecosystem.
As a result, the OP Stack is primarily focused around the creation of a shared, high-quality, and fully open-source system for creating new L2 blockchains.
By coordinating on shared standards, the Optimism Collective can avoid rebuilding the same software in silos repeatedly.
Although the OP Stack today significantly simplifies the process of creating L2 blockchains, it’s important to note that this does not fundamentally define what the OP Stack **is**.
The OP Stack is *all* of the software that powers Optimism.
As Optimism evolves, so will the OP Stack.
**The OP Stack can be thought of as software components that either help define a specific layer of the Optimism ecosystem or fill a role as a module within an existing layer.**
Although the current heart of the OP Stack is infrastructure for running L2 blockchains, the OP Stack theoretically extends to layers on top of the underlying blockchain including tools like block explorers, message passing mechanisms, governance systems, and more.
Layers are generally more tightly defined towards the bottom of the stack (like the Data Availability Layer) but become more loosely defined towards the top of the stack (like the Governance Layer).
## The OP Stack today
Optimism Bedrock is the current iteration of the OP Stack.
The Bedrock release provides the tools for launching a production-quality Optimistic Rollup blockchain.
At this point in time, the APIs for the different layers of the OP Stack are still tightly coupled to this Rollup configuration of the stack.
If you'd like to learn more about the current state of the OP Stack, check out [the page describing the Bedrock release](/docs/releases/bedrock/README.md).
The OP Stack of today was built to support [the Optimism Superchain](./docs/understand/explainer.md), a proposed network of L2s that share security, communication layers, and a common development stack (the OP Stack itself).
The Bedrock release of the OP Stack makes it easy to spin up an L2 that will be compatible with the Superchain when it launches.
If you'd like to launch a Superchain-ready L2, check out our guide for running a chain based on the Bedrock release of the OP Stack.
It is possible to modify components of the OP Stack to build novel L2 systems.
If you're interested in experimenting with the OP Stack, check out [the OP Stack Hacks section of this site](/docs/build/hacks.md).
Please note that, as of the Bedrock release, the OP Stack is *not* designed to support these modifications and you will very much be *hacking* on the codebase.
As a result, **you should, for the moment, expect limited (if any) developer support for OP Stack Hacks.**
OP Stack Hacks will likely make your chain incompatible with the Optimism Superchain.
Have fun, but at your own risk and **stick to the Bedrock release if you're looking to join the Superchain!**
## The OP Stack tomorrow
The OP Stack is an evolving concept.
As Optimism grows, so will the OP Stack.
Today, the Bedrock Release of the OP Stack simplifies the process of deploying new L2 Rollups.
As work on the stack continues, it should become easier to plug in and configure different modules.
As the Superchain (link) begins to take shape, the OP Stack can evolve alongside it, to include the message-passing infrastructure that allows different chains to interoperate seamlessly.
At the end of the day, the OP Stack becomes what Optimism needs.
## Dive Deeper into the OP Stack
Ready to dive into the world of the OP Stack?
- If you’re interested in learning more about the current release of the OP Stack, check out the Bedrock Release page.
- If you’re interested in understanding the OP Stack in more depth, start with the [Design Principles](/docs/understand/design-principles.md) and [Landscape Overview](/docs/understand/landscape.md).
- If you're excited to join the Superchain, launch your first Superchain-ready L2 with our [Getting Started guide](/docs/build/getting-started.md) or dive directly into the OP Stack codebase to learn more.
The OP Stack is the next frontier for Ethereum. You’re already here, so what are you waiting for?
---
title: OP Stack Docs
lang: en-US
---
[Click here](..)
---
title: Building with the OP Stack
lang: en-US
---
The OP Stack is the decentralized development stack that powers Optimism. The current release of the OP Stack (named “Bedrock”) was designed to facilitate the creation of L2 blockchains that can interoperate with the proposed [Optimism Superchain](https://app.optimism.io/superchain/). The Superchain is a proposed network of rollups that share a security model, communication protocols, and a common development stack (the OP Stack itself). **The best way to get started building with the OP Stack today is to launch your own Bedrock-based Rollup by following our guide to [Running a Bedrock Rollup](./getting-started.md)**. Bedrock Rollups will be compatible with the Superchain.
You can also experiment with the OP Stack by making modifications to its various components. The OP Stack, as of the Bedrock release, is not explicitly designed to handle any significant modifications out of the box. By modifying the components of the OP Stack, you will likely no longer be compatible with the Superchain. Please also note that there is limited developer support available for modifications to the OP Stack. Have fun, but modify the stack at your own risk!
---
title: Configuration
lang: en-US
---
The OP Stack is a flexible platform with various configuration values that you can tweak to fit your specific needs. If you’re looking to fine-tune your deployment, look no further.
::: warning 🚧 Work in Progress
OP Stack configuration is an active work in progress and will likely evolve significantly as time goes on. If something isn’t working about your configuration, check back with this page to see if anything has changed.
:::
## New Blockchain Configuration
New OP Stack blockchains are currently configured with a JSON file inside the Optimism repository. The file is `<optimism repository>/packages/contracts-bedrock/deploy-config/<chain name>.json`. For example, [this is the configuration file for the tutorial blockchain](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/deploy-config/getting-started.json).
### Admin accounts
| Key | Type | Description | Default / Recommended value |
| --- | --- | --- | --- |
| `finalSystemOwner` | L1 Address | Address that will own all ownable contracts on L1 once the deployment is finished, including the `ProxyAdmin` contract. | It is recommended to have a single admin account to retain a common security model. |
| `controller` | L1 Address | Address that will own the `SystemDictator` contract and can therefore control the flow of the deployment or upgrade. | It is recommended to have a single admin account to retain a common security model. |
| `proxyAdminOwner` | L2 Address | Address that will own the `ProxyAdmin` contract on L2. The L2 `ProxyAdmin` contract owns all of the `Proxy` contracts for every predeployed contract in the range `0x42...0000` to `0x42..2048`. This makes predeployed contracts easily upgradeable. | It is recommended to have a single admin account to retain a common security model. |
### Fee recipients
| Key | Type | Description | Default value |
| --- | --- | --- | --- |
| `baseFeeVaultRecipient` | L1 Address | L1 address that the base fees from all transactions on the L2 can be withdrawn to. | It is recommended to have a single admin account to retain a common security model. |
| `l1FeeVaultRecipient` | L1 Address | L1 address that the L1 data fees from all transactions on the L2 can be withdrawn to. | It is recommended to have a single admin account to retain a common security model. |
| `sequencerFeeVaultRecipient` | L1 Address | L1 address that the tip fees from all transactions on the L2 can be withdrawn to. | It is recommended to have a single admin account to retain a common security model. |
### Misc.
| Key | Type | Description | Default value |
| --- | --- | --- | --- |
| `numDeployConfirmations` | Number of blocks | Number of confirmations to wait when deploying smart contracts to L1. | 1 |
| `l1StartingBlockTag` | Block hash | Block tag for the L1 block where the L2 chain will begin syncing from. Generally recommended to use a finalized block to avoid issues with reorgs. | |
| `l1ChainID` | Number | Chain ID of the L1 chain. | 1 for L1 Ethereum mainnet, <br> 5 for the Goerli test network. <br> [See here for other blockchains](https://chainlist.org/?testnets=true). |
| `l2ChainID` | Number | Chain ID of the L2 chain. | 42069 |
### Blocks
These fields apply to L2 blocks: Their timing, when do they need to be written to L1, and how they get written.
| Key | Type | Description | Default value |
| --- | --- | --- | --- |
| `l2BlockTime` | Number of seconds | Number of seconds between each L2 block. | 2 |
| `maxSequencerDrift` | Number of seconds | How far the L2 timestamp can differ from the actual L1 timestamp | 600 (10 minutes) |
| `sequencerWindowSize` | Number of blocks | Maximum number of L1 blocks that a Sequencer can wait to incorporate the information in a specific L1 block. For example, if the window is `10` then the information in L1 block `n` must be incorporated by L1 block `n+10`. | 3600 (12 hours) |
| `channelTimeout` | Number of blocks | Maximum number of L1 blocks that a transaction channel frame can be considered valid. A transaction channel frame is a chunk of a compressed batch of transactions. After the timeout, the frame is dropped. | 300 (1 hour) |
| `p2pSequencerAddress` | L1 Address | Address of the key that the Sequencer uses to sign blocks on the p2p network. | Sequencer, an address for which you own the private key |
| `batchInboxAddress` | L1 Address | Address that Sequencer transaction batches are sent to on L1. | 0xff00…0042069 |
| `batchSenderAddress` | L1 Address | Address of the account that nodes will filter for when searching for Sequencer transaction batches being sent to the `batchInboxAddress`. Can be updated later via the `SystemConfig` contract on L1. | Batcher, an address for which you own the private key |
### Proposal fields
These fields apply to output root proposals.
| Key | Type | Description | Default value |
| --- | --- | --- | --- |
| `l2OutputOracleStartingBlockNumber` | Number | Block number of the first OP Stack block. Typically this should be zero, but this may be non-zero for networks that have been upgraded from a legacy system (like Optimism Mainnet). Will be removed with the addition of permissionless proposals. | 0 |
| `l2OutputOracleStartingTimestamp` | Number | Timestamp of the first OP Stack block. This MUST be the timestamp corresponding to the block defined by the `l1StartingBlockTag`. Will be removed with the addition of permissionless proposals. | |
| `l2OutputOracleSubmissionInterval` | Number of blocks | Number of blocks between proposals to the `L2OutputOracle`. Will be removed with the addition of permissionless proposals. | 120 (24 minutes) |
| `finalizationPeriodSeconds` | Number of seconds | Number of seconds that a proposal must be available to challenge before it is considered finalized by the `OptimismPortal` contract. | We recommend 12 on test networks, seven days on production ones |
| `l2OutputOracleProposer` | L1 Address | Address that is allowed to submit output proposals to the `L2OutputOracle` contract. Will be removed when we have permissionless proposals. | |
| `l2OutputOracleChallenger` | L1 Address | Address that is allowed to challenge output proposals submitted to the `L2OutputOracle`. Will be removed when we have permissionless challenges. | It is recommended to have a single admin account to retain a common security model. |
### L1 data fee
These fields apply to the cost of the [L1 data fee](https://community.optimism.io/docs/developers/build/transaction-fees/#the-l1-data-fee) for L2 transactions.
| Key | Type | Description | Default value |
| --- | --- | --- | --- |
| `gasPriceOracleOverhead` | Number | Fixed L1 gas overhead per transaction. Default value will likely be adjusted with more information from the Optimism Goerli deployment. | 2100 |
| `gasPriceOracleScalar` | Number | Dynamic L1 gas overhead per transaction, given in 6 decimals. Default value of 1000000 implies a dynamic gas overhead of exactly 1x (no overhead). | 1000000 |
### EIP 1559 gas algorithm
These fields apply to [the EIP 1559 algorithm](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) used for the [L2 execution costs](https://community.optimism.io/docs/developers/build/transaction-fees/#the-l2-execution-fee) of transactions on the blockchain.
| Key | Type | Description | Default value | Value on L1 Ethereum |
| --- | --- | --- | --- | --- |
| `eip1559Denominator` | Number | Denominator used for the [EIP1559 gas pricing mechanism on L2](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md). A larger denominator decreases the amount by which the base fee can change in a single block. | 50 | 8 |
| `eip1559Elasticity` | Number | Elasticity for the [EIP1559 gas pricing mechanism on L2](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md). A larger elasticity increases the maximum allowable gas limit per block. | 10 | 2 |
| `l2GenesisBlockGasLimit` | String | Initial block gas limit, represented as a hex string. Default is 25m, implying a 2.5m target when combined with a 10x elasticity. | 0x17D7840 | |
| `l2GenesisBlockBaseFeePerGas` | String | Initial base fee, used to avoid an unstable EIP1559 calculation out of the gate. Initial value is 1 gwei. | 0x3b9aca00 | |
### Governance token
The governance token is a side-effect of use of the OP Stack in the Optimism Mainnet network. It may not be included by default in future releases.
| Key | Type | Description | Default value |
| --- | --- | --- | --- |
| `governanceTokenOwner` | L2 Address | Address that will own the token contract deployed by default to every OP Stack based chain. | |
| `governanceTokenSymbol` | String | Symbol for the token deployed by default to each OP Stack chain. | OP |
| `governanceTokenName` | String | Name for the token deployed by default to each OP Stack chain. | Optimism |
\ No newline at end of file
---
title: Data Availability Hacks
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
## Overview
The Data Availability Layer is responsible for the *ordering* and *storage* of the raw input data that forms the backbone of an OP Stack based chain (transactions, state roots, calls from other blockchains, etc.). You can conceptually think of this as an array of inputs — the ordering of this array should remain stable and the contents of this array should remain available. Unstable ordering of inputs will lead to reorgs of the OP Stack chain, while unavailable inputs will cause the OP Stack chain to halt entirely.
## Default
The default Data Availability Layer module for an OP Stack chain is the Ethereum DA module. When using the Ethereum DA module, all raw input data is expected to be found on Ethereum. Any data that is accessible on Ethereum can be queried when using this module, including calldata, events, and other block data.
## Security
OP Stack based chains are functions of the raw input data found on the Data Availability Layer module(s) used. If a required piece of data is not available, nodes will not be able to properly sync the chain. This also means that these nodes will not be able to dispute any invalid state proposals made to a Settlement Layer module. An OP Stack based chain cannot be safer than the Data Availability module.
You should be careful to understand the security properties of any Data Availability module(s) that you use. The standard Ethereum DA module generally provides the best security guarantees at the cost of higher transaction fees. Alternative DA modules may be appropriate depending on your particular use-case and risk tolerance.
## Modding
### Alternative EVM DA
A simple modification is to use an EVM-based blockchain other than Ethereum as the Data Availability Layer. Doing so simply requires using an L1 RPC other than Ethereum.
### EVM-Ordered Alternative DA
A more involved modification to the Data Availability Layer is an "EVM-Ordered" Alternative DA module. This involves using an EVM-based chain to maintain the *ordering* of transaction data while using a different data storage system to host the underlying data. Generally, ordering is maintained by publishing hashes of the data to the EVM-based chain while publishing the preimages to those hashes to the alternative data source.
An EVM-Ordered Alternative DA module significantly reduces costs by only publishing hashes and not full input data to the EVM chain. Using an EVM chain for ordering also reduces the number of changes that must be made to the standard Rollup configuration to achieve this result.
An example of an EVM-Ordered Alternative DA module can be found within [this modification to the OP Stack](https://github.com/celestiaorg/optimism/pull/3) that uses the Celestia blockchain as a third-party data availability provider.
### Non-EVM DA
A non-EVM DA module uses a chain not based on the EVM to manage both the ordering and storage of raw input data. Such a modification would require relatively significant modifications to the [derivation portion](https://github.com/ethereum-optimism/optimism/tree/develop/op-node/rollup/derive) of the `op-node`. No such fully-independent DA modules have been developed yet — be the first!
### Multiple DA
It is possible to use multiple Data Availability Layer modules at the same time. For instance, one could source data from two EVM-based chains simultaneously in order to form a bridge between the two chains. When using multiple Data Availability Layer modules, it is imperative to establish a global ordering between the two chains. One option for establishing this ordering is to use the timestamps of blocks from each chain.
Like a non-EVM DA module, a system with multiple Data Availability modules would need to make significant modifications to the [derivation portion](https://github.com/ethereum-optimism/optimism/tree/develop/op-node/rollup/derive) of the `op-node`. No such projects have been constructed yet.
\ No newline at end of file
---
title: Derivation Hacks
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
## Overview
The Derivation layer is responsible for parsing the raw inputs from the Data Availability layer and converting them into [Engine API](https://github.com/ethereum/execution-apis/tree/main/src/engine) payloads to be sent to the Execution layer. The Derivation Layer is generally tightly coupled to the Data Availability layer because it must understand both the APIs for the Data Availability layer module(s) of choice and the format of the raw data published to the chosen module(s).
## Default
The default Derivation layer module is the Rollup module. This module derives transactions from three sources: Sequencer transactions, user deposits, and L1 blocks. The Rollup module also enforces certain ordering properties that, for example, guarantee that user deposits are always included in the L2 chain within a certain configurable amount of time.
## Security
Modifying the Derivation layer can have unintended consequences. For example, removing or extending the time window in which user deposits must be included can allow a Sequencer to censor the L2 chain. Because of the flexibility of the Derivation layer, the exact impact of any change is likely to be unique to the specifics of the change. The negative impacts of any modifications should be carefully considered on a case-by-case basis.
## Modding
### EVM Event-Triggered Transactions
The default Rollup configuration of the OP Stack includes “deposited” transactions that are triggered whenever a specific event is emitted by the `OptimismPortal` contract on L1. Using the same principle, an OP Stack chain can derive transactions from events emitted by *any* contract on an EVM-based DA. Refer to [attributes.go](https://github.com/ethereum-optimism/optimism/blob/e468b66efedc5f47f4e04dc1acc803d4db2ce383/op-node/rollup/derive/attributes.go#L70) to understand how deposited transactions are derived and how custom transactions can be created.
### EVM Block-Triggered Transactions
Like with events, transactions on an OP Stack chain can be triggered whenever a new block is published on an EVM-based DA. The default Rollup configuration of the OP Stack already includes a block-triggered transaction in the form of [the “L1 info” transaction](https://github.com/ethereum-optimism/optimism/blob/e468b66efedc5f47f4e04dc1acc803d4db2ce383/op-node/rollup/derive/attributes.go#L103) that relays information like the latest block hash, timestamp, and base fee into L2. The Getting Started guide demonstrates the addition of a new block-triggered transaction in the form of a new transaction that reports the amount of gas burned via the base fee on L1.
### And much, much more…
The Derivation layer is one of the most flexible layers of the stack. Transactions can be generated from all sorts of raw input data and can be triggered from all sorts of conditions. You can derive transactions from any piece of data that can be found in the Data Availability layer modules!
[Tutorial: Adding attributes to the derivation function](./tutorials/add-attr.md).
\ No newline at end of file
---
title: Execution Hacks
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
## Overview
The Execution Layer is responsible for defining the format of state and the state transition function on L2. It is expected to trigger the state transition function when it receives a payload via the [Engine API](https://github.com/ethereum/execution-apis/tree/main/src/engine). Although the default Execution Layer module is the EVM, you can replace the EVM with any alternative VM as long as it sits behind the Engine API.
## Default
The default Execution Layer module is the Rollup EVM module. The Rollup EVM module utilizes a very lightly modified EVM that adds support for transactions that are triggered by smart contracts on L1 and introduces an L1 data fee to each transaction that accounts for the cost of publishing user transactions to L1. You can find the full set of differences between the standard EVM and the Rollup EVM [on this page](https://op-geth.optimism.io/).
## Security
As with modifications to the Derivation Layer, modifications to the Execution Layer can have unintended consequences. For instance, modifications to the EVM may break existing tooling or may open the door to denial of service attacks. Consider the impact of each modification carefully on a case-by-case basis.
## Modding
### EVM Tweaks
The default Execution Layer module is the EVM. It’s possible to modify the EVM in many different ways like adding new precompiles or inserting predeployed smart contracts into the genesis state. Precompiles can help make common smart contract operations cheaper and can therefore further reduce the cost of execution for your specific use-case. These modifications should be made directly to [the execution client](https://github.com/ethereum-optimism/op-geth).
It’s also possible to create alternative execution client implementations to improve the security properties of your chain. Note that if you modify the EVM, you must apply the same modifications to every execution client that you would like to support.
### Alternative VMs
The OP Stack allows you to replace the EVM with *any* state transition function, as long as the transition can be triggered via the Engine API. This has, for example, been used to implement an OP Stack chain that runs a GameBoy emulator rather than the EVM.
[Tutorial: Adding a precompile](./tutorials/new-precomp.md).
\ No newline at end of file
---
title: Explorer and Indexer
lang: en-US
---
The next step is to be able to see what is actually happening in your blockchain.
One easy way to do this is to use [Blockscout](https://www.blockscout.com/).
## Prerequisites
### Archive mode
Blockscout expects to interact with an Ethereum execution client in [archive mode](https://www.alchemy.com/overviews/archive-nodes#archive-nodes).
To create such a node, follow the [directions to add a node](./getting-started.md#adding-nodes), but in the command you use to start `op-geth` replace:
```sh
--gcmode=full \
```
with
```sh
--gcmode=archive \
```
### Docker
The easiest way to run Blockscout is to use Docker.
Download and install [Docker engine](https://docs.docker.com/engine/install/#server).
## Installation and configuration
1. Clone the Blockscout repository.
```sh
cd ~
git clone https://github.com/blockscout/blockscout.git
cd blockscout/docker-compose
```
1. Depending on the version of Docker you have, there may be an issue with the environment path.
Run this command to fix it:
```sh
ln -s `pwd`/envs ..
```
1. If `op-geth` in archive mode runs on a different computer or a port that isn't 8545, edit `docker-compose-no-build-geth.yml` to set `ETHEREUM_JSONRPC_HTTP_URL` to the correct URL.
1. Start Blockscout
```sh
docker compose -f docker-compose-no-build-geth.yml up
```
## Usage
After the docker containers start, browse to http:// < *computer running Blockscout* > :4000 to view the user interface.
You can also use the [API](https://docs.blockscout.com/for-users/api)
---
title: Featured Hacks
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
## Overview
Featured Hacks is a compilation of some of the cool stuff people are building on top of the OP Stack!
## OPCraft
### Author
[Lattice](https://lattice.xyz/)
### Description
OPCraft was an OP Stack chain that ran a modified EVM as the backend for a fully onchain 3D voxel game built with [MUD](https://mud.dev/).
### OP Stack Configuration
- Data Availability: Ethereum DA (Goerli)
- Sequencer: Single Sequencer
- Derivation: Standard Rollup
- Execution: Modified Rollup EVM
### Links
- [Announcing OPCraft: an Autonomous World built on the OP Stack](https://dev.optimism.io/opcraft-autonomous-world/)
- [OPCraft Explorer](https://opcraft.mud.dev/)
- [OPCraft on GitHub](https://github.com/latticexyz/opcraft)
- [MUD](https://mud.dev/)
## Ticking Optimism
### Author
[@therealbytes](https://twitter.com/therealbytes)
### Description
Ticking Optimism is a proof-of-concept implementation of an OP Stack chain that calls a `tick` function every block. By using the OP Stack, Ticking Optimism avoids the need for off-chain infrastructure to execute a function on a regular basis. Ticking Conway is a system that uses Ticking Optimism to build [Conway’s Game of Life](https://conwaylife.com/) onchain.
### OP Stack Configuration
- Data Availability: Ethereum DA (any)
- Sequencer: Single Sequencer
- Derivation: Standard Rollup with custom `tick` function
- Execution: Rollup EVM
### Links
- [Ticking Optimism on GitHub](https://github.com/therealbytes/ticking-optimism)
- [Ticking Conway on GitHub](https://github.com/therealbytes/ticking-conway)
\ No newline at end of file
---
title: Getting Started
lang: en-US
---
## Overview
Hello! This Getting Started guide is meant to help you kick off your OP Stack journey by taking you through the process of spinning up your very own OP Stack chain on the Ethereum Goerli testnet. You can use this chain to perform tests and prepare for the superchain, or you can modify it to adapt it to your own needs (which may make it incompatible with the superchain in the future).
## Know before you go
Before we kick off, note that this is a relatively long tutorial! You should prepare to set aside an hour or two to get everything running. Here’s an itemized list of what we’re about to do:
1. Install dependencies
2. Build the source code
3. Generate and fund accounts and private keys
4. Configure your network
5. Deploy the L1 contracts
6. Initialize op-geth
7. Run op-geth
8. Run op-node
9. Get some Goerli ETH on your L2
10. Send some test transactions
11. Celebrate!
## Prerequisites
You’ll need the following software installed to follow this tutorial:
- [Git](https://git-scm.com/)
- [Go](https://go.dev/)
- [Node](https://nodejs.org/en/)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
- [Foundry](https://github.com/foundry-rs/foundry#installation)
- [Make](https://linux.die.net/man/1/make)
This tutorial was checked on:
| Software | Version | Installation command(s) |
| -------- | ---------- | - |
| Ubuntu | 20.04 LTS | |
| git | OS default | |
| make | 4.2.1-1.2 | `sudo apt install -y make`
| Go | 1.20 | `sudo apt update` <br> `wget https://go.dev/dl/go1.20.linux-amd64.tar.gz` <br> `tar xvzf go1.20.linux-amd64.tar.gz` <br> `sudo cp go/bin/go /usr/bin/go` <br> `sudo mv go /usr/lib` <br> `echo export GOROOT=/usr/lib/go >> ~/.bashrc`
| Node | 16.19.0 | `curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -` <br> `sudo apt-get install -y nodejs`
| yarn | 1.22.19 | `sudo npm install -g yarn`
| Foundry | 0.2.0 | `curl -L https://foundry.paradigm.xyz | bash` <br> `sudo bash` <br> `foundryup`
## Build the Source Code
We’re going to be spinning up an EVM Rollup from the OP Stack source code. You could use docker images, but this way we keep the option to modify component behavior if you need to do so. The OP Stack source code is split between two repositories, the [Optimism Monorepo](https://github.com/ethereum-optimism/optimism) and the [`op-geth`](https://github.com/ethereum-optimism/op-geth) repository.
### Build the Optimism Monorepo
1. Clone the [Optimism Monorepo](https://github.com/ethereum-optimism/optimism).
```bash
cd ~
git clone https://github.com/ethereum-optimism/optimism.git
```
1. Enter the Optimism Monorepo.
```bash
cd optimism
```
1. Install required modules. This is a slow process, while it is running you can already start building `op-geth`, as shown below.
```bash
yarn install
```
1. Build the various packages inside of the Optimism Monorepo.
```bash
make build
```
### Build op-geth
1. Clone [`op-geth`](https://github.com/ethereum-optimism/op-geth):
```bash
cd ~
git clone https://github.com/ethereum-optimism/op-geth.git
```
1. Enter `op-geth`:
```bash
cd op-geth
```
1. Build `op-geth`:
```bash
make geth
```
## Get access to a Goerli node
Since we’re deploying our OP Stack chain to Goerli, you’ll need to have access to a Goerli L1 node. You can either use a node provider like [Alchemy](https://www.alchemy.com/) (easier) or [run your own Goerli node](https://notes.ethereum.org/@launchpad/goerli) (harder).
## Generate some keys
You’ll need four accounts and their private keys when setting up the chain:
- The `Admin` account which has the ability to upgrade contracts.
- The `Batcher` account which publishes Sequencer transaction data to L1.
- The `Proposer` account which publishes L2 transaction results to L1.
- The `Sequencer` account which signs blocks on the p2p network.
You can generate all of these keys with the `rekey` tool in the `contracts-bedrock` package.
1. Enter the Optimism Monorepo:
```bash
cd optimism
```
1. Move into the `contracts-bedrock` package:
```bash
cd packages/contracts-bedrock
```
1. Run the `rekey` command:
```bash
npx hardhat rekey
```
You should get an output like the following:
```
Mnemonic: barely tongue excite actor edge huge lion employ gauge despair this learn
Admin: 0x301c314ca0eedf88a5f7a44680d9dccceb8fcbea
Private Key: ef06ba0291b6e2fa336fd9c06de9c2f18f72ed17cd4fcbda7b376f10592b43d8
Proposer: 0x54355b7d195fcdea96696a522c444c185afaf1a8
Private Key: 8bf67a8cd20087472db00fd869a0ffd7574a4481fb2a07a5f5c6bfb46dcb09ca
Batcher: 0x9a686086e3c74ddd5b59b710b26a73407d9c7e97
Private Key: 1533b607f668cce9553cafbfdfe9529eb31d67f1958d4b16fbdf857a8c50dd56
Sequencer: 0x0324a4c8c1955cb8364e8f07558238b3d2aa5f55
Private Key: fba31658f320bb8ce1ce39fab3c7c2acea6b4dd69cc8483fd85388a461d8426b
```
Save these accounts and their respective private keys somewhere, you’ll need them later. Fund the `Admin` address with a small amount of Goerli ETH as we’ll use that account to deploy our smart contracts. You’ll also need to fund the `Proposer` and `Batcher` address — note that the `Batcher` burns through the most ETH because it publishes transaction data to L1.
Recommended funding amounts are as follows:
- `Admin` — 0.2 ETH
- `Proposer` — 0.5 ETH
- `Batcher` — 1.0 ETH
::: danger Not for production deployments
The `rekey` tool is *not* designed for production deployments. If you are deploying an OP Stack based chain into production, you should likely be using a combination of hardware security modules and hardware wallets.
:::
## Configure your network
Once you’ve built both repositories, you’ll need head back to the Optimism Monorepo to set up the configuration for your chain. Currently, chain configuration lives inside of the [`contracts-bedrock`](https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock) package.
1. Enter the Optimism Monorepo:
```bash
cd ~/optimism
```
1. Move into the `contracts-bedrock` package:
```bash
cd packages/contracts-bedrock
```
1. Before we can create our configuration file, we’ll need to pick an L1 block to serve as the starting point for our Rollup. It’s best to use a finalized L1 block as our starting block. You can use the `cast` command provided by Foundry to grab all of the necessary information (replace `<RPC>` with the URL for your L1 Goerli node):
```bash
cast block finalized --rpc-url <RPC> | grep -E "(timestamp|hash|number)"
```
You’ll get back something that looks like the following:
```
hash 0x784d8e7f0e90969e375c7d12dac7a3df6879450d41b4cb04d4f8f209ff0c4cd9
number 8482289
timestamp 1676253324
```
1. Fill out the remainder of the pre-populated config file found at [`deploy-config/getting-started.json`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/deploy-config/getting-started.json). Use the default values in the config file and make following modifications:
- Replace `"ADMIN"` with the address of the Admin account you generated earlier.
- Replace `"PROPOSER"` with the address of the Proposer account you generated earlier.
- Replace `"BATCHER"` with the address of the Batcher account you generated earlier.
- Replace `"SEQUENCER"` with the address of the Sequencer account you generated earlier.
- Replace `"BLOCKHASH"` with the blockhash you got from the `cast` command.
- Replace `"TIMESTAMP"` with the timestamp you got from the `cast` command. Note that although all the other fields are strings, this field is a number! Don’t include the quotation marks.
## Deploy the L1 contracts
Once you’ve configured your network, it’s time to deploy the L1 smart contracts necessary for the functionality of the chain.
1. Inside of `contracts-bedrock`, copy `.env.example` to `.env`.
```sh
cp .env.example .env
```
1. Fill out the two environment variables inside of that file:
- `L1_RPC` — URL for your L1 node.
- `PRIVATE_KEY_DEPLOYER` — Private key of the `Admin` account.
1. Once you’re ready, deploy the L1 smart contracts:
```bash
npx hardhat deploy --network getting-started
```
Contract deployment can take up to 15 minutes. Please wait for all smart contracts to be fully deployed before continuing to the next step.
## Generate the L2 config files
We’ve set up the L1 side of things, but now we need to set up the L2 side of things. We do this by generating three important files, a `genesis.json` file, a `rollup.json` configuration file, and a `jwt.txt` [JSON Web Token](https://jwt.io/introduction) that allows the `op-node` and `op-geth` to communicate securely.
1. Head over to the `op-node` package:
```bash
cd ~/optimism/op-node
```
1. Run the following command, and make sure to replace `<RPC>` with your L1 RPC URL:
```bash
go run cmd/main.go genesis l2 \
--deploy-config ../packages/contracts-bedrock/deploy-config/getting-started.json \
--deployment-dir ../packages/contracts-bedrock/deployments/getting-started/ \
--outfile.l2 genesis.json \
--outfile.rollup rollup.json \
--l1-rpc <RPC>
```
You should then see the `genesis.json` and `rollup.json` files inside the `op-node` package.
1. Next, generate the `jwt.txt` file with the following command:
```bash
openssl rand -hex 32 > jwt.txt
```
1. Finally, we’ll need to copy the `genesis.json` file and `jwt.txt` file into `op-geth` so we can use it to initialize and run `op-geth` in just a minute:
```bash
cp genesis.json ~/op-geth
cp jwt.txt ~/op-geth
```
## Initialize op-geth
We’re almost ready to run our chain! Now we just need to run a few commands to initialize `op-geth`. We’re going to be running a Sequencer node, so we’ll need to import the `Sequencer` private key that we generated earlier. This private key is what our Sequencer will use to sign new blocks.
1. Head over to the `op-geth` repository:
```bash
cd ~/op-geth
```
1. Create a data directory folder:
```bash
mkdir datadir
```
1. Put a password file into the data directory folder:
```bash
echo "pwd" > datadir/password
```
1. Put the `Sequencer` private key into the data directory folder (don’t include a “0x” prefix):
```bash
echo "<SEQUENCER KEY HERE>" > datadir/block-signer-key
```
1. Import the key into `op-geth`:
```bash
./build/bin/geth account import --datadir=datadir --password=datadir/password datadir/block-signer-key
```
1. Next we need to initialize `op-geth` with the genesis file we generated and copied earlier:
```bash
build/bin/geth init --datadir=datadir genesis.json
```
Everything is now initialized and ready to go!
## Run op-geth
Whew! We made it. It’s time to run `op-geth` and get our system started.
Run `op-geth` with the following command. Make sure to replace `<SEQUENCER>` with the address of the `Sequencer` account you generated earlier.
```bash
./build/bin/geth \
--datadir ./datadir \
--http \
--http.corsdomain="*" \
--http.vhosts="*" \
--http.addr=0.0.0.0 \
--http.api=web3,debug,eth,txpool,net,engine \
--ws \
--ws.addr=0.0.0.0 \
--ws.port=8546 \
--ws.origins="*" \
--ws.api=debug,eth,txpool,net,engine \
--syncmode=full \
--gcmode=full \
--nodiscover \
--maxpeers=0 \
--networkid=42069 \
--authrpc.vhosts="*" \
--authrpc.addr=0.0.0.0 \
--authrpc.port=8551 \
--authrpc.jwtsecret=./jwt.txt \
--rollup.disabletxpoolgossip=true \
--password=./datadir/password \
--allow-insecure-unlock \
--mine \
--miner.etherbase=<SEQUENCER> \
--unlock=<SEQUENCER>
```
And `op-geth` should be running! You should see some output, but you won’t see any blocks being created yet because `op-geth` is driven by the `op-node`. We’ll need to get that running next.
### Reinitializing op-geth
There are several situations are indicate database corruption and require you to reset the `op-geth` component:
- When `op-node` errors out when first started and exits.
- When `op-node` emits this error:
```
stage 0 failed resetting: temp: failed to find the L2 Heads to start from: failed to fetch L2 block by hash 0x0000000000000000000000000000000000000000000000000000000000000000
```
This is the reinitialization procedure:
1. Stop the `op-geth` process.
1. Delete the geth data.
```bash
cd ~/op-geth
rm -rf datadir/geth
```
1. Rerun init.
```bash
build/bin/geth init --datadir=datadir genesis.json
```
1. Start `op-geth`
1. Start `op-node`
## Run op-node
Once we’ve got `op-geth` running we’ll need to run `op-node`. Like Ethereum, the OP Stack has a consensus client (the `op-node`) and an execution client (`op-geth`). The consensus client drives the execution client over the Engine API.
Head over to the `op-node` package and start the `op-node` using the following command. Replace `<SEQUENCERKEY>` with the private key for the `Sequencer` account, replace `<RPC>` with the URL for your L1 node, and replace `<RPCKIND>` with the kind of RPC you’re connected to. Although the `l1.rpckind` argument is optional, setting it will help the `op-node` optimize requests and reduce the overall load on your endpoint. Available options for the `l1.rpckind` argument are `"alchemy"`, `"quicknode"`, `"quicknode"`, `"parity"`, `"nethermind"`, `"debug_geth"`, `"erigon"`, `"basic"`, and `"any"`.
```bash
./bin/op-node \
--l2=http://localhost:8551 \
--l2.jwt-secret=./jwt.txt \
--sequencer.enabled \
--sequencer.l1-confs=3 \
--verifier.l1-confs=3 \
--rollup.config=./rollup.json \
--rpc.addr=0.0.0.0 \
--rpc.port=8547 \
--p2p.listen.ip=0.0.0.0 \
--p2p.listen.tcp=9003 \
--p2p.listen.udp=9003 \
--rpc.enable-admin \
--p2p.sequencer.key=<SEQUENCERKEY> \
--l1=<RPC> \
--l1.rpckind=<RPCKIND>
```
Once you run this command, you should start seeing the `op-node` begin to process all of the L1 information after the starting block number that you picked earlier. Once the `op-node` has enough information, it’ll begin sending Engine API payloads to `op-geth`. At that point, you’ll start to see blocks being created inside of `op-geth`. We’re live!
## Run op-batcher
The final component necessary to put all the pieces together is the `op-batcher`. The `op-batcher` takes transactions from the Sequencer and publishes those transactions to L1. Once transactions are on L1, they’re officially part of the Rollup. Without the `op-batcher`, transactions sent to the Sequencer would never make it to L1 and wouldn’t become part of the canonical chain. The `op-batcher` is critical!
1. Head over to the `op-batcher` package inside the Optimism Monorepo:
```bash
cd ~/optimism/op-batcher
```
1. And run the `op-batcher` using the following command. Replace `<RPC>` with your L1 node URL and replace `<BATCHERKEY>` with the private key for the `Batcher` account that you created and funded earlier. It’s best to give the `Batcher` at least 1 Goerli ETH to ensure that it can continue operating without running out of ETH for gas.
```bash
./bin/op-batcher \
--l2-eth-rpc=http://localhost:8545 \
--rollup-rpc=http://localhost:8547 \
--poll-interval=1s \
--sub-safety-margin=6 \
--num-confirmations=1 \
--safe-abort-nonce-too-low-count=3 \
--resubmission-timeout=30s \
--rpc.addr=0.0.0.0 \
--rpc.port=8548 \
--target-l1-tx-size-bytes=2048 \
--l1-eth-rpc=<RPC> \
--private-key=<BATCHERKEY>
```
## Get some ETH on your Rollup
Once you’ve connected your wallet, you’ll probably notice that you don’t have any ETH on your Rollup. You’ll need some ETH to pay for gas on your Rollup. The easiest way to deposit Goerli ETH into your chain is to send funds directly to the `OptimismPortalProxy` contract. You can find the address of the `OptimismPortalProxy` contract for your chain by looking inside the `deployments` folder in the `contracts-bedrock` package.
1. First, head over to the `contracts-bedrock` package:
```bash
cd ~/optimism/packages/contracts-bedrock
```
1. Grab the address of the `OptimismPortalProxy` contract:
```bash
cat deployments/getting-started/OptimismPortalProxy.json | grep \"address\":
```
You should see a result like the following (**your address will be different**):
```
"address": "0x264B5fde6B37fb6f1C92AaC17BA144cf9e3DcFE9",
"address": "0x264B5fde6B37fb6f1C92AaC17BA144cf9e3DcFE9",
```
1. Grab the `OptimismPortalProxy` address and, using the wallet that you want to have ETH on your Rollup, send that address a small amount of ETH on Goerli (0.1 or less is fine). It may take up to 5 minutes for that ETH to appear in your wallet on L2.
## Use your Rollup
Congratulations, you made it! You now have a complete OP Stack based EVM Rollup.
To see your rollup in action, you can use the [Optimism Mainnet Getting Started tutorial](https://github.com/ethereum-optimism/optimism-tutorial/blob/main/getting-started). Follow these steps:
1. Clone the tutorials repository.
```bash
cd ~
git clone https://github.com/ethereum-optimism/optimism-tutorial.git
```
1. Change to the Foundry directory of the Getting Started tutorial.
```bash
cd optimism-tutorial/getting-started/foundry
```
1. Put your mnemonic (for the address where you have ETH, the one that sent ETH to `OptimismPortalProxy` on Goerli) in a file `mnem.delme`.
1. Provide the URL to your blockchain:
```bash
export ETH_RPC_URL=http://localhost:8545
```
1. Compile and deploy the `Greeter` contract:
```bash
forge create --mnemonic-path ./mnem.delme Greeter --constructor-args "hello" \
| tee deployment
```
1. Set the greeter to the deployed to address:
```bash
export GREETER=`cat deployment | awk '/Deployed to:/ {print $3}'`
echo $GREETER
```
1. See and modify the greeting
```bash
cast call $GREETER "greet()" | cast --to-ascii
cast send --mnemonic-path mnem.delme $GREETER "setGreeting(string)" "New greeting"
cast call $GREETER "greet()" | cast --to-ascii
```
To use any other development stack, see the getting started tutorial, just replace the Greeter address with the address of your rollup, and the Optimism Goerli URL with `http://localhost:8545`.
## Rollup operations
### Stopping your Rollup
To stop `op-geth` you should use Ctrl-C.
If `op-geth` aborts (for example, because the computer it is running on crashes), you will get these errors on `op-node`:
```
WARN [02-16|21:22:02.868] Derivation process temporary error attempts=14 err="stage 0 failed resetting: temp: failed to find the L2 Heads to start from: failed to fetch L2 block by hash 0x0000000000000000000000000000000000000000000000000000000000000000: failed to determine block-hash of hash 0x0000000000000000000000000000000000000000000000000000000000000000, could not get payload: not found"
```
In that case, you need to remove `datadir`, reinitialize it:
```bash
cd ~/op-geth
rm -rf datadir
mkdir datadir
echo "pwd" > datadir/password
echo "<SEQUENCER KEY HERE>" > datadir/block-signer-key
./build/bin/geth account import --datadir=./datadir --password=./datadir/password ./datadir/block-signer-key
./build/bin/geth init --datadir=./datadir ./genesis.json
```
### Starting your Rollup
To restart the blockchain, use the same order of components you did when you initialized it.
1. `op-geth`
2. `op-node`
3. `op-batcher`
## Adding nodes
To add nodes to the rollup, you need to initialize `op-node` and `op-geth`, similar to what you did for the first node:
1. Configure the OS and prerequisites as you did for the first node.
1. Build the Optimism monorepo and `op-geth` as you did for the first node.
1. Copy from the first node these files:
```bash
~/op-geth/genesis.json
~/optimism/op-node/rollup.json
```
1. Create a new `jwt.txt` file as a shared secret:
```bash
cd ~/op-geth
openssl rand -hex 32 > jwt.txt
cp jwt.txt ~/optimism/op-node
```
1. Initialize the new op-geth:
```bash
cd ~/op-geth
./build/bin/geth init --datadir=./datadir ./genesis.json
```
1. Start `op-geth` (using the same command line you used on the initial node)
1. Start `op-node` (using the same command line you used on the initial node)
1. Wait while the node synchronizes
## What’s next?
You can use this rollup the same way you’d use any other test blockchain. Once the superchain is available, this blockchain should be able to join the test version. Alternatively, you could [modify the blockchain in various ways](./hacks.md). **Please note that OP Stack Hacks are unofficial and are not explicitly supported by the OP Stack.** You will not be able to receive significant developer support for any modifications you make to the OP Stack.
\ No newline at end of file
---
title: Introduction to OP Stack Hacks
lang: en-US
---
Welcome to OP Stack Hacks, the **highly experimental** region of the OP Stack docs. OP Stack Hacks are an unofficial guide for messing around with the OP Stack. Here you’ll find information about ways that the OP Stack can be modified in interesting ways.
OP Stack Hacks create blockchains that aren’t exactly OP Stack, and may be insecure. Hacked OP Stack chains can break key invariants that are required to interoperate with [the Optimism Superchain](../understand/explainer.md). **Developers of chains that wish to interoperate with [the Optimism Superchain](../understand/explainer.md) should *not* include any hacks**. When in doubt, stick with the official components within [the current release of the OP Stack](../releases/README.md#current-release).
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
---
title: Settlement Hacks
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
# Overview
The Settlement Layer includes modules that are used by third-party chains to establish a *view* of the state of your OP Stack chain. This view can then be used by applications on those chains to make decisions based on the state of your OP Stack chain. Third-party chains can be any other blockchain, including other OP Stack chains. One common Settlement Layer mechanism is a withdrawal system that allows users to send assets from your OP Stack chain to the third-party chain. Modifications to this layer typically involve introducing new modules or tweaking the security model of existing modules.
## Default
The default Settlement Layer module is currently the Attestation Proof Optimistic Settlement module. This module allows a third-party chain to become aware of the state of an OP Stack chain through an Optimistic protocol where challenges can be executed alongside a threshold of attestations from a pre-defined set of addresses over a state that differs from the proposed state. With a Cannon fault proof shipped to production, this default module can be replaced with a module that allows anyone to challenge proposals by playing the Cannon dispute game.
## Security
Modifications to the Settlement Layer can strongly impact the security of common mechanisms like user withdrawals. A decreased withdrawal delay can, for instance, open the door to gas spam attacks that make challenges exceedingly expensive. It is generally not recommended to modify the Settlement Layer unless you know what you’re doing.
## Modding
### Tweaked parameters
One simple modification to the Settlement Layer is to tweak the parameters of the default Optimistic asset withdrawal mechanism. For example, the withdrawal period can be reduced if a smaller withdrawal period would be sufficient to secure your system.
### Custom proofs
Settlement Layer modules use a proof system to verify the correctness of the state of your OP Stack chain as proposed on the third-party chain. In general, these proofs are either Optimistic proofs that require a withdrawal delay or Validity proofs that use a mathematical proof system to assert the validity of the proposal. The current Attestation Proof Optimistic Settlement module could be replaced with a fault proof system.
### Multiple modules
There is no requirement that a system only have one Settlement Layer module. It is possible to use one or more Settlement Layer modules on one or more third-party chains. A system that aims to bridge assets between two chains will likely need to use one Data Availability Layer module and one Settlement Layer module per chain.
\ No newline at end of file
---
title: Adding Attributes to the Derivation Function
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
## Overview
In this guide, we’ll modify the Bedrock Rollup. Although there are many ways to modify the OP Stack, we’re going to spend this tutorial modifying the Derivation function. Specifically, we’re going to update the Derivation function to track the amount of ETH being burned on L1! Who’s gonna tell [ultrasound.money](http://ultrasound.money) that they should replace their backend with an OP Stack chain?
## Getting the idea
Let’s quickly recap what we’re about to do. The `op-node` is responsible for generating the Engine API payloads that trigger `op-geth` to produce blocks and transactions. The `op-node` already generates a “system transaction” for every L1 block that relays information about the current L1 state to the L2 chain. We’re going to modify the `op-node` to add a new system transaction that reports the total burn amount (the base fee multiplied by the gas used) in each block.
Although it might sound like a lot, the whole process only involves deploying a single smart contract, adding one new file to `op-node`, and modifying one existing file inside `op-node`. It’ll be painless. Let’s go!
## Deploy the burn contract
We’re going to use a smart contract on our Rollup to store the reports that the `op-node` makes about the L1 burn. Here’s the code for our smart contract:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title L1Burn
* @notice L1Burn keeps track of the total amount of ETH burned on L1.
*/
contract L1Burn {
/**
* @notice Total amount of ETH burned on L1.
*/
uint256 public total;
/**
* @notice Mapping of blocks numbers to total burn.
*/
mapping (uint64 => uint256) public reports;
/**
* @notice Allows the system address to submit a report.
*
* @param _blocknum L1 block number the report corresponds to.
* @param _burn Amount of ETH burned in the block.
*/
function report(uint64 _blocknum, uint64 _burn) external {
require(
msg.sender == 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001,
"L1Burn: reports can only be made from system address"
);
total += _burn;
reports[_blocknum] = total;
}
/**
* @notice Tallies up the total burn since a given block number.
*
* @param _blocknum L1 block number to tally from.
*
* @return Total amount of ETH burned since the given block number;
*/
function tally(uint64 _blocknum) external view returns (uint256) {
return total - reports[_blocknum];
}
}
```
Deploy this smart contract to your L2 (using any tool you find convenient). Make a note of the address that the contract is deployed to because you’ll need it in a minute. Simple!
## Add the burn transaction
Now we need to add logic to the `op-node` to automatically submit a burn report whenever an L1 block is produced. Since this transaction is very similar to the system transaction that reports other L1 block info (found in [l1_block_info.go](https://github.com/ethereum-optimism/optimism/blob/c9cd1215b76111888e25ee27a49a0bc0c4eeb0f8/op-node/rollup/derive/l1_block_info.go)), we’ll use that transaction as a jumping-off point.
1. Navigate to the `op-node` package:
```bash
cd ~/optimism/op-node
```
1. Inside of the folder `rollup/derive`, create a new file called `l1_burn_info.go`:
```bash
touch rollup/derive/l1_burn_info.go
```
1. Paste the following into `l1_burn_info.go`, and make sure to replace `YOUR_BURN_CONTRACT_HERE` with the address of the `L1Burn` contract you just deployed.
```go
package derive
import (
"bytes"
"encoding/binary"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum-optimism/optimism/op-node/eth"
)
const (
L1BurnFuncSignature = "report(uint64,uint64)"
L1BurnArguments = 2
L1BurnLen = 4 + 32*L1BurnArguments
)
var (
L1BurnFuncBytes4 = crypto.Keccak256([]byte(L1BurnFuncSignature))[:4]
L1BurnAddress = common.HexToAddress("YOUR_BURN_CONTRACT_HERE")
)
type L1BurnInfo struct {
Number uint64
Burn uint64
}
func (info *L1BurnInfo) MarshalBinary() ([]byte, error) {
data := make([]byte, L1BurnLen)
offset := 0
copy(data[offset:4], L1BurnFuncBytes4)
offset += 4
binary.BigEndian.PutUint64(data[offset+24:offset+32], info.Number)
offset += 32
binary.BigEndian.PutUint64(data[offset+24:offset+32], info.Burn)
return data, nil
}
func (info *L1BurnInfo) UnmarshalBinary(data []byte) error {
if len(data) != L1InfoLen {
return fmt.Errorf("data is unexpected length: %d", len(data))
}
var padding [24]byte
offset := 4
info.Number = binary.BigEndian.Uint64(data[offset+24 : offset+32])
if !bytes.Equal(data[offset:offset+24], padding[:]) {
return fmt.Errorf("l1 burn tx number exceeds uint64 bounds: %x", data[offset:offset+32])
}
offset += 32
info.Burn = binary.BigEndian.Uint64(data[offset+24 : offset+32])
if !bytes.Equal(data[offset:offset+24], padding[:]) {
return fmt.Errorf("l1 burn tx burn exceeds uint64 bounds: %x", data[offset:offset+32])
}
return nil
}
func L1BurnDepositTxData(data []byte) (L1BurnInfo, error) {
var info L1BurnInfo
err := info.UnmarshalBinary(data)
return info, err
}
func L1BurnDeposit(seqNumber uint64, block eth.BlockInfo, sysCfg eth.SystemConfig) (*types.DepositTx, error) {
infoDat := L1BurnInfo{
Number: block.NumberU64(),
Burn: block.BaseFee().Uint64() * block.GasUsed(),
}
data, err := infoDat.MarshalBinary()
if err != nil {
return nil, err
}
source := L1InfoDepositSource{
L1BlockHash: block.Hash(),
SeqNumber: seqNumber,
}
return &types.DepositTx{
SourceHash: source.SourceHash(),
From: L1InfoDepositerAddress,
To: &L1BurnAddress,
Mint: nil,
Value: big.NewInt(0),
Gas: 150_000_000,
IsSystemTransaction: true,
Data: data,
}, nil
}
func L1BurnDepositBytes(seqNumber uint64, l1Info eth.BlockInfo, sysCfg eth.SystemConfig) ([]byte, error) {
dep, err := L1BurnDeposit(seqNumber, l1Info, sysCfg)
if err != nil {
return nil, fmt.Errorf("failed to create L1 burn tx: %w", err)
}
l1Tx := types.NewTx(dep)
opaqueL1Tx, err := l1Tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to encode L1 burn tx: %w", err)
}
return opaqueL1Tx, nil
}
```
Feel free to take a look at this file if you’re interested. It’s relatively simple, mainly just defining a new transaction type and describing how the transaction should be encoded.
## Insert the burn transactions
Finally, we’ll need to update `~/optimism/op-node/rollup/derive/attributes.go` to insert the new burn transaction into every block. You’ll need to make the following changes:
1. Find these lines:
```go
l1InfoTx, err := L1InfoDepositBytes(seqNumber, l1Info, sysConfig)
if err != nil {
return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err))
}
```
1. After those lines, add this code fragment:
```go
l1BurnTx, err := L1BurnDepositBytes(seqNumber, l1Info, sysConfig)
if err != nil {
return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err))
}
```
1. Immediately following, change these lines:
```go
txs := make([]hexutil.Bytes, 0, 1+len(depositTxs))
txs = append(txs, l1InfoTx)
```
to
```go
txs := make([]hexutil.Bytes, 0, 2+len(depositTxs))
txs = append(txs, l1InfoTx)
txs = append(txs, l1BurnTx)
```
All we’re doing here is creating a new burn transaction after every `l1InfoTx` and inserting it into every block.
## Rebuild your op-node
Before we can see this change take effect, you’ll need to rebuild your `op-node`:
```bash
cd ~/optimism/op-node
make op-node
```
Now start your `op-node` if it isn’t running or restart your `op-node` if it’s already running. You should see the change immediately — new blocks will contain two system transactions instead of just one!
## Checking the result
Query the `total` function of your contract, you should also start to see the total slowly increasing. Play around with the `tally` function to grab the amount of gas burned since a given L2 block. You could use this to implement a version of [ultrasound.money](http://ultrasound.money) that keeps track of things with an OP Stack as a backend. We did it reddit!
One way to get the total is to run these commands:
```bash
export ETH_RPC_URL=http://localhost:8545
cast call <YOUR_BURN_CONTRACT_HERE> "total()" | cast --from-wei
```
## Conclusion
With just a few tiny changes to the `op-node`, you were just able to implement a change to the OP Stack that allows you to keep track of the L1 ETH burn on L2. With a live Cannon fault proof system, you should not only be able to track the L1 burn on L2, you should be able to *prove* the burn to contracts back on L1. You could build a trustless prediction market on the amount of ETH burned. That’s crazy!
The OP Stack is an extremely powerful platform that allows you to perform a large amount of computation trustlessly. It’s a superpower for smart contracts. Tracking the L1 burn is just one of the many, many wild things you can do with the OP Stack. If you’re looking for inspiration or you want to see what others are building on the OP Stack, check out our OP Stack Hacks page. Maybe you’ll find a project you want to work on, or maybe you’ll get the inspiration you need to build the next killer smart contract.
\ No newline at end of file
---
title: Adding a Precompile
lang: en-US
---
::: warning 🚧 OP Stack Hacks are explicitly things that you can do with the OP Stack that are *not* currently intended for production use
OP Stack Hacks are not for the faint of heart. You will not be able to receive significant developer support for OP Stack Hacks — be prepared to get your hands dirty and to work without support.
:::
One possible use of OP Stack is to run an EVM with a new precompile for operations to speed up calculations that are not currently supported. In this case we’ll make a simple precompile that returns a constant value if it’s called with four or less bytes, or an error if it is called with more than that.
To create a new precompile, the file to modify is [`op-geth/core/vm/contracts.go`](https://github.com/ethereum-optimism/op-geth/blob/optimism-history/core/vm/contracts.go).
1. Add to `PrecompiledContractsBerlin` on line 82 (or a later fork, if the list of precompiles changes again) a structure named after your new precompile, with an address that is unlikely to ever clash with a standard precompile (0x100, for example):
```go
common.BytesToAddress([]byte{1,0}): &retConstant{},
```
1. Add the lines for the precompile.
```go
type retConstant struct{}
func (c *retConstant) RequiredGas(input []byte) uint64 {
return uint64(1024)
}
var (
errConstInvalidInputLength = errors.New("invalid input length")
)
func (c *retConstant) Run(input []byte) ([]byte, error) {
// Only allow input up to four bytes (function signature)
if len(input) > 4 {
return nil, errConstInvalidInputLength
}
output := make([]byte, 6)
for i := 0; i < 6; i++ {
output[i] = byte(64+i)
}
return output, nil
}
```
1. Stop `op-geth` and recompile:
```bash
cd ~/op-geth
make geth
```
1. Restart `op-geth`.
1. Run these command to see the result of calling the precompile successfully, and the result of an error:
```bash
cast call 0x0000000000000000000000000000000000000100 "whatever()"
cast call 0x0000000000000000000000000000000000000100 "whatever(string)" "fail"
```
## How does it work?
This is the precompile interface definition:
```go
type PrecompiledContract interface {
RequiredGas(input []byte) uint64 // RequiredPrice calculates the contract gas use
Run(input []byte) ([]byte, error) // Run runs the precompiled contract
}
```
It means that for every precompile we need two functions:
- `RequiredGas` which returns the gas cost for the call. This function takes an array of bytes as input, and returns a single value, the gas cost.
- `Run` which runs the actual precompile. This function also takes an array of bytes, but it returns two values: the call’s output (a byte array) and an error.
For every fork that changes the precompiles we have a [`map`](https://www.w3schools.com/go/go_maps.php)from addresses to the `PrecompiledContract` definitions:
```go
// PrecompiledContractsBerlin contains the default set of pre-compiled Ethereum
// contracts used in the Berlin release.
var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
.
.
.
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{1,0}): &retConstant{},
}
```
The key of the map is an address. We create those from bytes using `common.BytesToAddress([]byte{<bytes to convert to address go here>})`. In this case we have two bytes, `0x01` and `0x00`. Together we get the address `0x0…0100`.
The syntax for a precompiled contract interface is `&<variable name>{}`.
The next step is to define the precompiled contract itself.
```go
type retConstant struct{}
```
First we create a structure for the precompile.
```go
func (c *retConstant) RequiredGas(input []byte) uint64 {
return uint64(1024)
}
```
Then we define a function as part of that structure. Here we just require a constant amount of gas, but of course the calculation can be a lot more sophisticated.
```go
var (
errConstInvalidInputLength = errors.New("invalid input length")
)
```
Next we define a variable for the error.
```go
func (c *retConstant) Run(input []byte) ([]byte, error) {
```
This is the function that actually executes the precompile.
```go
// Only allow input up to four bytes (function signature)
if len(input) > 4 {
return nil, errConstInvalidInputLength
}
```
Return an error if warranted. The reason this precompile allows up to four bytes of input is that any standard call (for example, using `cast`) is going to have at least four bytes for the function signature.
`return a, b` is the way we return two values from a function in Go. The normal output is `nil`, nothing, because we return an error.
```go
output := make([]byte, 6)
for i := 0; i < 6; i++ {
output[i] = byte(64+i)
}
return output, nil
}
```
Finally, we create the output buffer, fill it, and then return it.
## Conclusion
An OP Stack chain with additional precompiles can be useful, for example, to further reduce the computational effort required for cryptographic operations by moving them from interpreted EVM code to compiled Go code.
\ No newline at end of file
---
title: Contribute to OP Stack
lang: en-US
---
The OP Stack is a collaborative, decentralized development stack that only gets more powerful as more people contribute. Code for the OP Stack should follow the stack’s [design principles](./understand/design-principles.md), which means it should be entirely open source and accessible for people to hack on, contribute to, and extend. The Optimism Collective wins when it works together. ♥️✨
Whether you’re a budding protocol developer, dapp developer, bounty hunter, documentation editor, content creator, or anything in between, the OP Stack always has something for you to contribute to. Every contribution makes a difference — no contribution is too small.
If you’re looking to find a way to contribute, check out one of the following contributor pathways below. Come make your first contribution today!
## Component contributions
The OP Stack is a decentralized development stack. Anyone can contribute components that can be considered part of the OP Stack as long as as those components fit [the stack’s design principles and goals](./understand/design-principles.md). To start contributing components to the stack, check out some of [these useful ideas](https://github.com/ethereum-optimism/optimism-project-ideas) and get to building! And don’t forget that projects can also receive funding from the Collective via RetroPGF.
If you’d like to contribute to existing OP Stack code, rather than creating new components, check out [the current release of the OP Stack](./releases/README.md#current-release). Any contributions to existing OP Stack components are highly appreciated. If you’re looking for a good way to make your first contribution, check out the [Good First Issues](https://github.com/ethereum-optimism/optimism/contribute) on the Optimism Monorepo.
## Bounty hunting
The OP Stack needs YOU (yes you!) to help review the codebase for bugs and vulnerabilities. If you’re interested in bounty hunting, check out our Security Policy, Vulnerability Reporting, and Bug Bounties page.
## Documentation help
Spot a typo in these docs? See something that could be a little clearer? Head over to the Optimism Monorepo where the OP Stack docs are hosted and make a pull request. No contribution is too small!
## Community contributions
If you’re looking for other ways to get involved, here are a few options:
- Grab an idea from the [project ideas list](https://github.com/ethereum-optimism/optimism-project-ideas) to and building
- Suggest a new idea for the [project ideas list](https://github.com/ethereum-optimism/optimism-project-ideas)
- Improve the [Optimism Community Hub](https://community.optimism.io/) [documentation](https://github.com/ethereum-optimism/community-hub) or [tutorials](https://github.com/ethereum-optimism/optimism-tutorial)
- Become an Optimism Ambassador, Support Nerd, and more in the [Optimism Discord](https://discord-gateway.optimism.io/)
\ No newline at end of file
---
title: Release History
lang: en-US
---
The OP Stack codebase is a decentralized development stack maintained by the Optimism Collective and built to power Optimism.
The OP Stack is constantly evolving as new layers and modules are developed. The OP Stack codebase is also not a product (in the traditional sense) but rather a collection of software components that power the Optimism ecosystem.
**A “Release” of the OP Stack codebase is a particular set of software components that are production-ready and which fit the stack’s design principles and goals.**
Only the software components included within the Current Release of the OP Stack codebase are considered in the scope of the OP Stack. Any usage of the OP Stack outside of the official, intended capabilities of the Current Release are considered [OP Stack Hacks](../build/hacks.md) — unofficial modifications that are useful for experimentation but could have unforeseen results, such as security vulnerabilities, and are likely cause your chain to no longer be interoperable with [the Optimism Superchain](https://app.optimism.io/superchain/). **Developer support for OP Stack Hacks is limited — when in doubt, stick to the capabilities of the Current Release!**
## Current Release
[OP Stack codebase V1: Bedrock](./bedrock/)
## Past Releases
N/A
\ No newline at end of file
---
title: OP Stack codebase V1 - Bedrock
lang: en-US
---
## Overview
The first release of the OP Stack codebase is called **Bedrock**.
The Bedrock release primarily consists of the core software required to run L2 blockchains and was originally designed to power an upgrade to the Optimism Mainnet network.
## Resources
### Rollup Protocol
Learn about the basics of the Rollup protocol used by Bedrock on the [Rollup Protocol](https://community.optimism.io/docs/protocol/2-rollup-protocol/) page.
### Bedrock Explainer
Learn all about the Bedrock release of the OP Stack by reading the [Bedrock Explainer](./explainer.md).
### Specifications
Dive deep into the specifications for the Bedrock release in the [specs folder of the Optimism Monorepo](https://github.com/ethereum-optimism/optimism/blob/develop/specs/README.md).
## Components
- [`op-node`](https://github.com/ethereum-optimism/optimism/tree/develop/op-node)
- [`op-geth`](https://github.com/ethereum-optimism/op-geth)
- [`op-batcher`](https://github.com/ethereum-optimism/optimism/tree/develop/op-batcher)
- [`op-proposer`](https://github.com/ethereum-optimism/optimism/tree/develop/op-proposer)
- [`contracts-bedrock`](https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock)
- [`fault-detector`](https://github.com/ethereum-optimism/optimism/tree/develop/packages/fault-detector)
- [`sdk`](https://github.com/ethereum-optimism/optimism/tree/develop/packages/sdk)
- [`chain-mon`](https://github.com/ethereum-optimism/optimism/tree/develop/packages/chain-mon)
\ No newline at end of file
---
title: Differences between Bedrock and L1 Ethereum
lang: en-US
---
It's important to note that there are various minor discrepancies between the behavior of Optimism and Ethereum.
You should be aware of these descrepancies when building apps on top of Optimism or the OP Stack codebase.
## Opcode Differences
| Opcode | Solidity equivalent | Behavior |
| - | - | - |
| `COINBASE` | `block.coinbase` | Undefined |
| `DIFFICULTY` | `block.difficulty` | Random value. As this value is set by the sequencer, it is not as reliably random as the L1 equivalent. |
| `NUMBER` | `block.number` | L2 block number
| `TIMESTAMP` | `block.timestamp` | Timestamp of the L2 block
| `ORIGIN` | `tx.origin` | If the transaction is an L1 ⇒ L2 transaction, then `tx.origin` is set to the [aliased address](#address-aliasing) of the address that triggered the L1 ⇒ L2 transaction. Otherwise, this opcode behaves normally. |
| `CALLER` | `msg.sender` | If the transaction is an L1 ⇒ L2 transaction, and this is the initial call (rather than an internal transaction from one contract to another), the same [address aliasing](#address-aliasing) behavior applies.
::: tip `tx.origin == msg.sender`
On L1 Ethereum `tx.origin` is equal to `msg.sender` only when the smart contract was called directly from an externally owned account (EOA).
However, on Optimism `tx.origin` is the origin *on Optimism*.
It could be an EOA.
However, in the case of messages from L1, it is possible for a message from a smart contract on L1 to appear on L2 with `tx.origin == msg.origin`.
This is unlikely to make a significant difference, because an L1 smart contract cannot directly manipulate the L2 state.
However, there could be edge cases we did not think about where this matters.
:::
### Accessing L1 information
If you need the equivalent information from the latest L1 block, you can get it from [the `L1Block` contract](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/contracts/L2/L1Block.sol).
This contract is a predeploy at address [`0x4200000000000000000000000000000000000015`](https://goerli-optimism.etherscan.io/address/0x4200000000000000000000000000000000000015).
You can use [the getter functions](https://docs.soliditylang.org/en/v0.8.12/contracts.html#getter-functions) to get these parameters:
- `number`: The latest L1 block number known to L2
- `timestamp`: The timestamp of the latest L1 block
- `basefee`: The base fee of the latest L1 block
- `hash`: The hash of the latest L1 block
- `sequenceNumber`: The number of the L2 block within the epoch (the epoch changes when there is a new L1 block)
### Address Aliasing
<details>
Because of the behavior of the `CREATE` opcode, it is possible for a user to create a contract on L1 and on L2 that share the same address but have different bytecode.
This can break trust assumptions, because one contract may be trusted and another be untrusted (see below).
To prevent this problem the behavior of the `ORIGIN` and `CALLER` opcodes (`tx.origin` and `msg.sender`) differs slightly between L1 and L2.
The value of `tx.origin` is determined as follows:
| Call source | `tx.origin` |
| ---------------------------------- | ------------------------------------------ |
| L2 user (Externally Owned Account) | The user's address (same as in Ethereum) |
| L1 user (Externally Owned Account) | The user's address (same as in Ethereum) |
| L1 contract (using `CanonicalTransactionChain.enqueue`) | `L1_contract_address + 0x1111000000000000000000000000000000001111` |
The value of `msg.sender` at the top-level (the very first contract being called) is always equal to `tx.origin`.
Therefore, if the value of `tx.origin` is affected by the rules defined above, the top-level value of `msg.sender` will also be impacted.
Note that in general, [`tx.origin` should *not* be used for authorization](https://docs.soliditylang.org/en/latest/security-considerations.html#tx-origin).
However, that is a separate issue from address aliasing because address aliasing also affects `msg.sender`.
#### Why is address aliasing an issue?
The problem with two identical source addresses (the L1 contract and the L2 contract) is that we extend trust based on the address.
It is possible that we will want to trust one of the contracts, but not the other.
1. Helena Hacker forks [Uniswap](https://uniswap.org/) to create her own exchange (on L2), called Hackswap.
**Note:** There are actually multiple contracts in Uniswap, so this explanation is a bit simplified.
[See here if you want additional details](https://ethereum.org/en/developers/tutorials/uniswap-v2-annotated-code/).
1. Helena Hacker provides Hackswap with liquidity that appears to allow for profitable arbitrage opportunities.
For example, she can make it so that you can spend 1 [DAI](https://www.coindesk.com/price/dai/)to buy 1.1 [USDT](https://www.coindesk.com/price/tether/).
Both of those coins are supposed to be worth exactly $1.
1. Nimrod Naive knows that if something looks too good to be true it probably is.
However, he checks the Hackswap contract's bytecode and verifies it is 100% identical to Uniswap.
He decides this means the contract can be trusted to behave exactly as Uniswap does.
1. Nimrod approves an allowance of 1000 DAI for the Hackswap contract.
Nimrod expects to call the swap function on Hackswap and receive back nearly 1100 USDT.
1. Before Nimrod's swap transaction is sent to the blockchain, Helena Hacker sends a transaction from an L1 contract with the same address as Hackswap.
This transaction transfers 1000 DAI from Nimrod's address to Helena Hacker's address.
If this transaction were to come from the same address as Hackswap on L2, it would be able to transfer the 1000 DAI because of the allowance Nimrod *had* to give Hackswap in the previous step to swap tokens.
Nimrod, despite his naivete, is protected because Optimism modified the transaction's `tx.origin` (which is also the initial `msg.sender`).
That transaction comes from a *different* address, one that does not have the allowance.
**Note:** It is simple to create two different contracts on the same address in different chains.
But it is nearly impossible to create two that are different by a specified amount, so Helena Hacker can't do that.
</details>
## Blocks
There are several differences in the way blocks are produced between L1 Ethereum and Optimism Bedrock.
| Parameter | L1 Ethereum | Optimism Bedrock |
| - | - | - |
| Time between blocks | 12 seconds(1) | 2 seconds |
| Block target size | 15,000,000 gas | to be determined |
| Block maximum size | 30,000,000 gas | to be determined |
(1) This is the ideal.
If any blocks are missed it could be an integer multiple such as 24 seconds, 36 seconds, etc.
**Note:** The L1 Ethereum parameter values are taken from [ethereum.org](https://ethereum.org/en/developers/docs/blocks/#block-time). The Optimism Bedrock values are taken from [the Optimism specs](https://github.com/ethereum-optimism/optimism/blob/develop/specs/guaranteed-gas-market.md#limiting-guaranteed-gas).
## Network specifications
### JSON-RPC differences
OP Stack codebase uses the same [JSON-RPC API](https://eth.wiki/json-rpc/API) as Ethereum.
Some additional OP Stack specific methods have been introduced.
See the full list of [custom JSON-RPC methods](https://community.optimism.io/docs/developers/build/json-rpc/) for more information.
### Pre-EIP-155 support
[Pre-EIP-155](https://eips.ethereum.org/EIPS/eip-155) transactions do not have a chain ID, which means a transaction on one Ethereum blockchain can be replayed on others.
This is a security risk, so pre-EIP-155 transactions are not supported on OP Stack by default.
## Transaction costs
[By default, transaction costs on OP Stack chains](https://community.optimism.io/docs/developers/build/transaction-fees/) include an [L2 execution fee](https://community.optimism.io/docs/developers/build/transaction-fees#the-l2-execution-fee) and an [L1 data fee](https://community.optimism.io/docs/developers/build/transaction-fees#the-l1-data-fee).
---
title: Bedrock Explainer
lang: en-US
meta:
- name: og:image
content: https://dev.optimism.io/content/images/size/w2000/2022/12/bedrock-BLUE.jpg
---
![Bedrock](https://dev.optimism.io/content/images/size/w2000/2022/12/bedrock-BLUE.jpg)
::: tip Staying up to date
[Stay up to date on the Superchain and the OP Stack by subscribing to the Optimism newsletter](https://optimism.us6.list-manage.com/subscribe/post?u=9727fa8bec4011400e57cafcb&id=ca91042234&f_id=002a19e3f0).
:::
Bedrock is the name of the first ever official release of the OP Stack codebase, which is a set of free and open-source modular components that work together to power Optimism.
- To understand what is in the Bedrock release, keep reading.
- To develop on Optimism Mainnet, which will upgrade its infrastructure to the Bedrock release, read the docs.
- To contribute to the OP Stack, see the contribution guidelines on the ethereum-optimism monorepo.
## Summary of Improvements
Bedrock improves on its predecessor by reducing transaction fees using optimized batch [compression](#optimized-data-compression) and Ethereum as a data availability layer; shortening delays of including L1 transactions in rollups by handling L1 re-orgs more gracefully; enabling modular proof systems through code re-use; and improving node performance by removing technical debt.
### Lower fees
In addition, Bedrock implements an optimized data [compression](#optimized-data-compression) strategy to minimize data costs. We are currently benchmarking the impact of this change, but we expect it to reduce fees significantly.
Bedrock also removes all gas costs associated with EVM execution when submitting data to L1. This reduces fees by an additional 10% over the previous version of the protocol.
### Shorter deposit times
Bedrock introduces support for L1 re-orgs in the node software, which significantly reduces the amount of time users need to wait for deposits. Earlier versions of the protocol could take up to 10 minutes to confirm deposits. With Bedrock, we expect deposits to confirm within 3 minutes.
### Improved proof modularity
Bedrock abstracts the proof system from the OP Stack so that a rollup may use either a fault proof or validity proof (e.g., a zk-SNARK) to prove correct execution of inputs on the rollup. This abstraction enables systems like [Cannon](https://github.com/ethereum-optimism/cannon) to be used to prove faults in the system.
### Improved node performance
The node software performance has been significantly improved by enabling execution of several transactions in a single rollup "block" as opposed to the prior "one transaction per block" model in the previous version. This allows the cost of merkle trie updates to be amortized across multiple transactions. At current transaction volume, this reduces state growth by approximately 15GB/year.
Node performance is further improved by removing technical debt from the previous version of the protocol. This includes removing the need for a separate "data transport layer" node to index L1, and updating the node software to efficiently query for transaction data from L1.
### Improved Ethereum equivalence
Bedrock was designed from the ground up to be as close to Ethereum as possible. Multiple deviations from Ethereum in the previous version of the protocol have been removed, including:
1. The one-transaction-per-block model.
2. Custom opcodes to get L1 block information.
3. Separate L1/L2 fee fields in the JSON-RPC API.
4. A custom ERC20 representation of ETH balances.
Bedrock also adds support for EIP-1559, chain re-orgs, and other Ethereum features present on L1.
## Design Principles
Bedrock was built to be modular & upgradeable, to reuse existing code from Ethereum, and to be as close to 100% Ethereum-equivalent as possible.
### Modularity
Bedrock makes it easy to swap out different components in the OP Stack codebase and add new capabilities by using well-defined interfaces and versioning schemes. This allows for a flexible architecture that can adapt to future developments in the Ethereum ecosystem.
Examples:
- Separation of [rollup node](#rollup-node) and execution client
- Modular fault proof design
### Code re-use
Bedrock uses existing Ethereum architecture and infrastructure as much as possible. This approach enables the OP Stack to inherit security and "lindy" benefits from the battle-tested codebases used in production on Ethereum Mainnet. You'll find examples of this throughout the design including:
Examples:
- [Minimally modified execution clients](https://op-geth.optimism.io/)
- EVM contracts instead of precompiled client code
### Ethereum equivalence
Bedrock is designed to have maximum compatibility with the existing Ethereum developer experience. A few exceptions exist due to fundamental differences between an L1 and a rollup: an altered fee model, faster block time (2s vs 12s), and a special transaction type for including L1 [deposit](#deposits) transactions.
Examples:
- Fault proof designed to prove faults of minimally modified Ethereum [execution client](#execution-client)
- Code re-use of Ethereum [execution client](#execution-client) for use by nodes in the L2 network and sequencers
## Protocol
Rollups are derived from a data availability source (generally an L1 blockchain like Ethereum). In their most common configuration, rollup protocols derive a **"canonical L2 chain"** from two primary sources of information:
1. Transaction data posted by Sequencers to the L1 and;
2. [Deposit](#deposits) transactions posted by accounts and contracts to a bridge contract on L1.
The following are the fundamental components of the protocol:
* Deposits are _writes_ to the canonical L2 chain by directly interacting with smart contracts on the L1.
* Withdrawals are _writes_ to the canonical L2 chain that implicitly trigger interactions with contracts and accounts on the L1.
* Batches are _writes_ of data corresponding to batches on the rollup.
* Block derivation is how _reads_ of data on the L1 are interpreted to understand the canonical L2 chain.
* Proof systems define _finality_ of posted output roots on the L1 such that they may be _executed_ upon (e.g., to execute withdrawals).
### Deposits
A **deposit** is a transaction on L1 that is to be included in the rollup. [Deposits](#deposits) are _guaranteed_ by definition to be included in the [canonical L2 chain](#protocol) as a means of preventing censorship or control of the L2.
#### Arbitrary message passing from L1
A **deposited transaction** is the transaction on the rollup that is made as a part of a [deposit](#deposits). With Bedrock, [deposits](#deposits) are fully generalized Ethereum transactions. For example, an account or contract on Ethereum can “deposit” a contract creation.
Bedrock defines a **deposit contract** that is available on the L1: it is a smart contract that L1 accounts and contracts can interact with to write to the L2. [Deposited transactions](#arbitrary-message-passing-from-l1) on the L2 are derived from the values in the event(s) emitted by this [deposit](#deposits) contract, which include expected parameters such as from, to, and data.
For full details, see the [deposit contract](https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#deposit-contract) section of the protocol specifications.
#### Purchasing guaranteed L2 gas on L1
Bedrock also specifies a gas burn mechanism and a fee market for [deposits](#deposits). The gas that [deposited transactions](#arbitrary-message-passing-from-l1) spend on an L2 is bought on L1 via a gas burn. This gas is purchased on a fee market and there is a hard cap on the amount of gas provided to all [deposits](#deposits) in a single L1 block. This mechanism is used to prevent denial of service attacks that could occur by writing transactions to L2 from L1 that are extremely gas-intensive on L2, but cheap on L1.
The gas provided to [deposited transactions](#arbitrary-message-passing-from-l1) is sometimes called "guaranteed gas." Guaranteed gas is unique in that it is paid for by burning gas on L1 and is therefore not refundable.The total amount of L1 gas that must be burned per unit of guaranteed L2 gas requested depends on the price of L2 gas reported by a EIP-1559 style fee mechanism. Furthermore, users receive a dynamic gas stipend based on the amount of L1 gas spent to compute updates to the fee mechanism.
For a deeper explanation, read the [deposits section](https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#deposits) of the protocol specifications.
### Withdrawals
**Withdrawals** are cross-domain transactions that are initiated on L2 and finalized by a transaction executed on L1. Notably, withdrawals may be used by an L2 account to call an L1 contract, or to transfer ETH from an L2 account to an L1 account.
Withdrawals are initiated on L2 via a call to the **Message Passer** predeploy contract, which records the important properties of the message in its storage. Withdrawals are finalized on L1 via a call to the [OptimismPortal](https://github.com/ethereum-optimism/optimism/blob/develop/specs/withdrawals.md#the-optimism-portal-contract) contract, which proves the inclusion of this withdrawal message. In this way, withdrawals are different from [deposits](#deposits). Instead of relying on [block derivation](#block-derivation), withdrawal transactions must use smart contracts on L1 for finalization.
#### Two-step withdrawals
Withdrawal proof validation bugs have been the root cause of many of the biggest bridge hacks of the last few years. The Bedrock release introduces an additional step in the withdrawals’ process of prior versions meant to provide an extra layer of defense against these types of bugs. In the two-step withdrawal process, a Merkle proof corresponding to the withdrawal must be submitted 7 days before the withdrawal can be finalized.. This new safety mechanism gives monitoring tools a full 7 days to find and detect invalid withdrawal proofs . If the [withdrawal](#withdrawals) proof is found to be invalid, a contract fix can be deployed before funds are lost. This dramatically reduces the risk of a bridge compromise.
For full details, see the [withdrawals](https://github.com/ethereum-optimism/optimism/blob/develop/specs/withdrawals.md) section of the protocol specification.
### Batches
In Bedrock, a wire format is defined for messaging between the L1 and L2 (i.e., for L2 deriving blocks from L1 and for L2 to write transactions to the L1). This wire format is designed to minimize costs and software complexity for writing to the L1.
#### Optimized data compression
To optimize data compression, lists of L2 transactions called **sequencer batches** are organized into groups of objects called **channels**, each of which have a maximum size that is defined in a [configurable parameter](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#channel-format) that will initially be set to ~9.5Mb. These [channels](#optimized-data-compression) are expected to be compressed using a compression function and submitted to the L1.
#### Parallelized batch submission
To parallelize messages from the sequencers that are submitting [compressed](#optimized-data-compression) [channel](#optimized-data-compression) data to the L1, [channels](#optimized-data-compression) are further broken down into **channel frames**, which are chunks of [compressed](#optimized-data-compression) [channel](#optimized-data-compression) data that can fit inside of a single L1 transaction. Given [channel frames](#parallelized-batch-submission) are mutually independent and the ordering is known, the Ethereum transactions sent by the sequencer to the L1 can be sent in parallel which minimizes sequencer software complexity and allows for filling up all available space for data on the L1.
#### Minimized usage of Ethereum gas
Bedrock removes all execution gas used by the L1 system from submitting [channel](#optimized-data-compression) data to the L1 in transactions called **batcher transactions**. All validation logic that was previously happening on smart contracts on the L1 is moved into the [block derivation](#block-derivation) logic. Instead, [batcher transactions](#minimized-usage-of-ethereum-gas) are sent to a single EOA on Ethereum referred to as the **batch inbox address**.
Batches are still subject to validity checks (i.e. they have to be encoded correctly), and so are individual transactions within the batch (e.g. signatures have to be valid). Invalid [batches](#optimized-data-compression) and invalid individual transactions within an otherwise valid batch are considered to be discarded and irrelevant to the system.
> Note: Ethereum will soon upgrade to include [EIP-4844](https://eip4844.com/), which introduces a separate fee market for writing data and an increased cap of the amount of data the Ethereum protocol is willing to store. This change is expected to further decrease the costs associated with posting data to an L1.
For a deeper explanation, read [the wire format specifications](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#overview).
### Block Derivation
In Bedrock, the protocol is designed to guarantee that the timing of [deposits](#deposits) on the L1 is respected with regards to the [block derivation](#block-derivation) of the [canonical L2 chain](#protocol). Doing so is a _pure function_ of data written to the L1 by sequencers, [deposits](#deposits), and L1 block attributes. To accomplish this, the protocol defines strategies for guaranteeing inclusion of deposits, handling L1 and L2 timestamps, and processing sequencing windows in a pipeline to ensure correct ordering.
#### Guaranteed inclusion of deposits
A goal of the [block derivation](#block-derivation) protocol is to define it such that there must be an L2 block every "L2 block time" number of seconds, and that the timestamp of L2 blocks stays in sync with the timestamps of L1 (i.e., to ensure [deposits](#deposits) are included in a logical temporal order).
In Bedrock, the concept of a **sequencing epoch** is introduced: it is a range of L2 blocks derived from a range of L1 blocks. Each [epoch](#guaranteed-inclusion-of-deposits) is identified by an **epoch number**, which is equal to the block number of the first L1 block in the sequencing window. Epochs can vary in size, subject to some constraints.
The batch derivation pipeline treats the timestamps of the L1 blocks associated with [epoch number](#guaranteed-inclusion-of-deposits) as the anchor point for determining the order of transactions on the L2. The protocol guarantees that the first L2 block of an [epoch](#guaranteed-inclusion-of-deposits) never falls behind the timestamp of the L1 block matching the [epoch](#guaranteed-inclusion-of-deposits). The first blocks of an epoch _must_ contain deposits on L1 in order to guarantee that deposits will be processed.
Note that the target configuration for the block time on L2 in the Bedrock release is 2 seconds.
#### Handling L1 and L2 timestamps
Bedrock attempts to address the problem of reconciling the timestamps on L2 with timestamps on L1 present in [deposited transactions](#arbitrary-message-passing-from-l1). It does this by allowing a short window of time for sequencing to liberally apply timestamps on L2 transactions between [epochs](#guaranteed-inclusion-of-deposits).
A **sequencing window** is a sequence of L1 blocks from which an [epoch](#guaranteed-inclusion-of-deposits) can be derived. A [sequencing window](#handling-l1-and-l2-timestamps) whose first L1 block has the number `N` contains [batcher transactions](#minimized-usage-of-ethereum-gas) for [epoch](#guaranteed-inclusion-of-deposits) `N`.
The [sequencing window](#handling-l1-and-l2-timestamps) contains blocks `[N, N + SWS)` where `SWS` is the **sequencer window size**: a fixed rollup-level configuration parameter. This parameter must be at least 2. Increasing it provides more opportunity for sequencers to order L2 transactions with respect to [deposits](#deposits), and lowering it introduces stricter windows of time for sequencers to submit batcher transactions. It is a tradeoff between creating MEV opportunity and increasing software complexity.
A protocol constant called **max sequencer drift** governs the maximum timestamp a block can have within its epoch. Having this drift allows the sequencer to maintain liveness in case of temporary problems connecting to L1. Each L2 block’s timestamp fits within the following range:
```
l1_timestamp <= l2_block.timestamp <= max(l1_timestamp + max_sequencer_drift, l1_timestamp + l2_block_time)
```
#### Block derivation pipeline
The [canonical L2 chain](#protocol) can be processed from scratch by starting with the L2 genesis state, setting the L2 chain inception as the first epoch, and then processing all sequencing windows in order to determine the correct ordering of [sequencer batches](#optimized-data-compression) and [deposits](#deposits) according to the following simplified pipeline:
| **Stage** | **Notes** |
| --- | --- |
| Read from L1 | Epochs are defined by L1 blocks. Contained within an L2 block is data pertaining to [batcher transactions](#minimized-usage-of-ethereum-gas) or [deposits](#deposits) which must be included in the [canonical L2 chain](#protocol) |
| Buffer and decode into [channels](#optimized-data-compression) | The data from L1 blocks contains unordered [channel frames](#parallelized-batch-submission), which must all be collected before reconstructing them into channels. |
| Decompress [channels](#optimized-data-compression) into [batches](#optimized-data-compression) | Since [channels](#optimized-data-compression) are [compressed](#optimized-data-compression) to minimize data fee costs on the L1, they must be decompressed. |
| Queue [batches](#optimized-data-compression) into sequential order | With the latest information from L1, [batches](#optimized-data-compression) can be validated and processed sequentially. There are some nuances to what the correct ordering is in relation to [epochs](#guaranteed-inclusion-of-deposits) and timestamps from L2, see the full specification [here](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#batch-queue). |
| Interpret as L2 blocks | At this point, the correct ordering of [batches](#optimized-data-compression) can be determined.<br><br>Following this, the [execution client](#execution-client) can interpret them into L2 blocks. For implementation details pertaining to [execution clients](#execution-client), see the [engine queue](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#engine-queue) section of the protocol specifications. |
### Fault Proofs
After a sequencer processes one or more L2 blocks, the outputs computed from executing transactions in those blocks will need to be written with L1 for trustless execution of L2-to-L1 messaging, such as [withdrawals](#withdrawals).
In Bedrock, outputs are hashed in a tree-structured form which minimizes the cost of proving any piece of data captured by the outputs. Proposers periodically submit **output roots** that are Merkle roots of the entire [canonical L2 chain](#protocol) to the L1.
Future upgrades of the OP Stack codebase should include a specification for a variation of a fault proof with bonding included to create incentives for proposers to propose correct output roots.
For full details, read the [L2 Output Root Proposals section](https://github.com/ethereum-optimism/optimism/blob/develop/specs/proposals.md#l2-output-root-proposals-specification) of the protocol specifications.
## Implementation
With Bedrock, the OP Stack codebase leans heavily into the technical separation of concerns specified by Ethereum by mirroring the separation between the Ethereum execution layer and consensus layer. Bedrock introduces separation of execution client and rollup node in this same way.
### Execution Client
An **execution client** is the system that sequencers and other kinds of node operators run to determine the state of the [canonical L2 chain](#protocol). It also performs other functions such as processing inbound transactions and communicating them peer-to-peer, and handling the state of the system to process queries against it.
With Bedrock, the OP Stack is designed to reuse [Ethereum’s own execution client specifications](https://github.com/ethereum/execution-specs) and its many implementations. In this release, Bedrock has demonstrated an extremely limited modification of go-ethereum, the most popular Ethereum client written in Go, to a [diff of less than 2000 lines of code](https://op-geth.optimism.io/).
There are two fundamental reasons for having any diff at all: handling deposited transactions, and charging transaction fees.
#### Handling deposited transactions
To represent [deposited transactions](#arbitrary-message-passing-from-l1) in the rollup, there is an additional transaction type introduced. The [execution client](#execution-client) implements this [new transaction type](https://github.com/ethereum-optimism/optimism/blob/develop/specs/%5Bdeposits%5D(#deposits).md%23the-deposited-transaction-type) according to the [EIP-2718 typed transactions](https://eips.ethereum.org/EIPS/eip-2718) standard.
#### Charging transaction fees
Rollups also fundamentally have two kinds of fees associated with transactions:
**Sequencer fees**
The cost of operating a sequencer is computed using the same gas table as Ethereum and with the same [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) algorithm. These fees go to the protocol for operating sequencers and fluctuate based on the congestion of the network.
**Data availability fees**
Data availability costs are associated with writing [batcher transactions](#minimized-usage-of-ethereum-gas) to the L1. These fees are intended to cover the cost that sequencers need to pay to submit [batcher transactions](#minimized-usage-of-ethereum-gas) to the L1.
In Bedrock, the data availability portion of the fee is determined based on information in a system contract on the rollup called a [GasPriceOracle](https://github.com/ethereum-optimism/optimism/blob/develop/specs/predeploys.md#gaspriceoracle). This contract is updated during [block derivation](#block-derivation) from the gas pricing information retrieved from the L1 block attributes that get inserted at the beginning of every [epoch](#guaranteed-inclusion-of-deposits).
Bedrock specifies that both of these fees are added up into a single `gasPrice` field when using the JSON-RPC.
### Rollup Node
Unlike Ethereum, Bedrock does not have proof-of-stake consensus. Instead, the consensus of the [canonical L2 chain](#protocol) is defined by [block derivation](#block-derivation). An [execution client](#execution-client) of the OP Stack communicates to a new component that implements [block derivation](#block-derivation) called a **rollup node**. This node communicates to the [execution client](#execution-client) using the exact same [Engine API](https://github.com/ethereum/execution-apis/tree/main/src/engine) that Ethereum uses.
The [rollup node](#rollup-node) is a stateless component responsible for deriving the state of the system by reading data and [deposits](#deposits) on the L1. In Bedrock, a [rollup node](#rollup-node) can either be used to sequence incoming transactions from users or other [rollup nodes](#rollup-node) or to verify confirmed transactions posted on the L1 by singularly relying on the L1.
The multiple uses of a rollup node are outlined below.
#### Verifying the canonical L2 chain
The simplest mode of running a [rollup node](#rollup-node) is to only follow the [canonical L2 chain](#protocol). In this mode, the [rollup node](#rollup-node) has no peers and is strictly used to read data from the L1 and to interpret it according to [block derivation](#block-derivation) protocol rules.
One purpose of this kind of node is to verify that any output roots shared by other nodes or posted on the L1 are correct according to protocol definition. Additionally, proposers intending to submit output roots to the L1 themselves can generate the output roots they need using the [optimism_outputAtBlock](https://github.com/ethereum-optimism/optimism/blob/develop/specs/rollup-node.md#l2-output-rpc-method) of the node which returns a 32-byte hash corresponding to the L2 output root.
For this purpose, nodes should only need to follow the finalized head. The term ["finalized"](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/#finality) refers to the Ethereum proof-of-stake consensus (i.e. canonical and practically irreversible) — the finalized L2 head is the head of the [canonical L2 chain](#protocol) that is derived only from finalized L1 blocks.
#### Participating in the L2 network
The most common way to use a [rollup node](#rollup-node) is to participate in a network of other [rollup nodes](#rollup-node) tracking the progression and state of an L2. In this mode, a [rollup node](#rollup-node) is both reading the data and [deposits](#deposits) it observes from the L1 and interpreting it as blocks and accepting inbound transactions from users and peers in a network of other [rollup nodes](#rollup-node).
Nodes participating in the network may make use of the safe and unsafe heads of the L2 they're syncing.
- The **safe L2 head** represents the rollup that can be constructed where every block up to and including the head can be fully derived from the reference L1 chain, before L1 has necessarily finalized (i.e., a re-org may occur on L1 still).
- The **unsafe L2 head** includes [unsafe blocks](https://github.com/ethereum-optimism/optimism/blob/develop/specs/glossary.md#unsafe-l2-block) that have not yet been derived from L1. These blocks either come from operating the [rollup node](#rollup-node) as a sequencer or from [unsafe sync](https://github.com/ethereum-optimism/optimism/blob/develop/specs/glossary.md#unsafe-sync) with the sequencer. This is also known as the "latest" head. The safe L2 head is always chosen over the unsafe L2 head in cases of disagreements. When disagreements occur, the unsafe portion of the chain will reorg.
For most purposes, nodes in the L2 network will refer to the unsafe L2 head for end-user applications.
#### Sequencing transactions
The third way to use a [rollup node](#rollup-node) is to sequence transactions. In this mode, a [rollup node](#rollup-node) will _create_ new blocks on top of the unsafe L2 head. Currently, there is only one sequencer per OP Stack network.
The sequencer is also responsible for posting batches to L1 for other nodes in the network to sync from.
### Batcher
The role of a sequencer is to produce [batches](#batches). To do this, a sequencer can run [rollup nodes](#rollup-node) and have separate processes which perform [batching](#batches) by reading from a trusted [rollup node](#rollup-node) they run. This warrants an additional component of the OP Stack called a [batcher](https://github.com/ethereum-optimism/optimism/blob/develop/specs/glossary.md#batcher) that reads transaction data from a [rollup node](#rollup-node) and interprets it into [batcher transactions](#minimized-usage-of-ethereum-gas) to be written to the L1. The batcher component is responsible for reading the unsafe L2 head of a [rollup node](#rollup-node) run by a sequencer, creating batcher transactions, and writing them to the L1.
### Standard Bridge Contracts
Bedrock also includes a pair of bridge contracts used for the most common kinds of [deposits](#deposits) called the [standard bridges](https://github.com/ethereum-optimism/optimism/blob/develop/specs/bridges.md#standard-bridges). These contracts wrap the [deposit](#deposits) and [withdrawal](#withdrawals) contracts to provide simple interfaces for [depositing](#deposits) and [withdrawing](#withdrawals) ETH and ERC-20 tokens.
These bridges are designed to involve a native token on one side of the bridge, and a wrapped token on the other side that can manage minting and burning. Bridging a native token involves locking the native token in a contract and then minting an equivalent amount of mintable token on the other side of the bridge.
For full details, see the [standard bridge](https://github.com/ethereum-optimism/optimism/blob/develop/specs/bridges.md#standard-bridges) section of the protocol specifications.
### Cannon
Although fault proof construction and verification is implemented in the [Cannon](https://github.com/ethereum-optimism/cannon) project, the fault proof game specification and integration of an output root challenger into the rollup node are part of later specification milestones.
## Further Reading
### Protocol Specification
The protocol specification defines the technical details of the OP Stack codebase. It is the most up-to-date source of truth for the inner workings of the protocol. The protocol specification is located in the ethereum-optimism [monorepo](https://github.com/ethereum-optimism/optimism/blob/develop/specs/README.md).
### Bedrock Differences
For a deep dive into the differences between Bedrock and previous versions of the protocol, see the [How is Bedrock Different?](https://community.optimism.io/docs/developers/bedrock/differences/) page.
---
title: Security FAQs
lang: en-US
---
::: warning 🚧 Work in Progress
The OP Stack is a work in progress. Constantly pushing to improve the overall security and decentralization of the OP Stack is a top priority.
:::
## Security in the decentralized context
The OP Stack is a decentralized development stack that powers Optimism. Components of the OP Stack may be maintained by various different teams within the Optimism Collective. It is generally easier to talk about the security model of specific chains built on the OP Stack rather than the security model of the stack itself. **The OP Stack security baseline is to create safe defaults while still giving developers the flexibility to make modifications and extend the stack.**
## FAQ
### Is every OP Stack chain safe?
The security model of an OP Stack based blockchain depends on the modules used for its components. Because of the flexibility provided by OP Stack, it is always possible to set up an insecure blockchain using OP Stack components. **The goal of the OP Stack is to provide safe defaults.**
Please also keep in mind that just like any other system, **the OP Stack may contain unknown bugs** that could lead to the loss of some or all of the assets held within an OP Stack based system. [Many components of the OP Stack codebase have been audited](https://github.com/ethereum-optimism/optimism/tree/develop/technical-documents/security-reviews) but **audits are not a stamp of approval** and **a completed audit does not mean that the audited codebase is free of bugs.** It’s important to understand that using the OP Stack inherently exposes you to the risk of bugs within the OP Stack codebase.
### Is the OP Stack safe to modify?
As with anything, modify the OP Stack at your own risk. There is no guarantee that modifications to the stack will be safe. If you aren’t entirely sure about what you’re doing, stick with the safer defaults that the OP Stack provides. At the moment, the OP Stack is not particularly amenable to modifications and **you should not expect any technical support for modifications that fall outside of the standard Rollup configuration of the stack**.
### Can I use fault proofs?
**Not yet.** The OP Stack does not currently have a fault proof system. **Note that fault proofs do not meaningfully improve the security of a system if that system can be upgraded within the 7 day challenge window (”fast upgrade keys”)**. A system with fast upgrade keys is fully dependent on the upgrade keys for security.
Fault proofs are a key milestone and top priority for the OP Stack. In the meantime, the OP Stack can be shipped with several other excellent security options for systems that want to improve security before fault proofs are available in production.
### How can I help make the OP Stack more secure?
One of the easiest ways to help secure the OP Stack is to look for bugs and vulnerabilities. [Optimism Mainnet, a user of the OP Stack, has one of the biggest bug bounties (ever)](https://immunefi.com/bounty/optimism/). You can earn up to $2,000,042 by finding critical bugs in the Optimism Mainnet codebase (and by extension the OP Stack).
Don’t forget that the OP Stack is a decentralized development stack. Anyone can start to contribute to the OP Stack by building software that follows [the stack’s design principles](../understand/design-principles.md). You can always help make the OP Stack more secure by building components, like alternative client or proof implementations, that users of the OP Stack can take advantage of.
### Where do I report bugs?
[View the Security Policy for details about reporting vulnerabilities and available bug bounty programs](./policy.md)
\ No newline at end of file
README.md
\ No newline at end of file
---
title: Security Policy, Vulnerability Reporting, and Bug Bounties
lang: en-US
---
## Reporting in the decentralized context
It's important to remember that the OP Stack is a decentralized software development stack built by the Optimism Collective. Different components of the OP Stack may be maintained by different teams that have different reporting processes. **This page describes general best practices for reporting bugs and provides specific reporting guidelines for the OP Stack code contained within the [ethereum-optimism](https://github.com/ethereum-optimism) GitHub organization**.
## Reporting bugs and vulnerabilities
::: danger 🚫 How NOT to disclose a vulnerability
Do *not* disclose vulnerabilities publicly or by executing them against a production network. If you do, will you not only be putting users at risk, but you will forfeit your right to a reward. Always follow the appropriate reporting pathways as described below.
- Do *not* disclose the vulnerability publicly, for example by filing a public ticket.
- Do *not* test the vulnerability on a publicly available network, either the testnet or the mainnet.
:::
### OP Stack bounty programs
The security of OP Stack smart contracts and blockchain infrastructure is paramount. Below are the various OP Stack-related bug bounty programs, as well as how to reach out if your bug is not covered by an existing bounty.
#### Optimism Mainnet bounty program
Optimism Mainnet is covered by a comprehensive [bug bounty program on Immunefi](https://immunefi.com/bounty/optimism/), which has already resulted in one of the [largest bounty payouts ever](https://medium.com/ethereum-optimism/disclosure-fixing-a-critical-bug-in-optimisms-geth-fork-a836ebdf7c94). In the listing you can find all the information relating to assets in scope, reporting, and the payout process. Because Optimism Mainnet is currently the primary user of the OP Stack, bugs in OP Stack software can generally be reported via the Optimism Mainnet bounty program.
#### Unscoped bugs
If you think you have found a significant bug or vulnerabilities in OP Stack smart contracts, infrastructure, etc., even if that component is not covered by an existing bug bounty, please report it to via the [Optimism Mainnet Immunefi program](https://immunefi.com/bounty/optimism/). The impact of any and all reported issues will be considered and the program has previously rewarded security researchers for bugs not within its stated scope.
### Other vulnerabilities
For vulnerabilities in any websites, email servers, or other non-critical infrastructure within the OP Stack, please email [OP Labs](https://www.oplabs.co/) at [security@oplabs.co](mailto:security@oplabs.co) and include detailed instructions for confirming and reproducing the vulnerability.
## Vulnerability disclosure
Each OP Stack component maintainer may determine its own process for vulnerability disclosure. However, the following describes a recommended process for disclosure that is currently in use by [OP Labs](https://www.oplabs.co/).
In the event that an OP Stack component maintainer learns of a critical security vulnerability, the maintainer reserves the right to silently fix it without immediately publicly disclosing the existence of nature of the vulnerability.
In such a scenario, the disclosure process used by [OP Labs](https://www.oplabs.co/) is as follows:
1. Silently fix the vulnerability and include the fix in release X.
1. After 4-8 weeks, disclose that release X contained a security fix.
1. After an additional 4-8 weeks, publish details of the vulnerability, along with credit to the reporter (with express permission from the reporter).
Alongside this policy, maintainers also reserve the right to:
- Bypass this policy and publish details on a shorter timeline.
- Directly notify a subset of downstream users prior to making a public announcement.
This policy is based the [Geth](https://geth.ethereum.org/) team’s [silent patch policy](https://geth.ethereum.org/docs/vulnerabilities/vulnerabilities#why-silent-patches).
\ No newline at end of file
---
title: Design Principles for USEful Software
lang: en-US
---
::: tip The OP Stack is USEful software
The OP Stack is a set of software components for building L2 blockchain ecosystems, built by the Optimism Collective to power Optimism.
Components to be added to the OP Stack should be built according to three key design principles:
- **U**tility
- **S**implicity
- **E**xtensibility.
Software that follows these principles is **USE**ful software for the Optimism Collective!
:::
## Utility
For something to be part of the OP Stack, it should help power the Optimism Collective.
This condition helps guide the type of software that can be included in the stack.
For instance, a powerful open-source block explorer that makes it easier for users to inspect [the Superchain](https://app.optimism.io/superchain/) would be a great addition to the OP Stack.
Although utility is important for inclusion in the OP Stack, you shouldn’t be afraid to experiment.
Do something crazy.
Build something that’s never been built before, even if it doesn’t have any clear utility. Make a blockchain for Emojis, or whatever. Have fun!
## Simplicity
Complex code does not scale.
Code that makes it into the OP Stack should be simple.
Simplicity reduces engineering overhead, which in turn means the Collective can spend its time working on new features instead of re-creating existing ones.
The OP Stack prefers to use existing battle-tested code and infrastructure where possible.
The most visible example of this philosophy in practice is the choice to use Geth as the OP Stack’s default execution engine.
When dealing with critical infrastructure, simplicity is also security and maintainability.
Every line of code written is an opportunity to introduce bugs and vulnerabilities.
A simple protocol means there's less code to write and, as a result, less surface area for potential mistakes.
A clean and minimal codebase is also more accessible to external contributors and auditors.
All of this serves to maximize the security and correctness of the OP Stack.
## Extensibility
Good OP Stack code is inherently open, collaborative, and extensible.
Collaboration allows us to break out of siloed development.
Collaboration allows us spend more time building on top of one another's work and less time rebuilding the same components over and over again.
Collaboration is how we win, *together*.
Extensible code should be designed with the mindset that others will want to build with and on top of that code.
In practice, this means that the code should be open source (under a permissive license), expose clean APIs, and generally be modular such that another developer can relatively easily extend the functionality of the code.
Extensibility is a key design principle that unlocks the superpower of collaboration within the Optimism Collective ecosystem.
## Contributing to the OP Stack
The OP Stack is a decentralized software stack that anyone can contribute to.
If you're interested in contributing to the OP Stack, check out [the Contributing section of these docs](../contribute.md).
Of course, software that has impact for the Optimism Collective can receive [Retroactive Public Goods Funding](https://app.optimism.io/retropgf).
Build for the OP Stack — get rewarded for writing great open source software. What's not to love?
---
title: Superchain Explainer
lang: en-US
image: /assets/logos/twitter-superchain.png
meta:
- name: twitter:image
content: https://stack.optimism.io/assets/logos/twitter-superchain.png
- property: og:image
content: https://stack.optimism.io/assets/logos/twitter-superchain.png
- name: twitter:title
content: Superchain Explainer
- property: og:title
content: Superchain Explainer
- name: twitter:card
content: summary_large_image
---
::: tip Staying up to date
[Stay up to date on the Superchain and the OP Stack by subscribing to the Optimism newsletter](https://optimism.us6.list-manage.com/subscribe/post?u=9727fa8bec4011400e57cafcb&id=ca91042234&f_id=002a19e3f0).
:::
The next major scalability improvement to the OP Stack after [Bedrock](../releases/bedrock/) is to introduce the concept of *a Superchain*: a network of chains that share bridging, decentralized governance, upgrades, a communication layer and more—all built on the OP Stack.
The launch of the Superchain would merge Optimism Mainnet and other chains into a single unified network of OP Chains (i.e., chains within the Superchain), and mark a major step towards bringing scalable and decentralized compute to the world. The goal of this document is to describe the scalability vision, the Superchain concept, and some changes to the OP Stack required to make this vision a reality.
This is the detailed explanation. [Click here for a less technical introduction](https://app.optimism.io/superchain/).
::: tip Note
Today, the Superchain is a concept and in-flight project, not a concrete reality. This documentation represents our best current guess as to what the Superchain’s components, features, and roadmap will be. Ultimately, its actualization will depend on (and change alongside) contributions from across the entire Optimism Collective. We cannot wait to see where it goes.
:::
## The Scalability Vision
### Blockchain tech today is insufficient for the decentralized web
The unfortunate truth is that the blockchain ecosystem has not realized the potential of creating the decentralized web, a re-architected internet where trusted entities are replaced by permissionless protocols. This is largely due to the fact that a majority of web applications are unable to be run onchain due to scalability constraints inherent to the current state of blockchain technology—a problem which has plagued the industry since its inception.
In fact, in a display of remarkable foresight, the very first response to the Bitcoin whitepaper was:
> **We very, very much need such a system, but the way I understand your proposal, it does not seem to scale to the required size.**
More than a decade later this has not changed.
### The value of scalable decentralized compute is immense…
Imagine a world where we solved the blockchain scalability problem. Imagine if transacting onchain would be as cheap as interacting with centralized backends. In this world, what would be possible?
- Developers wouldn’t need to worry about the backend infrastructure their app exists on because the chain guarantees correct execution, uptime, and [horizontal scalability](https://en.wikipedia.org/wiki/Scalability#Horizontal_(scale_out)_and_vertical_scaling_(scale_up)) of their app.
- Due to the shared smart contract execution environment, composability would be supercharged far beyond the capabilities of traditional REST APIs.
- With standardized gas markets, developers are not required to front all infrastructure costs for their users. Paying for a viral application would no longer be a barrier to entry for app devs, and more monetization strategies would be unlocked.
The combination of these features would make it possible to program highly scalable web applications without having to touch the traditional backend software stack. Removing the need to worry about backends is a value proposition which extends beyond decentralization enthusiasts into regular application developers who just want to ship a product. With scalability, blockchains can go from a niche interest to becoming a core component of every developer’s toolkit.
Additionally, in this world where most applications go onchain more data becomes cryptographically verifiable. This cryptographic verifiability enables users to build reputations which transfer across all of their applications. The reputation can then be used for voting, loans, and collateral—facilitating trust on the internet. Plus there is no risk of losing access because users retain ownership of their data, applications, and reputation.
There is no doubt that the promise of blockchains could change the internet as we know it.
### …and the decentralized web can still be realized
This hypothetical isn’t a dream, it’s a tangible vision for the future which has motivated many—including Optimism—to dedicate their lives to its pursuit. Due to these collective contributions, every year we learn more about the blockchain technology stack and get closer to realizing the vision.
With the support of the industry, we think a clear picture for how to architect a truly scalable blockchain is beginning to come into view. We call it the “Superchain”. This document lays out the core technical principles underlying the Superchain architecture, as well as a set of tangible projects which, when complete, we believe will finally realize the blockchain scalability vision. It will be a multi-year (if not decade) journey. However, if we know roughly where we’re going we’ll get there a little faster.
## Foundational Superchain Concepts
### Horizontal scalability requires multiple chains…
Horizontal blockchain scalability fundamentally requires multiple chains. This is because the hardware requirements to sync a chain increase linearly with the amount of compute the chain performs. Therefore, to achieve horizontal scalability we must run chains in parallel.
::: details Chain
A state [transition system](https://en.wikipedia.org/wiki/Transition_system)—consisting of an initial state, a state transition function, and a list of inputs (transactions)—which is cryptographically committed to and can be independently replicated with commodity computer hardware and internet connection.
:::
### …but traditional multi-chain architectures are insufficient
Traditional approaches to ‘multi-chain’ architectures suffer from two fundamental problems:
1. Each chain introduces a new security model, resulting in compounding systemic risk as new chains are introduced into the ecosystem. (related [link](https://twitter.com/VitalikButerin/status/1479501366192132099?s=20))
2. New chains are costly to spin up because they require new validator sets & block producers.
These issues come from a lack of a single shared blockchain (an “L1” chain) which serves as a shared source of truth for all of the chains (”L2” chains) within the multi-chain system. By using the shared source of truth it is possible to: a) enforce standard security models across all chains; and b) remove the requirement that chain deployments require a new set of validators because each L2 chain uses L1 consensus.
### Not multi-chain, not mono-chain… Superchain
By using L2 chains to comprise the multi-chain ecosystem, it becomes possible to begin to treat chains as commodities—interchangeable compute resources. This commodification of chains enables developers to build cross-chain applications without introducing systemic risk and without incurring large overhead as new chains are deployed for their application. The concept of a chain itself can become abstracted, and at this point it will become possible to treat this network of interoperable chains as a single unit: the Superchain.
::: details Superchain
A decentralized blockchain platform which consists of many chains that share security and a technology stack (OP Stack). The interoperability and standardization enables individual chains to be treated identically by tools and wallets.
:::
## Superchain Overview
### The Superchain at a glance
The Superchain is a network of L2 chains, known as OP Chains, which share security, a communication layer, and an open source technology stack. However, unlike multi-chain designs, these chains are standardized and intended to be used as interchangeable resources. This enables developers to build applications which target the Superchain as a whole, and abstract away the underlying chains the apps are running on.
::: details OP Chain
An individual chain within the Optimism Superchain. All chains, regardless of their specific properties are considered OP Chains if they are officially governed by the Optimism Collective, and therefore part of the Superchain.
:::
![Superchain Explainer Diagram.png](../../assets/docs/understand/superchain-diag.png)
### Properties of the Superchain
In order for Optimism to upgraded to a Superchain, it must have the following properties:
| Property | Purpose |
| - | - |
| Shared L1 blockchain | Provides a total ordering of transactions across all OP Chains.
| Shared bridge for all OP Chains | Enables OP Chains to have standardized security properties.
| Cheap OP Chain deployment | Enables deploying and transacting on OP Chains without the high fees of transacting on L1.
| Configuration options for OP Chains | Enables OP Chains to configure their data availability provider, sequencer address, etc.
| Secure transactions and cross-chain messages | Enables users to safely migrate assets between OP Chains.
Once Optimism has satisfied these properties it may be considered a Superchain.
## Upgrading Optimism to Become a Superchain
We believe the following changes (after the Bedrock release) are required to create a initial Superchain that makes it possible to deploy and upgrade many chains with the same bridge:
### Upgrade the Bedrock bridge to be a chain factory
Bedrock introduced the [SystemConfig contract](https://github.com/ethereum-optimism/optimism/blob/74a63c94d881442b4edd4df6492513e0113eb064/packages/contracts-bedrock/contracts/L1/SystemConfig.sol) which began to define the L2 chain directly with L1 smart contracts. This can be extended to put *all information* defining the L2 chain, onchain. Including generating a unique chain ID, key configuration values such as block gas limit, etc.
Once the chain data is entirely onchain, we can create a factory which deploys the configuration and all other required contracts for each chain. This can be extended further by making the contract addresses deterministic with CREATE2, meaning that given a chain config it is possible to determine all bridge addresses associated with that chain. This also enables chains to be interacted with without having to deploy their bridge contracts, making (counterfactual) chain deployment virtually free, and allowing chains to inherit standard security properties.
### Derive OP Chain data using the chain factory
[Bedrock introduced L2 chain derivation from an L1 chain](../releases/bedrock/explainer/#block-derivation), where all chain data can be synced based on L1 blocks. With the L1 chain factory extending this to put all configuration onchain, it should become possible for Optimism nodes to sync *any* OP Chain deterministically given a single L1 address plus a connection to L1.
::: tip 📌
When the OP Chain is synced, the chain state is locally computed. This means determining the state of the OP Chain is fully permissionless & secure. No proof system is required for chain derivation because all invalid transactions are simply ignored by the local computation process performed by the node. A proof system is, however, still required to enable Superchain withdrawals.
:::
### Permissionless proof system to enable withdrawals
In Bedrock, there is a permissioned entity (the proposer) who is required for users to submit withdrawals. Additionally, proposers must submit proposals to L1 at a set interval. This introduces linear overhead as the number of chains in the Superchain increases, and even introduces an upper bound on the number of chains due to the limited L1 resources.
In order to address these issues, we can introduce two features:
1. Withdrawal claims (a.k.a. Permissionless proposals) — allow anyone to submit a withdrawal (aka a proposal), not just a designated proposer. This removes the permissioned entity from the system, enabling users to submit their own withdrawal messages.
2. Remove proposal submission interval — enable withdrawal claims to be made *only* when a user needs to withdraw. This removes the overhead incurred when deploying a new OP Chain.
::: details Withdrawal claims
A claim about the state of one chain made on another chain. For instance, I can claim that in OP Mainnet I have burned my tokens with the intent to withdraw those tokens back to L1.
:::
We can enable these two features first by introducing a permissionless proof system to the Optimism bridge contracts. With the modular proof design introduced in Bedrock, proofs may come in the form of fault proofs or validity proofs (e.g. zero knowledge proofs). However, until validity proofs are productionized, we assume withdrawals will use a fault proof system.
In the envisioned fault proof system, anyone can submit a withdrawal claim, and these withdrawal claims can be submitted at any time. Submitting withdrawal claims can be permissionless when claims come with bonds attached to them, as these bonds act as collateral if the claim is proven to be invalid. If a challenger successfully challenges the claim, the bond is paid out to the challenger for their participation in securing the system, thereby preventing spam even within this permissionless system. Additionally, there is no need to submit them at a regular interval because the fault proof game can efficiently prove the entire history of the chain since genesis.
The fault proof implementation may initially rely on a trusted set of chain attestors to be the final arbiter of disputes. Challengers must request attestations from a large number of chain attestors and combine these attestations into a single transaction called an attestation proof. The attestation proof is then used to challenge invalid claims.
The attestation-based fault proof should be designed to prefer safety over liveness. That means that if these chain attestors are malicious they cannot alone break the safety of withdrawals. The worst failure they can cause is preventing withdrawals from being processed until the next upgrade—a liveness failure.
In the future, the attestation proof will be incrementally phased out and replaced with trust-minimized proofs such as the [Cannon proof system](https://github.com/ethereum-optimism/cannon).
### Configurable sequencer per OP Chain
Bedrock introduced the ability to set the sequencer address in the SystemConfig contract. As we introduce multiple chains with their own SystemConfig contracts, we can enable the sequencer address to be configured by the OP Chain deployer. We call this configurable sequencer design modular sequencing. This enables OP Chains to be sequenced by different entities while retaining the standard [Superchain bridge] security model—a critical step towards sequencer decentralization.
::: details Modular sequencing
The ability to configure the sequencer address during OP Chain deployment. This value can be configured by the OP Chain deployer.
:::
::: details Superchain bridge
The L1 bridge contracts which govern all OP Chains in the Superchain. This bridge can be upgraded by the Optimism Collective.
:::
Within the Superchain bridge security model, chain safety (i.e. validity) as well as chain liveness (i.e. censorship resistance) is guaranteed. Safety is guaranteed by the proof system, and liveness is guaranteed by the ability to submit [transactions directly to L1](../releases/bedrock/explainer/#deposits). The combination of safety and liveness means that if an OP Chain sequencer were to misbehave, users can always submit transactions to L1 that migrates their usage to a new OP Chain with a correctly functioning sequencer.
Modular sequencing also enables permissionless experimentation with different sequencing models. Developers can envision implementing sequencing protocols such as: round robin sequencing, sequencer consensus protocols, PGA ordering, or FIFO ordering. We can expect that over time user friendly sequencing standards will emerge from the competition between competing sequencing protocols.
### One shared upgrade path for all OP Chains
To ship the initial Superchain with high confidence in security and decentralization, a decentralized security council should be introduced to govern upgrades. The security council should be able to update the set of chain attestors, initiate contract upgrades with a delay, and hit an emergency bridge pause button which also cancels pending upgrades.
The ability to pause the bridge in case of emergency means that in the worst case, where the requisite threshold of the security council participants had their private keys leaked, the result would be that withdrawals are indefinitely paused and bridge upgrades would be perpetually canceled. In other words, the L1 funds would be frozen. This follows the design principle of safety over liveness—the principle that one should always prevent the loss of funds (i.e. enforce safety) even if it means the funds get locked (i.e. sacrifice liveness).
#### Unfreezing the bridge via L1 soft fork
In order to address the frozen funds, there is a potential final recovery mechanism we call the “L1 Soft Fork Upgrade Recovery” mechanism. This mechanism enables L1 to initiate a bridge upgrade with a soft fork, bypassing all other permissions within the Superchain bridge contracts. The mechanism is as follows:
*Anyone* may propose an upgrade by submitting a transaction to a special bridge contract, along with a very large bond. This begins a two week challenge period. During this challenge period, *anyone* may submit a challenge which immediately *cancels* the upgrade and claims the bond. Under normal circumstances, it is impossible that an upgrade would go uncancelled for the required two weeks due to the large incentive provided for anyone to cancel the upgrade. However, if the upgrade is accompanied by a modification to Ethereum L1 validator software (the L1 soft fork), which ignores blocks that contain the cancellation transaction then it may succeed.
While a successful upgrade of this type would represent a soft fork of Ethereum L1, it would not incur long term technical debt to the Ethereum codebase because the soft fork logic can be removed once the upgrade has completed.
We expect this escape hatch will never be used, but its very existence should deter malicious behavior.
### The combination of these features results in a system satisfying the core Superchain properties
We believe these upgrades can provide a shared bridge for all OP Chains, cheap OP Chain deployment, important configuration options for the OP Chains, as well as secure transactions and cross-chain messages. Because the Bedrock release already provides the property of a shared L1 blockchain, after these changes we will have achieved all of the core properties required for the Superchain.
## Extending the Superchain—enhancements to realize the vision
We expect that, if successful, the post-Bedrock Superchain release will mark a major milestone in the scalability and decentralization of Optimism. However, there will still be significant pain points which must be addressed before the full scalable blockchain vision has been realized. Anticipated pain points include:
1. Withdrawal claims rely on a trusted set of chain attestors.
2. Cross-Chain transactions are slow because they require waiting a challenge period.
3. Cross-Chain transactions are asynchronous, breaking the ability to perform atomic cross-chain transactions (like flash loans).
4. Posting transactions to the Superchain is not-scalable because the transaction data must be submitted to L1 which has limited capacity.
5. There are no easy frameworks for building scalable dApps which utilize many OP Chains.
6. There is no easy wallet for managing assets and dApps across many OP Chains.
If each one of these pain points were addressed, it could be possible to build decentralized alternatives to even the most complex web2 applications.
The following is an overview of potential future enhancements, which when combined, addresses each one of these pain points.
### Multi-Proof Security
#### Pain Point:
1) Withdrawal claims rely on a trusted set of chain attestors.
#### Proposed Solution:
It is possible to replace the trusted set of chain attestors by introducing permissionless proofs—such as Cannon—where dispute resolution is entirely onchain. However, the challenge with entirely onchain proofs is there is no fallback mechanism if they were to break. To ensure that they will never fail, it is possible to introduce a multi-proof system which provides safety through redundancy. For more information on the multi-proof design click [here](https://medium.com/ethereum-optimism/our-pragmatic-path-to-decentralization-cb5805ca43c1).
### Low Latency L2 to L2 Message Passing
#### Pain Point:
2) Cross-Chain transactions are slow because they require waiting a challenge period.
#### Proposed Solution:
Fault proofs introduce a UX burden because they require waiting a challenge period in order to safely finalize. This means that, depending on your challenge period length, users need to wait a long time before their assets are migrated from one OP Chain to the next.
On the other hand, validity proofs do not have this problem. Validity proofs don’t have a challenge period and therefore provide instant withdrawals from one OP Chain to the next. This is extremely important if users are expected to migrate between chains frequently, even during normal dApp execution. However, validity proofs are commonly implemented using zero-knowledge proofs (ZKPs), which are expensive and bug-prone. It will likely take years to truly productionize ZKPs enough such that they can be the primary cross-chain communication protocol.
However, while ZKPs are being productionized, it is possible to achieve low latency L2 to L2 message passing using the OP Stack’s modular proof system. With modular proofs it is possible to use two proof systems for the same chain. This opens up the possibility to provide low latency bridging which trades off security while *also* providing high security high latency bridging.
This heterogeneous bridging system means that developers can build their applications using one of many bridge types, such as:
1. High security, high latency fault proof (standard high security bridge)
2. Low security, low latency fault proof (a short challenge period to achieve low latency)
3. Low security, low latency validity proof (using trusted chain attestors in place of a ZKP)
4. High security, low latency validity proof (once ZKPs are ready)
Mixing multiple proof systems enables developers to provide low latency bridging for low value assets and high latency for high value assets. It is even possible to turn a low security asset which was instantly bridged into a high security asset by proving the asset’s validity using a high security high latency bridge. This building block enables developers to make interesting security tradeoffs such as using a high threshold attestation proof with a high security high latency fault proof fallback.
### Synchronous Cross-Chain Transactions
#### Pain Point:
3) Cross-Chain transactions are asynchronous, breaking the ability to perform atomic cross-chain transactions (like flash loans).
#### Proposed Solution:
Traditional cross-chain messaging is done asynchronously, which means that cross-chain transactions are *not* atomic. For example, if a user would would like to execute a cross-chain arbitrage transaction—buying token A on chain A, and selling token B on chain B—there is no guarantee that their transaction executes in its entirety. The user might end up buying token A without having sold token B.
It is possible to introduce synchronous cross-chain messaging and enable atomic cross-chain interactions by using a shared sequencing protocol on both OP Chains. In our example, the sequencers on chain A and chain B would each receive the arbitrage transaction, come to consensus on when they will include it, and then atomically include each transaction in the linked block. Fees would only be paid if the transaction was indeed included on each chain, meaning the sequencers take the synchronization risk as opposed to the user in our initial example. These shared sequencing protocols can be implemented permissionlessly on top of the modular sequencing layer of the post-Bedrock Superchain.
With the combination of low latency L2 to L2 message passing as well as shared sequencing, it is possible to perform complex transactions such as cross-chain flash loans. It is even possible to go further and create an EVM abstraction where individual smart contracts (or even individual storage slots) exist on different chains.
### Alt-Data Availability Layer — Plasma Protocol
#### Pain Point:
4) Posting transactions to the Superchain is not-scalable because the transaction data must be submitted to L1 which has limited capacity.
#### Proposed Solution:
Today L1 data availability (DA) does not scale nearly enough to be able to support internet-level scale. However, it is possible to extend the amount of data availability accessible to OP Chains by using a Plasma protocol which enables alternative DA providers to supplement the more limited L1 DA.
A generic Plasma protocol is able to scale beyond what is possible on L1 because only the users who are interested in the transaction data will download the Plasma data, whereas on L1 every Ethereum node downloads all of the transaction data on L1. This means that Plasma data is extremely cheap. However, where Plasma has a worse security model than L1—it is possible for Plasma chain data to temporarily become unavailable, meaning users must withdraw from the chain. Note, this security model still guarantees safety of the Plasma chains, just not liveness.
::: details Plasma chain
A chain where transaction data is committed to on L1 but not supplied to L1 directly, with a data availability challenge fallback.
:::
**Plasma protocol overview:**
- Data Availability (DA) Providers receive transaction data from users.
- DA Providers then hash the transaction data and submit the hash to the Plasma Contract.
- Once the hash has been submitted, the DA Provider sends a proof to the user which proves inclusion of their transaction data in the hash. If the DA Provider is misbehaving, they will withhold the proof, not sending it to the user.
- If the DA Provider does not send the proof to the user, the user may submit a DA challenge. This forces the DA Provider to post the transaction data onchain. If the DA Provider does not submit the proof onchain, the hash is deleted. This ensures the user can always (after the challenge period) sync the Plasma chain.
- DA challenge periods may be extended in case of heavy L1 congestion.
- The user may also submit an L1 transaction to withdraw from the Plasma chain in order to switch their DA Provider.
- Settlement of Plasma chains use a near identical fault proof system to Rollup chains with the only difference being that additional data is derived from the chain using the hashes that are finalized in the Plasma Contract.
Because of the ability for hashes to reduce arbitrary size data into a constant size commitment, and the ability to parallelize transaction data hashing, it is possible to achieve near-perfect horizontal scalability of data commitments using Plasma DA. This means that it is possible to put massively scalable applications such as games or social media on Plasma chains.
### Multi-Chain dApp Frameworks
#### Pain Points:
5) There are no easy frameworks for building scalable dApps which utilize many OP Chains.
6) There is no easy wallet for managing assets and dApps across many OP Chains.
#### Proposed Solution (Sketch):
This is not a core protocol change, but instead tooling which can be built on top of the core Superchain protocols. The suggestions here are intended to give rough intuitions for how to build tools which improve the experience of deploying to the Superchain.
These are some tools which could make developing on the Superchain a better experience:
1. Content-addressable smart contracts — this enables contracts to have the same address on all chains. This way developers can write smart contracts which are counterfactually deployed to all OP Chains at the same address. If a user on an OP Chain would like to use the smart contract that is not yet available on their chain, they can independently deploy the code.
2. Cross-chain contract state management standards — creating standards for how smart contract state can migrate from one chain to the next enables developers to shard their applications on many chains. Additionally, this logic can be used in wallets to display user state as if it is all on the same chain. For instance, if a user has tokens split across many chains, the wallet can use the cross-chain state management logic to know that it should display the user balance as a sum of all of their token balances across all chains.
::: tip 📌
For the Ethereum scalability nerds: the state growth problem can be addressed in these frameworks by making it easy to migrate user state from bloated chains into fresh chains. Old bloated chains can be maintained with a low gas limit or deprecated entirely.
:::
3. Superchain RPC endpoint — creating a single RPC endpoint where users can send their Superchain transactions regardless of which OP Chain they are intended to enables users to avoid constantly switching their network.
With robust multi-chain dApp frameworks, it may become as easy to deploy cross-chain dApps as it is to deploy dApps which target a single chain.
## Get Involved
We believe scaling blockchains will radically decentralize the internet and make it easy to create horizontally scalable, secure, and decentralized web applications. We think the Superchain release of the OP Stack could mark a major step towards realizing this vision. However, after the release it will still take an enormous amount of work to realize the scalability vision.
However, with great challenge comes great opportunity! The work needed to arrive at the initial Superchain release of the OP stack, as well as the resulting ecosystem should be exciting greenfields of opportunities for developers who want to contribute. There will be an enormous amount of low hanging fruit contributions unlocked. We can’t pick it alone! The only way we can hope to achieve it is through open source contributions from folks like you! And with [retroactive public goods funding](https://medium.com/ethereum-optimism/retroactive-public-goods-funding-33c9b7d00f0c) your open source contributions may be rewarded too!
Exciting times ahead.
Stay Optimistic 🔴✨
## Glossary
- **Attestation-Based Fault Proof**: A fault proof where challenges can be successfully made by supplying an attestation proof which disagrees with the original withdrawal claim.
- **Attestation-Based Validity Proof**: A validity proof which can be verified by supplying an attestation proof which agrees with the withdrawal claim.
- **Attestation Proof**: A proof which consists of some number of signatures from a pre-agreed upon set of chain attestors.
- **Cannon Fault Proof**: A fault proof where challenges are evaluated using an onchain game which is guaranteed to result in a truthful outcome, given economic rationality assumptions.
- **Chain**: A state [transition system](https://en.wikipedia.org/wiki/Transition_system)—consisting of an initial state, a state transition function, and a list of inputs (transactions)—which is cryptographically committed to and can be independently replicated with commodity computer hardware and internet connection.
- **Chain Proof**: Difficult to forge evidence of the validity of a particular withdrawal claim. Proofs are commonly used to enable chains to communicate with each other.
- **Challenge Period**: The window of time in which a challenge can be made to disprove a fault proof.
- **Fault Proof**: A proof which relies on the absence of counter-evidence to prove correctness.
- **Modular Proof**: The ability to use multiple proof systems for the same OP Chain. For instance, it should be possible to prove an OP Chain using a fault proof or a validity proof.
- **Modular Sequencing**: The ability to configure the sequencer address during OP Chain deployment. This value can be configured by the OP Chain deployer.
- **OP Chain**: An individual chain within the Optimism Superchain. All chains, regardless of their specific properties are considered OP Chains if they are officially governed by the Optimism Collective, and therefore part of the Superchain.
- **Plasma Chain**: A chain where transaction data is committed to on L1 but not supplied to L1 directly, with a data availability challenge fallback.
- **Rollup Chain**: A chain where all transaction data is submitted to L1.
- **Sequencer**: The specific entity or smart contract which has priority when submitting transactions to an OP Chain.
- **Superchain**: A decentralized blockchain platform which consists of many chains that share security and a technology stack (OP Stack). The interoperability and standardization enables individual chains to be treated identically by tools and wallets.
- **Superchain Bridge**: The L1 bridge contracts which govern all OP Chains in the Superchain. This bridge can be upgraded by the Optimism Collective.
- **Validity Proof**: A proof of a withdrawal claim which can be immediately validated, without a challenge period.
- **Withdrawal Claim**: A claim about the state of one chain made on another chain. For instance, I can claim that in OP Mainnet I have burned my tokens with the intent to withdraw those tokens back to L1.
- **Zero Knowledge Proof**: A validity proof which relies on cryptographic properties and low error margins.
---
title: The OP Stack Landscape
lang: en-US
---
**The OP Stack is a common development stack for building L2 blockchain ecosystems, built by the Optimism Collective to power Optimism.**
The OP Stack is best thought of as a collection of software components maintained by the Optimism Collective that either help to define new layers of the stack or fit in as modules within the stack.
Because the OP Stack is a work in progress, the landscape of the different layers and modules is still evolving.
This page sketches out the different conceptual layers of the stack as they exist today and introduces some of the modules that fit into those layers.
This doesn't include all of the modules or layers that may exist in the future, but gives a good overview of the landscape of the OP Stack today.
If you’re interested in learning more about the latest *production* release of the OP Stack, the components of the stack that are highly tested and ready for real-world action, check out the page about the [Bedrock Release](../releases/bedrock.md).
::: warning
Please note that not all of the modules described on this page already exist in a production state — these are explicitly marked as either “**in development**” or “**proposed**
:::
## Existing Landscape
![The OP Stack layers](../../assets/docs/understand/landscape.png)
## Layers
### Data Availability
The Data Availability Layer defines where the raw inputs to an OP Stack based chain are published. An OP Stack chain can use one or more Data Availability module to source its input data. Because an OP Stack chain is derived from the Data Availability Layer, the Data Availability module(s) used have a significant impact on the security model of a system. For example, if a certain piece of data can no longer be retrieved from the Data Availability Layer, it may not be possible to sync the chain.
#### Ethereum DA
Ethereum DA is currently the most widely used Data Availability module for the OP Stack. When using the Ethereum DA module, source data can be derived from any piece of information accessible on the Ethereum blockchain. This includes Ethereum calldata, events, and 4844 data blobs.
- [Specifications](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#batch-submission-wire-format)
- [Source code](https://github.com/ethereum-optimism/optimism/tree/develop/op-batcher)
### Sequencing
The Sequencing Layer determines how user transactions on an OP Stack chain are collected and published to the Data Availability Layer module(s) in use. In the default Rollup configuration of the OP Stack, Sequencing is typically handled by a single dedicated Sequencer. Rules defined in the Derivation Layer generally restrict the Sequencer’s ability to withhold transactions for more than a specific period of time. In the proposed future, Sequencing will be modular such that chains can easily select and change the mechanism that controls their current Sequencer.
#### Single Sequencer
The default Sequencer module for the OP Stack is the Single Sequencer module in which a dedicated actor is given the ability to act as the Sequencer. The Single Sequencer module allows a governance mechanism to determine who may act as the Sequencer at any given time.
#### Multiple Sequencer (proposed)
A simple modification to the Single Sequencer module is the Multiple Sequencer module in which the Sequencer at any given time is selected from a pre-defined set of possible actors. Individual OP Stack based chains would be able to determine the exact mechanism that defines the set of possible Sequencers and the mechanism that selects a Sequencer from the set.
### Derivation
The Derivation Layer defines how the raw data in the Data Availability Layer is processed to form the processed inputs that are sent to the Execution Layer via the standard [Ethereum Engine API](https://github.com/ethereum/execution-apis/blob/94164851c1630ff0a9c31d8d7d3d4fb886e196c0/src/engine/README.md). The Derivation Layer may also use the current system state, as defined by the Execution Layer, to inform the parsing of raw input data. The Derivation Layer can be modified to derive Engine API inputs from many different data sources. The Derivation Layer is typically tied closely to the Data Availability Layer because it must understand how to parse any raw input data.
#### Rollup
The Rollup module derives Engine API inputs from Ethereum block data, Sequencer transaction batches, Deposited transaction events, and more.
- [Specifications](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#l2-chain-derivation-pipeline)
- [Source code](https://github.com/ethereum-optimism/optimism/tree/develop/op-node)
#### Indexer (proposed)
The Indexer module is a proposed Derivation Layer module that would derive Engine API inputs when transactions are sent to, events are emitted by, or storage is modified in specific smart contracts on a Data Availability Layer module like Ethereum DA.
### Execution
The Execution Layer defines the structure of state within an OP Stack system and defines the state transition function that mutates this state. State transitions are triggered when inputs are received from the Derivation Layer via the Engine API. The Execution Layer abstraction opens up the door to EVM modifications or different underlying VMs entirely.
#### EVM
The EVM is an Execution Layer module that uses the same state representation and state transition function as the Ethereum Virtual Machine. The EVM module in the Ethereum Rollup configuration of the OP Stack is a [lightly modified](https://op-geth.optimism.io/) version of the EVM that adds support for L2 transactions initiated on Ethereum and adds an extra L1 Data Fee to each transaction to account for the cost of publishing transactions to Ethereum.
- [Specifications](https://github.com/ethereum-optimism/optimism/blob/develop/specs/exec-engine.md) (where it differs from [geth](https://geth.ethereum.org/))
- [Source code](https://github.com/ethereum-optimism/op-geth)
### Settlement Layer
The Settlement Layer is a mechanism on external blockchains that establish a **view** of the state of an OP Stack chain on those external chains (including other OP Stack chains). For each OP Stack chain, there may be one or more Settlement mechanisms on one or more external chains. Settlement Layer mechanisms are **read-only** and allow parties external to the blockchain to make decisions based on the state of an OP Stack chain.
The term “Settlement Layer” has its origins in the fact that Settlement Layer mechanisms are often used to handle withdrawals of assets out of a blockchain. This sort of withdrawal system first involves proving the state of the target blockchain to some third-party chain and then processing a withdrawal based on that state. However, the Settlement Layer is not strictly (or even predominantly) financial and, at its core, simply allows a third-party chain to become aware of the state of the target chain.
Once a transaction is published and finalized on the corresponding Data Availability layer, the transaction is also finalized on the OP Stack chain. Short of breaking the underlying Data Availability layer, it can no longer be modified or removed. It may not be accepted by the Settlement Layer yet because the Settlement Layer needs to be able to verify transaction *results*, but the transaction itself is already immutable.
#### Attestation-based Fault Proof
An Attestation-based Fault Proof mechanism uses an optimistic protocol to establish a view of an OP Stack chain. In optimistic settlement mechanisms generally, **Proposer** entities can propose what they believe to be the current valid state of the OP Stack chain. If these proposals are not invalidated within a certain period of time (the “challenge period”), then the proposals are assumed by the mechanism to be correct. In the Attestation Proof mechanism in particular, a proposal can be invalidated if some threshold of pre-defined parties provide attestations to a valid state that is different than the state in the proposal. This places a trust assumption on the honesty of at least a threshold number of the pre-defined participants.
- [Specifications](https://github.com/ethereum-optimism/optimism/blob/develop/specs/withdrawals.md) (called [withdrawal transactions](https://community.optimism.io/docs/developers/bridge/messaging/#))
- [Source code](https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock/contracts)
#### Fault Proof Optimistic Settlement (proposed)
A Fault Proof Optimistic Settlement mechanism is mostly identical to the Attestation-based Fault Proof mechanism used today but it replaces the MultiSig challenger with a permissionless fault proving process. A correctly constructed fault proof should be able to invalidate any incorrect proposals during the allocated challenge period. This places a trust assumption on the correctness of the fault proof construction. At this time, work on the development of a Fault Proof mechanism is well underway.
#### Validity Proof Settlement (proposed)
A Validity Proof Settlement mechanism uses a mathematical proof to attest to the correctness of a proposed view. A proposed state will not be accepted without a valid proof. This places a trust assumption on the correctness of the validity proof construction.
### Governance
The Governance Layer refers to the general set of tools and processes used to manage system configuration, upgrades, and design decisions. This is a relatively abstract layer that can contain a wide range of mechanisms on a target OP Stack chain and on third-party chains that impact many of the other layers of the OP Stack.
#### MultiSig Contracts
MultiSig Contracts are smart contracts that carry out actions when they receive a threshold of signatures from some pre-defined set of participants. These are often used to manage upgrades of components of an OP Stack based system. Currently, this is the mechanism used to manage upgrades of the bridge contracts on Optimism Mainnet. The security of a MultiSig Contract system depends on many different factors, including the number of participants, the threshold, and the safety procedures of each individual participant.
#### Governance Tokens
Governance Tokens are widely used to decentralize decision making. Although the exact functionality of a Governance Token varies on a case-by-case basis, the most common mechanisms allow token holders to vote on some subset of decisions that a project must make. Voting can either be carried out directly or via delegation.
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -11,6 +11,28 @@ import ( ...@@ -11,6 +11,28 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
) )
var (
ErrInvalidMaxFrameSize = errors.New("max frame size cannot be zero")
ErrInvalidChannelTimeout = errors.New("channel timeout is less than the safety margin")
ErrInputTargetReached = errors.New("target amount of input data reached")
ErrMaxFrameIndex = errors.New("max frame index reached (uint16)")
ErrMaxDurationReached = errors.New("max channel duration reached")
ErrChannelTimeoutClose = errors.New("close to channel timeout")
ErrSeqWindowClose = errors.New("close to sequencer window timeout")
)
type ChannelFullError struct {
Err error
}
func (e *ChannelFullError) Error() string {
return "channel full: " + e.Err.Error()
}
func (e *ChannelFullError) Unwrap() error {
return e.Err
}
type ChannelConfig struct { type ChannelConfig struct {
// Number of epochs (L1 blocks) per sequencing window, including the epoch // Number of epochs (L1 blocks) per sequencing window, including the epoch
// L1 origin block itself // L1 origin block itself
...@@ -48,12 +70,40 @@ type ChannelConfig struct { ...@@ -48,12 +70,40 @@ type ChannelConfig struct {
ApproxComprRatio float64 ApproxComprRatio float64
} }
// Check validates the [ChannelConfig] parameters.
func (cc *ChannelConfig) Check() error {
// The [ChannelTimeout] must be larger than the [SubSafetyMargin].
// Otherwise, new blocks would always be considered timed out.
if cc.ChannelTimeout < cc.SubSafetyMargin {
return ErrInvalidChannelTimeout
}
// If the [MaxFrameSize] is set to 0, the channel builder
// will infinitely loop when trying to create frames in the
// [channelBuilder.OutputFrames] function.
if cc.MaxFrameSize == 0 {
return ErrInvalidMaxFrameSize
}
return nil
}
// InputThreshold calculates the input data threshold in bytes from the given // InputThreshold calculates the input data threshold in bytes from the given
// parameters. // parameters.
func (c ChannelConfig) InputThreshold() uint64 { func (c ChannelConfig) InputThreshold() uint64 {
return uint64(float64(c.TargetNumFrames) * float64(c.TargetFrameSize) / c.ApproxComprRatio) return uint64(float64(c.TargetNumFrames) * float64(c.TargetFrameSize) / c.ApproxComprRatio)
} }
type frameID struct {
chID derive.ChannelID
frameNumber uint16
}
type frameData struct {
data []byte
id frameID
}
// channelBuilder uses a ChannelOut to create a channel with output frame // channelBuilder uses a ChannelOut to create a channel with output frame
// size approximation. // size approximation.
type channelBuilder struct { type channelBuilder struct {
...@@ -76,7 +126,7 @@ type channelBuilder struct { ...@@ -76,7 +126,7 @@ type channelBuilder struct {
// list of blocks in the channel. Saved in case the channel must be rebuilt // list of blocks in the channel. Saved in case the channel must be rebuilt
blocks []*types.Block blocks []*types.Block
// frames data queue, to be send as txs // frames data queue, to be send as txs
frames []taggedData frames []frameData
} }
func newChannelBuilder(cfg ChannelConfig) (*channelBuilder, error) { func newChannelBuilder(cfg ChannelConfig) (*channelBuilder, error) {
...@@ -319,7 +369,7 @@ func (c *channelBuilder) outputFrame() error { ...@@ -319,7 +369,7 @@ func (c *channelBuilder) outputFrame() error {
c.setFullErr(ErrMaxFrameIndex) c.setFullErr(ErrMaxFrameIndex)
} }
frame := taggedData{ frame := frameData{
id: txID{chID: c.co.ID(), frameNumber: fn}, id: txID{chID: c.co.ID(), frameNumber: fn},
data: buf.Bytes(), data: buf.Bytes(),
} }
...@@ -343,41 +393,21 @@ func (c *channelBuilder) NumFrames() int { ...@@ -343,41 +393,21 @@ func (c *channelBuilder) NumFrames() int {
// NextFrame returns the next available frame. // NextFrame returns the next available frame.
// HasFrame must be called prior to check if there's a next frame available. // HasFrame must be called prior to check if there's a next frame available.
// Panics if called when there's no next frame. // Panics if called when there's no next frame.
func (c *channelBuilder) NextFrame() (txID, []byte) { func (c *channelBuilder) NextFrame() frameData {
if len(c.frames) == 0 { if len(c.frames) == 0 {
panic("no next frame") panic("no next frame")
} }
f := c.frames[0] f := c.frames[0]
c.frames = c.frames[1:] c.frames = c.frames[1:]
return f.id, f.data return f
} }
// PushFrame adds the frame back to the internal frames queue. Panics if not of // PushFrame adds the frame back to the internal frames queue. Panics if not of
// the same channel. // the same channel.
func (c *channelBuilder) PushFrame(id txID, frame []byte) { func (c *channelBuilder) PushFrame(frame frameData) {
if id.chID != c.ID() { if frame.id.chID != c.ID() {
panic("wrong channel") panic("wrong channel")
} }
c.frames = append(c.frames, taggedData{id: id, data: frame}) c.frames = append(c.frames, frame)
}
var (
ErrInputTargetReached = errors.New("target amount of input data reached")
ErrMaxFrameIndex = errors.New("max frame index reached (uint16)")
ErrMaxDurationReached = errors.New("max channel duration reached")
ErrChannelTimeoutClose = errors.New("close to channel timeout")
ErrSeqWindowClose = errors.New("close to sequencer window timeout")
)
type ChannelFullError struct {
Err error
}
func (e *ChannelFullError) Error() string {
return "channel full: " + e.Err.Error()
}
func (e *ChannelFullError) Unwrap() error {
return e.Err
} }
package batcher
import (
"testing"
"github.com/stretchr/testify/require"
)
// defaultChannelConfig returns a valid, default [ChannelConfig] struct.
func defaultChannelConfig() ChannelConfig {
return ChannelConfig{
SeqWindowSize: 15,
ChannelTimeout: 40,
MaxChannelDuration: 1,
SubSafetyMargin: 4,
MaxFrameSize: 120000,
TargetFrameSize: 100000,
TargetNumFrames: 1,
ApproxComprRatio: 0.4,
}
}
// TestConfigValidation tests the validation of the [ChannelConfig] struct.
func TestConfigValidation(t *testing.T) {
// Construct a valid config.
validChannelConfig := defaultChannelConfig()
require.NoError(t, validChannelConfig.Check())
// Set the config to have a zero max frame size.
validChannelConfig.MaxFrameSize = 0
require.ErrorIs(t, validChannelConfig.Check(), ErrInvalidMaxFrameSize)
// Reset the config and test the Timeout error.
// NOTE: We should be fuzzing these values with the constraint that
// SubSafetyMargin > ChannelTimeout to ensure validation.
validChannelConfig = defaultChannelConfig()
validChannelConfig.ChannelTimeout = 0
validChannelConfig.SubSafetyMargin = 1
require.ErrorIs(t, validChannelConfig.Check(), ErrInvalidChannelTimeout)
}
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ import (
"math" "math"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -15,29 +14,6 @@ import ( ...@@ -15,29 +14,6 @@ import (
var ErrReorg = errors.New("block does not extend existing chain") var ErrReorg = errors.New("block does not extend existing chain")
// txID is an opaque identifier for a transaction.
// It's internal fields should not be inspected after creation & are subject to change.
// This ID must be trivially comparable & work as a map key.
type txID struct {
chID derive.ChannelID
frameNumber uint16
}
func (id txID) String() string {
return fmt.Sprintf("%s:%d", id.chID.String(), id.frameNumber)
}
// TerminalString implements log.TerminalStringer, formatting a string for console
// output during logging.
func (id txID) TerminalString() string {
return fmt.Sprintf("%s:%d", id.chID.TerminalString(), id.frameNumber)
}
type taggedData struct {
data []byte
id txID
}
// channelManager stores a contiguous set of blocks & turns them into channels. // channelManager stores a contiguous set of blocks & turns them into channels.
// Upon receiving tx confirmation (or a tx failure), it does channel error handling. // Upon receiving tx confirmation (or a tx failure), it does channel error handling.
// //
...@@ -59,7 +35,7 @@ type channelManager struct { ...@@ -59,7 +35,7 @@ type channelManager struct {
// pending channel builder // pending channel builder
pendingChannel *channelBuilder pendingChannel *channelBuilder
// Set of unconfirmed txID -> frame data. For tx resubmission // Set of unconfirmed txID -> frame data. For tx resubmission
pendingTransactions map[txID][]byte pendingTransactions map[txID]txData
// Set of confirmed txID -> inclusion block. For determining if the channel is timed out // Set of confirmed txID -> inclusion block. For determining if the channel is timed out
confirmedTransactions map[txID]eth.BlockID confirmedTransactions map[txID]eth.BlockID
} }
...@@ -68,7 +44,7 @@ func NewChannelManager(log log.Logger, cfg ChannelConfig) *channelManager { ...@@ -68,7 +44,7 @@ func NewChannelManager(log log.Logger, cfg ChannelConfig) *channelManager {
return &channelManager{ return &channelManager{
log: log, log: log,
cfg: cfg, cfg: cfg,
pendingTransactions: make(map[txID][]byte), pendingTransactions: make(map[txID]txData),
confirmedTransactions: make(map[txID]eth.BlockID), confirmedTransactions: make(map[txID]eth.BlockID),
} }
} }
...@@ -87,7 +63,10 @@ func (s *channelManager) Clear() { ...@@ -87,7 +63,10 @@ func (s *channelManager) Clear() {
func (s *channelManager) TxFailed(id txID) { func (s *channelManager) TxFailed(id txID) {
if data, ok := s.pendingTransactions[id]; ok { if data, ok := s.pendingTransactions[id]; ok {
s.log.Trace("marked transaction as failed", "id", id) s.log.Trace("marked transaction as failed", "id", id)
s.pendingChannel.PushFrame(id, data[1:]) // strip the version byte // Note: when the batcher is changed to send multiple frames per tx,
// this needs to be changed to iterate over all frames of the tx data
// and re-queue them.
s.pendingChannel.PushFrame(data.Frame())
delete(s.pendingTransactions, id) delete(s.pendingTransactions, id)
} else { } else {
s.log.Warn("unknown transaction marked as failed", "id", id) s.log.Warn("unknown transaction marked as failed", "id", id)
...@@ -128,7 +107,7 @@ func (s *channelManager) TxConfirmed(id txID, inclusionBlock eth.BlockID) { ...@@ -128,7 +107,7 @@ func (s *channelManager) TxConfirmed(id txID, inclusionBlock eth.BlockID) {
// TODO: Create separate "pending" state // TODO: Create separate "pending" state
func (s *channelManager) clearPendingChannel() { func (s *channelManager) clearPendingChannel() {
s.pendingChannel = nil s.pendingChannel = nil
s.pendingTransactions = make(map[txID][]byte) s.pendingTransactions = make(map[txID]txData)
s.confirmedTransactions = make(map[txID]eth.BlockID) s.confirmedTransactions = make(map[txID]eth.BlockID)
} }
...@@ -166,21 +145,19 @@ func (s *channelManager) pendingChannelIsFullySubmitted() bool { ...@@ -166,21 +145,19 @@ func (s *channelManager) pendingChannelIsFullySubmitted() bool {
} }
// nextTxData pops off s.datas & handles updating the internal state // nextTxData pops off s.datas & handles updating the internal state
func (s *channelManager) nextTxData() ([]byte, txID, error) { func (s *channelManager) nextTxData() (txData, error) {
if s.pendingChannel == nil || !s.pendingChannel.HasFrame() { if s.pendingChannel == nil || !s.pendingChannel.HasFrame() {
s.log.Trace("no next tx data") s.log.Trace("no next tx data")
return nil, txID{}, io.EOF // TODO: not enough data error instead return txData{}, io.EOF // TODO: not enough data error instead
} }
id, data := s.pendingChannel.NextFrame() frame := s.pendingChannel.NextFrame()
// prepend version byte for first frame of transaction txdata := txData{frame}
// TODO: more memory efficient solution; shouldn't be responsibility of id := txdata.ID()
// channelBuilder though.
data = append([]byte{0}, data...)
s.log.Trace("returning next tx data", "id", id) s.log.Trace("returning next tx data", "id", id)
s.pendingTransactions[id] = data s.pendingTransactions[id] = txdata
return data, id, nil return txdata, nil
} }
// TxData returns the next tx data that should be submitted to L1. // TxData returns the next tx data that should be submitted to L1.
...@@ -188,7 +165,7 @@ func (s *channelManager) nextTxData() ([]byte, txID, error) { ...@@ -188,7 +165,7 @@ func (s *channelManager) nextTxData() ([]byte, txID, error) {
// It currently only uses one frame per transaction. If the pending channel is // It currently only uses one frame per transaction. If the pending channel is
// full, it only returns the remaining frames of this channel until it got // full, it only returns the remaining frames of this channel until it got
// successfully fully sent to L1. It returns io.EOF if there's no pending frame. // successfully fully sent to L1. It returns io.EOF if there's no pending frame.
func (s *channelManager) TxData(l1Head eth.BlockID) ([]byte, txID, error) { func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) {
dataPending := s.pendingChannel != nil && s.pendingChannel.HasFrame() dataPending := s.pendingChannel != nil && s.pendingChannel.HasFrame()
s.log.Debug("Requested tx data", "l1Head", l1Head, "data_pending", dataPending, "blocks_pending", len(s.blocks)) s.log.Debug("Requested tx data", "l1Head", l1Head, "data_pending", dataPending, "blocks_pending", len(s.blocks))
...@@ -201,15 +178,15 @@ func (s *channelManager) TxData(l1Head eth.BlockID) ([]byte, txID, error) { ...@@ -201,15 +178,15 @@ func (s *channelManager) TxData(l1Head eth.BlockID) ([]byte, txID, error) {
// If we have no saved blocks, we will not be able to create valid frames // If we have no saved blocks, we will not be able to create valid frames
if len(s.blocks) == 0 { if len(s.blocks) == 0 {
return nil, txID{}, io.EOF return txData{}, io.EOF
} }
if err := s.ensurePendingChannel(l1Head); err != nil { if err := s.ensurePendingChannel(l1Head); err != nil {
return nil, txID{}, err return txData{}, err
} }
if err := s.processBlocks(); err != nil { if err := s.processBlocks(); err != nil {
return nil, txID{}, err return txData{}, err
} }
// Register current L1 head only after all pending blocks have been // Register current L1 head only after all pending blocks have been
...@@ -218,7 +195,7 @@ func (s *channelManager) TxData(l1Head eth.BlockID) ([]byte, txID, error) { ...@@ -218,7 +195,7 @@ func (s *channelManager) TxData(l1Head eth.BlockID) ([]byte, txID, error) {
s.registerL1Block(l1Head) s.registerL1Block(l1Head)
if err := s.pendingChannel.OutputFrames(); err != nil { if err := s.pendingChannel.OutputFrames(); err != nil {
return nil, txID{}, fmt.Errorf("creating frames with channel builder: %w", err) return txData{}, fmt.Errorf("creating frames with channel builder: %w", err)
} }
return s.nextTxData() return s.nextTxData()
......
...@@ -3,11 +3,14 @@ package batcher_test ...@@ -3,11 +3,14 @@ package batcher_test
import ( import (
"io" "io"
"math/big" "math/big"
"math/rand"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-batcher/batcher" "github.com/ethereum-optimism/optimism/op-batcher/batcher"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
derivetest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -54,15 +57,15 @@ func TestChannelManagerReturnsErrReorgWhenDrained(t *testing.T) { ...@@ -54,15 +57,15 @@ func TestChannelManagerReturnsErrReorgWhenDrained(t *testing.T) {
log := testlog.Logger(t, log.LvlCrit) log := testlog.Logger(t, log.LvlCrit)
m := batcher.NewChannelManager(log, batcher.ChannelConfig{ m := batcher.NewChannelManager(log, batcher.ChannelConfig{
TargetFrameSize: 0, TargetFrameSize: 0,
MaxFrameSize: 100, MaxFrameSize: 120_000,
ApproxComprRatio: 1.0, ApproxComprRatio: 1.0,
}) })
lBlock := types.NewBlock(&types.Header{ l1Block := types.NewBlock(&types.Header{
BaseFee: big.NewInt(10), BaseFee: big.NewInt(10),
Difficulty: common.Big0, Difficulty: common.Big0,
Number: big.NewInt(100), Number: big.NewInt(100),
}, nil, nil, nil, trie.NewStackTrie(nil)) }, nil, nil, nil, trie.NewStackTrie(nil))
l1InfoTx, err := derive.L1InfoDeposit(0, lBlock, eth.SystemConfig{}, false) l1InfoTx, err := derive.L1InfoDeposit(0, l1Block, eth.SystemConfig{}, false)
require.NoError(t, err) require.NoError(t, err)
txs := []*types.Transaction{types.NewTx(l1InfoTx)} txs := []*types.Transaction{types.NewTx(l1InfoTx)}
...@@ -77,10 +80,50 @@ func TestChannelManagerReturnsErrReorgWhenDrained(t *testing.T) { ...@@ -77,10 +80,50 @@ func TestChannelManagerReturnsErrReorgWhenDrained(t *testing.T) {
err = m.AddL2Block(a) err = m.AddL2Block(a)
require.NoError(t, err) require.NoError(t, err)
_, _, err = m.TxData(eth.BlockID{}) _, err = m.TxData(eth.BlockID{})
require.NoError(t, err) require.NoError(t, err)
_, _, err = m.TxData(eth.BlockID{}) _, err = m.TxData(eth.BlockID{})
require.ErrorIs(t, err, io.EOF) require.ErrorIs(t, err, io.EOF)
err = m.AddL2Block(x) err = m.AddL2Block(x)
require.ErrorIs(t, err, batcher.ErrReorg) require.ErrorIs(t, err, batcher.ErrReorg)
} }
func TestChannelManager_TxResend(t *testing.T) {
require := require.New(t)
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
log := testlog.Logger(t, log.LvlError)
m := batcher.NewChannelManager(log, batcher.ChannelConfig{
TargetFrameSize: 0,
MaxFrameSize: 120_000,
ApproxComprRatio: 1.0,
})
a, _ := derivetest.RandomL2Block(rng, 4)
err := m.AddL2Block(a)
require.NoError(err)
txdata0, err := m.TxData(eth.BlockID{})
require.NoError(err)
txdata0bytes := txdata0.Bytes()
data0 := make([]byte, len(txdata0bytes))
// make sure we have a clone for later comparison
copy(data0, txdata0bytes)
// ensure channel is drained
_, err = m.TxData(eth.BlockID{})
require.ErrorIs(err, io.EOF)
// requeue frame
m.TxFailed(txdata0.ID())
txdata1, err := m.TxData(eth.BlockID{})
require.NoError(err)
data1 := txdata1.Bytes()
require.Equal(data1, data0)
fs, err := derive.ParseFrames(data1)
require.NoError(err)
require.Len(fs, 1)
}
...@@ -35,6 +35,17 @@ type Config struct { ...@@ -35,6 +35,17 @@ type Config struct {
Channel ChannelConfig Channel ChannelConfig
} }
// Check ensures that the [Config] is valid.
func (c *Config) Check() error {
if err := c.Rollup.Check(); err != nil {
return err
}
if err := c.Channel.Check(); err != nil {
return err
}
return nil
}
type CLIConfig struct { type CLIConfig struct {
/* Required Params */ /* Required Params */
......
...@@ -99,6 +99,11 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*BatchSubmitte ...@@ -99,6 +99,11 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*BatchSubmitte
}, },
} }
// Validate the batcher config
if err := batcherCfg.Check(); err != nil {
return nil, err
}
return NewBatchSubmitter(ctx, batcherCfg, l) return NewBatchSubmitter(ctx, batcherCfg, l)
} }
...@@ -280,7 +285,7 @@ func (l *BatchSubmitter) loop() { ...@@ -280,7 +285,7 @@ func (l *BatchSubmitter) loop() {
} }
// Collect next transaction data // Collect next transaction data
data, id, err := l.state.TxData(l1tip.ID()) txdata, err := l.state.TxData(l1tip.ID())
if err == io.EOF { if err == io.EOF {
l.log.Trace("no transaction data available") l.log.Trace("no transaction data available")
break // local for loop break // local for loop
...@@ -289,10 +294,10 @@ func (l *BatchSubmitter) loop() { ...@@ -289,10 +294,10 @@ func (l *BatchSubmitter) loop() {
break break
} }
// Record TX Status // Record TX Status
if receipt, err := l.txMgr.SendTransaction(l.ctx, data); err != nil { if receipt, err := l.txMgr.SendTransaction(l.ctx, txdata.Bytes()); err != nil {
l.recordFailedTx(id, err) l.recordFailedTx(txdata.ID(), err)
} else { } else {
l.recordConfirmedTx(id, receipt) l.recordConfirmedTx(txdata.ID(), receipt)
} }
// hack to exit this loop. Proper fix is to do request another send tx or parallel tx sending // hack to exit this loop. Proper fix is to do request another send tx or parallel tx sending
......
package batcher
import (
"fmt"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
)
// txData represents the data for a single transaction.
//
// Note: The batcher currently sends exactly one frame per transaction. This
// might change in the future to allow for multiple frames from possibly
// different channels.
type txData struct {
frame frameData
}
// ID returns the id for this transaction data. It can be used as a map key.
func (td *txData) ID() txID {
return td.frame.id
}
// Bytes returns the transaction data. It's a version byte (0) followed by the
// concatenated frames for this transaction.
func (td *txData) Bytes() []byte {
return append([]byte{derive.DerivationVersion0}, td.frame.data...)
}
// Frame returns the single frame of this tx data.
//
// Note: when the batcher is changed to possibly send multiple frames per tx,
// this should be changed to a func Frames() []frameData.
func (td *txData) Frame() frameData {
return td.frame
}
// txID is an opaque identifier for a transaction.
// It's internal fields should not be inspected after creation & are subject to change.
// This ID must be trivially comparable & work as a map key.
//
// Note: transactions currently only hold a single frame, so it can be
// identified by the frame. This needs to be changed once the batcher is changed
// to send multiple frames per tx.
type txID = frameID
func (id txID) String() string {
return fmt.Sprintf("%s:%d", id.chID.String(), id.frameNumber)
}
// TerminalString implements log.TerminalStringer, formatting a string for console
// output during logging.
func (id txID) TerminalString() string {
return fmt.Sprintf("%s:%d", id.chID.TerminalString(), id.frameNumber)
}
package main
import (
"context"
"fmt"
"math/big"
"os"
"strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/db"
"github.com/mattn/go-isatty"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/hardhat"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
)
func main() {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd()))))
app := &cli.App{
Name: "check-migration",
Usage: "Run sanity checks on a migrated database",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "l1-rpc-url",
Value: "http://127.0.0.1:8545",
Usage: "RPC URL for an L1 Node",
Required: true,
},
&cli.StringFlag{
Name: "ovm-addresses",
Usage: "Path to ovm-addresses.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-allowances",
Usage: "Path to ovm-allowances.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-messages",
Usage: "Path to ovm-messages.json",
Required: true,
},
&cli.StringFlag{
Name: "witness-file",
Usage: "Path to witness file",
Required: true,
},
&cli.StringFlag{
Name: "db-path",
Usage: "Path to database",
Required: true,
},
cli.StringFlag{
Name: "deploy-config",
Usage: "Path to hardhat deploy config file",
Required: true,
},
cli.StringFlag{
Name: "network",
Usage: "Name of hardhat deploy network",
Required: true,
},
cli.StringFlag{
Name: "hardhat-deployments",
Usage: "Comma separated list of hardhat deployment directories",
Required: true,
},
cli.IntFlag{
Name: "db-cache",
Usage: "LevelDB cache size in mb",
Value: 1024,
},
cli.IntFlag{
Name: "db-handles",
Usage: "LevelDB number of handles",
Value: 60,
},
},
Action: func(ctx *cli.Context) error {
deployConfig := ctx.String("deploy-config")
config, err := genesis.NewDeployConfig(deployConfig)
if err != nil {
return err
}
ovmAddresses, err := crossdomain.NewAddresses(ctx.String("ovm-addresses"))
if err != nil {
return err
}
ovmAllowances, err := crossdomain.NewAllowances(ctx.String("ovm-allowances"))
if err != nil {
return err
}
ovmMessages, err := crossdomain.NewSentMessageFromJSON(ctx.String("ovm-messages"))
if err != nil {
return err
}
evmMessages, evmAddresses, err := crossdomain.ReadWitnessData(ctx.String("witness-file"))
if err != nil {
return err
}
log.Info(
"Loaded witness data",
"ovmAddresses", len(ovmAddresses),
"evmAddresses", len(evmAddresses),
"ovmAllowances", len(ovmAllowances),
"ovmMessages", len(ovmMessages),
"evmMessages", len(evmMessages),
)
migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresses,
OvmAllowances: ovmAllowances,
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
network := ctx.String("network")
deployments := strings.Split(ctx.String("hardhat-deployments"), ",")
hh, err := hardhat.New(network, []string{}, deployments)
if err != nil {
return err
}
l1RpcURL := ctx.String("l1-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return err
}
var block *types.Block
tag := config.L1StartingBlockTag
if tag.BlockNumber != nil {
block, err = l1Client.BlockByNumber(context.Background(), big.NewInt(tag.BlockNumber.Int64()))
} else if tag.BlockHash != nil {
block, err = l1Client.BlockByHash(context.Background(), *tag.BlockHash)
} else {
return fmt.Errorf("invalid l1StartingBlockTag in deploy config: %v", tag)
}
if err != nil {
return err
}
dbCache := ctx.Int("db-cache")
dbHandles := ctx.Int("db-handles")
// Read the required deployment addresses from disk if required
if err := config.GetDeployedAddresses(hh); err != nil {
return err
}
if err := config.Check(); err != nil {
return err
}
postLDB, err := db.Open(ctx.String("db-path"), dbCache, dbHandles)
if err != nil {
return err
}
if err := genesis.PostCheckMigratedDB(
postLDB,
migrationData,
&config.L1CrossDomainMessengerProxy,
config.L1ChainID,
config.FinalSystemOwner,
config.ProxyAdminOwner,
&derive.L1BlockInfo{
Number: block.NumberU64(),
Time: block.Time(),
BaseFee: block.BaseFee(),
BlockHash: block.Hash(),
BatcherAddr: config.BatchSenderAddress,
L1FeeOverhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(config.GasPriceOracleOverhead))),
L1FeeScalar: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(config.GasPriceOracleScalar))),
},
); err != nil {
return err
}
if err := postLDB.Close(); err != nil {
return err
}
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("error in migration", "err", err)
}
}
...@@ -106,6 +106,11 @@ func main() { ...@@ -106,6 +106,11 @@ func main() {
Value: "rollup.json", Value: "rollup.json",
Required: true, Required: true,
}, },
cli.BoolFlag{
Name: "post-check-only",
Usage: "Only perform sanity checks",
Required: false,
},
}, },
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
deployConfig := ctx.String("deploy-config") deployConfig := ctx.String("deploy-config")
......
...@@ -20,3 +20,13 @@ func getOVMETHTotalSupplySlot() common.Hash { ...@@ -20,3 +20,13 @@ func getOVMETHTotalSupplySlot() common.Hash {
key := common.BytesToHash(common.LeftPadBytes(position.Bytes(), 32)) key := common.BytesToHash(common.LeftPadBytes(position.Bytes(), 32))
return key return key
} }
func GetOVMETHTotalSupplySlot() common.Hash {
return getOVMETHTotalSupplySlot()
}
// GetOVMETHBalance gets a user's OVM ETH balance from state by querying the
// appropriate storage slot directly.
func GetOVMETHBalance(db *state.StateDB, addr common.Address) *big.Int {
return db.GetState(OVMETHAddress, CalcOVMETHStorageKey(addr)).Big()
}
...@@ -17,7 +17,7 @@ var ( ...@@ -17,7 +17,7 @@ var (
// OVMETHAddress is the address of the OVM ETH predeploy. // OVMETHAddress is the address of the OVM ETH predeploy.
OVMETHAddress = common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000") OVMETHAddress = common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000")
OVMETHIgnoredSlots = map[common.Hash]bool{ ignoredSlots = map[common.Hash]bool{
// Total Supply // Total Supply
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002"): true, common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002"): true,
// Name // Name
...@@ -29,14 +29,9 @@ var ( ...@@ -29,14 +29,9 @@ var (
} }
) )
// MigrateLegacyETH checks that the given list of addresses and allowances represents all storage type FilteredOVMETHAddresses []common.Address
// slots in the LegacyERC20ETH contract. We don't have to filter out extra addresses like we do for
// withdrawals because we'll simply carry the balance of a given address to the new system, if the func MigrateLegacyETH(db *state.StateDB, addresses FilteredOVMETHAddresses, chainID int, noCheck bool) error {
// account is extra then it won't have any balance and nothing will happen. For each valid balance,
// this method will migrate into state. This method does the checking as part of the migration loop
// in order to avoid having to iterate over state twice. This saves approximately 40 minutes during
// the mainnet migration.
func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool, dryRun bool) error {
// Chain params to use for integrity checking. // Chain params to use for integrity checking.
params := crossdomain.ParamsByChainID[chainID] params := crossdomain.ParamsByChainID[chainID]
if params == nil { if params == nil {
...@@ -46,99 +41,25 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, allowances ...@@ -46,99 +41,25 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, allowances
// Log the chain params for debugging purposes. // Log the chain params for debugging purposes.
log.Info("Chain params", "chain-id", chainID, "supply-delta", params.ExpectedSupplyDelta) log.Info("Chain params", "chain-id", chainID, "supply-delta", params.ExpectedSupplyDelta)
return doMigration(db, addresses, allowances, params.ExpectedSupplyDelta, noCheck, dryRun) // Migrate the legacy ETH to ETH.
}
func doMigration(db *state.StateDB, addresses []common.Address, allowances []*crossdomain.Allowance, expSupplyDiff *big.Int, noCheck bool, dryRun bool) error {
// We'll need to maintain a list of all addresses that we've seen along with all of the storage
// slots based on the witness data.
slotsAddrs := make(map[common.Hash]common.Address)
slotTypes := make(map[common.Hash]int)
// For each known address, compute its balance key and add it to the list of addresses.
// Mint events are instrumented as regular ETH events in the witness data, so we no longer
// need to iterate over mint events during the migration.
for _, addr := range addresses {
sk := CalcOVMETHStorageKey(addr)
slotTypes[sk] = 1
slotsAddrs[sk] = addr
}
// For each known allowance, compute its storage key and add it to the list of addresses.
for _, allowance := range allowances {
slotTypes[CalcAllowanceStorageKey(allowance.From, allowance.To)] = 2
}
// Add the old SequencerEntrypoint because someone sent it ETH a long time ago and it has a
// balance but none of our instrumentation could easily find it. Special case.
sequencerEntrypointAddr := common.HexToAddress("0x4200000000000000000000000000000000000005")
slotTypes[CalcOVMETHStorageKey(sequencerEntrypointAddr)] = 1
// Migrate the OVM_ETH to ETH.
log.Info("Migrating legacy ETH to ETH", "num-accounts", len(addresses)) log.Info("Migrating legacy ETH to ETH", "num-accounts", len(addresses))
totalMigrated := new(big.Int) totalMigrated := new(big.Int)
logAccountProgress := util.ProgressLogger(1000, "imported OVM_ETH storage slot") logAccountProgress := util.ProgressLogger(1000, "imported accounts")
var innerErr error for _, addr := range addresses {
err := db.ForEachStorage(predeploys.LegacyERC20ETHAddr, func(key, value common.Hash) bool { // Balances are pre-checked not have any balances in state.
defer logAccountProgress()
// We can safely ignore specific slots (totalSupply, name, symbol).
if OVMETHIgnoredSlots[key] {
return true
}
// Look up the slot type.
slotType, ok := slotTypes[key]
if !ok {
log.Error("unknown storage slot in state", "slot", key.String())
if !noCheck {
innerErr = fmt.Errorf("unknown storage slot in state: %s", key.String())
return false
}
}
switch slotType { // Pull out the OVM ETH balance.
case 1: ovmBalance := GetOVMETHBalance(db, addr)
// Balance slot.
bal := value.Big()
totalMigrated.Add(totalMigrated, bal)
addr := slotsAddrs[key]
// There should never be any balances in state, so verify that here.
if db.GetBalance(addr).Sign() > 0 {
log.Error("account has non-zero balance in state - should never happen", "addr", addr)
if !noCheck {
innerErr = fmt.Errorf("account has non-zero balance in state - should never happen: %s", addr)
return false
}
}
if dryRun { // Actually perform the migration by setting the appropriate values in state.
return true db.SetBalance(addr, ovmBalance)
} db.SetState(predeploys.LegacyERC20ETHAddr, CalcOVMETHStorageKey(addr), common.Hash{})
// Set the balance, and delete the legacy slot. // Bump the total OVM balance.
db.SetBalance(addr, bal) totalMigrated = totalMigrated.Add(totalMigrated, ovmBalance)
db.SetState(predeploys.LegacyERC20ETHAddr, key, common.Hash{})
case 2:
// Allowance slot. Nothing to do here.
return true
default:
// Should never happen.
log.Error("unknown slot type", "slot", key.String(), "type", slotType)
if !noCheck {
innerErr = fmt.Errorf("unknown slot type: %d", slotType)
return false
}
}
return true // Log progress.
}) logAccountProgress()
if err != nil {
return fmt.Errorf("failed to iterate over OVM_ETH storage: %w", err)
}
if innerErr != nil {
return fmt.Errorf("error in migration: %w", innerErr)
} }
// Make sure that the total supply delta matches the expected delta. This is equivalent to // Make sure that the total supply delta matches the expected delta. This is equivalent to
...@@ -146,44 +67,33 @@ func doMigration(db *state.StateDB, addresses []common.Address, allowances []*cr ...@@ -146,44 +67,33 @@ func doMigration(db *state.StateDB, addresses []common.Address, allowances []*cr
// same check against the total found (a = b, b = c => a = c). // same check against the total found (a = b, b = c => a = c).
totalSupply := getOVMETHTotalSupply(db) totalSupply := getOVMETHTotalSupply(db)
delta := new(big.Int).Sub(totalSupply, totalMigrated) delta := new(big.Int).Sub(totalSupply, totalMigrated)
if delta.Cmp(expSupplyDiff) != 0 { if delta.Cmp(params.ExpectedSupplyDelta) != 0 {
if noCheck { if noCheck {
log.Error( log.Error(
"supply mismatch", "supply mismatch",
"migrated", totalMigrated.String(), "migrated", totalMigrated.String(),
"supply", totalSupply.String(), "supply", totalSupply.String(),
"delta", delta.String(), "delta", delta.String(),
"exp_delta", expSupplyDiff.String(), "exp_delta", params.ExpectedSupplyDelta.String(),
) )
} else { } else {
log.Error( log.Crit(
"supply mismatch", "supply mismatch",
"migrated", totalMigrated.String(), "migrated", totalMigrated.String(),
"supply", totalSupply.String(), "supply", totalSupply.String(),
"delta", delta.String(), "delta", delta.String(),
"exp_delta", expSupplyDiff.String(), "exp_delta", params.ExpectedSupplyDelta.String(),
) )
return fmt.Errorf("supply mismatch: exp delta %s != %s", expSupplyDiff.String(), delta.String())
} }
} }
// Supply is verified.
log.Info(
"supply verified OK",
"migrated", totalMigrated.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", expSupplyDiff.String(),
)
// Set the total supply to 0. We do this because the total supply is necessarily going to be // Set the total supply to 0. We do this because the total supply is necessarily going to be
// different than the sum of all balances since we no longer track balances inside the contract // different than the sum of all balances since we no longer track balances inside the contract
// itself. The total supply is going to be weird no matter what, might as well set it to zero // itself. The total supply is going to be weird no matter what, might as well set it to zero
// so it's explicitly weird instead of implicitly weird. // so it's explicitly weird instead of implicitly weird.
if !dryRun {
db.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{}) db.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{})
log.Info("Set the totalSupply to 0") log.Info("Set the totalSupply to 0")
}
// Fin.
return nil return nil
} }
package ether
import (
"fmt"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/log"
)
const (
// checkJobs is the number of parallel workers to spawn
// when iterating the storage trie.
checkJobs = 64
)
// maxSlot is the maximum possible storage slot.
var maxSlot = common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
// accountData is a wrapper struct that contains the balance and address of an account.
// It gets passed via channel to the collector process.
type accountData struct {
balance *big.Int
address common.Address
}
type DBFactory func() (*state.StateDB, error)
// PreCheckBalances checks that the given list of addresses and allowances represents all storage
// slots in the LegacyERC20ETH contract. We don't have to filter out extra addresses like we do for
// withdrawals because we'll simply carry the balance of a given address to the new system, if the
// account is extra then it won't have any balance and nothing will happen.
func PreCheckBalances(dbFactory DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool) (FilteredOVMETHAddresses, error) {
// Chain params to use for integrity checking.
params := crossdomain.ParamsByChainID[chainID]
if params == nil {
return nil, fmt.Errorf("no chain params for %d", chainID)
}
return doMigration(dbFactory, addresses, allowances, params.ExpectedSupplyDelta, noCheck)
}
func doMigration(dbFactory DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, expDiff *big.Int, noCheck bool) (FilteredOVMETHAddresses, error) {
// We'll need to maintain a list of all addresses that we've seen along with all of the storage
// slots based on the witness data.
addrs := make([]common.Address, 0)
slotsAddrs := make(map[common.Hash]common.Address)
slotsInp := make(map[common.Hash]int)
// For each known address, compute its balance key and add it to the list of addresses.
// Mint events are instrumented as regular ETH events in the witness data, so we no longer
// need to iterate over mint events during the migration.
for _, addr := range addresses {
sk := CalcOVMETHStorageKey(addr)
slotsAddrs[sk] = addr
slotsInp[sk] = 1
}
// For each known allowance, compute its storage key and add it to the list of addresses.
for _, allowance := range allowances {
sk := CalcAllowanceStorageKey(allowance.From, allowance.To)
slotsAddrs[sk] = allowance.From
slotsInp[sk] = 2
}
// Add the old SequencerEntrypoint because someone sent it ETH a long time ago and it has a
// balance but none of our instrumentation could easily find it. Special case.
sequencerEntrypointAddr := common.HexToAddress("0x4200000000000000000000000000000000000005")
entrySK := CalcOVMETHStorageKey(sequencerEntrypointAddr)
slotsAddrs[entrySK] = sequencerEntrypointAddr
slotsInp[entrySK] = 1
// WaitGroup to wait on each iteration job to finish.
var wg sync.WaitGroup
// Channel to receive storage slot keys and values from each iteration job.
outCh := make(chan accountData)
// Channel to receive errors from each iteration job.
errCh := make(chan error, checkJobs)
// Channel to cancel all iteration jobs as well as the collector.
cancelCh := make(chan struct{})
// Keep track of the total migrated supply.
totalFound := new(big.Int)
// Divide the key space into partitions by dividing the key space by the number
// of jobs. This will leave some slots left over, which we handle below.
partSize := new(big.Int).Div(maxSlot.Big(), big.NewInt(checkJobs))
// Define a worker function to iterate over each partition.
worker := func(start, end common.Hash) {
// Decrement the WaitGroup when the function returns.
defer wg.Done()
db, err := dbFactory()
if err != nil {
log.Crit("cannot get database", "err", err)
}
// Create a new storage trie. Each trie returned by db.StorageTrie
// is a copy, so this is safe for concurrent use.
st, err := db.StorageTrie(predeploys.LegacyERC20ETHAddr)
if err != nil {
// Should never happen, so explode if it does.
log.Crit("cannot get storage trie for LegacyERC20ETHAddr", "err", err)
}
if st == nil {
// Should never happen, so explode if it does.
log.Crit("nil storage trie for LegacyERC20ETHAddr")
}
it := trie.NewIterator(st.NodeIterator(start.Bytes()))
// Below code is largely based on db.ForEachStorage. We can't use that
// because it doesn't allow us to specify a start and end key.
for it.Next() {
select {
case <-cancelCh:
// If one of the workers encounters an error, cancel all of them.
return
default:
break
}
// Use the raw (i.e., secure hashed) key to check if we've reached
// the end of the partition.
if new(big.Int).SetBytes(it.Key).Cmp(end.Big()) >= 0 {
return
}
// Skip if the value is empty.
rawValue := it.Value
if len(rawValue) == 0 {
continue
}
// Get the preimage.
key := common.BytesToHash(st.GetKey(it.Key))
// Parse the raw value.
_, content, _, err := rlp.Split(rawValue)
if err != nil {
// Should never happen, so explode if it does.
log.Crit("mal-formed data in state: %v", err)
}
// We can safely ignore specific slots (totalSupply, name, symbol).
if ignoredSlots[key] {
continue
}
slotType, ok := slotsInp[key]
if !ok {
if noCheck {
log.Error("ignoring unknown storage slot in state", "slot", key.String())
} else {
errCh <- fmt.Errorf("unknown storage slot in state: %s", key.String())
return
}
}
// No accounts should have a balance in state. If they do, bail.
addr, ok := slotsAddrs[key]
if !ok {
log.Crit("could not find address in map - should never happen")
}
bal := db.GetBalance(addr)
if bal.Sign() != 0 {
log.Error(
"account has non-zero balance in state - should never happen",
"addr", addr,
"balance", bal.String(),
)
if !noCheck {
errCh <- fmt.Errorf("account has non-zero balance in state - should never happen: %s", addr.String())
return
}
}
// Add balances to the total found.
switch slotType {
case 1:
// Convert the value to a common.Hash, then send to the channel.
value := common.BytesToHash(content)
outCh <- accountData{
balance: value.Big(),
address: addr,
}
case 2:
// Allowance slot.
continue
default:
// Should never happen.
if noCheck {
log.Error("unknown slot type", "slot", key, "type", slotType)
} else {
log.Crit("unknown slot type %d, should never happen", slotType)
}
}
}
}
for i := 0; i < checkJobs; i++ {
wg.Add(1)
// Compute the start and end keys for this partition.
start := common.BigToHash(new(big.Int).Mul(big.NewInt(int64(i)), partSize))
var end common.Hash
if i < checkJobs-1 {
// If this is not the last partition, use the next partition's start key as the end.
end = common.BigToHash(new(big.Int).Mul(big.NewInt(int64(i+1)), partSize))
} else {
// If this is the last partition, use the max slot as the end.
end = maxSlot
}
// Kick off our worker.
go worker(start, end)
}
// Make a channel to make sure that the collector process completes.
collectorCloseCh := make(chan struct{})
// Keep track of the last error seen.
var lastErr error
// There are multiple ways that the cancel channel can be closed:
// - if we receive an error from the errCh
// - if the collector process completes
// To prevent panics, we wrap the close in a sync.Once.
var cancelOnce sync.Once
// Kick off another background process to collect
// values from the channel and add them to the map.
var count int
progress := util.ProgressLogger(1000, "Collected OVM_ETH storage slot")
go func() {
defer func() {
collectorCloseCh <- struct{}{}
}()
for {
select {
case account := <-outCh:
progress()
// Accumulate addresses and total supply.
addrs = append(addrs, account.address)
totalFound = new(big.Int).Add(totalFound, account.balance)
case err := <-errCh:
lastErr = err
cancelOnce.Do(func() {
close(cancelCh)
})
case <-cancelCh:
return
}
}
}()
// Wait for the workers to finish.
wg.Wait()
// Close the cancel channel to signal the collector process to stop.
cancelOnce.Do(func() {
close(cancelCh)
})
// Wait for the collector process to finish.
<-collectorCloseCh
// If we saw an error, return it.
if lastErr != nil {
return nil, lastErr
}
// Log how many slots were iterated over.
log.Info("Iterated legacy balances", "count", count)
// Verify the supply delta. Recorded total supply in the LegacyERC20ETH contract may be higher
// than the actual migrated amount because self-destructs will remove ETH supply in a way that
// cannot be reflected in the contract. This is fine because self-destructs just mean the L2 is
// actually *overcollateralized* by some tiny amount.
db, err := dbFactory()
if err != nil {
log.Crit("cannot get database", "err", err)
}
totalSupply := getOVMETHTotalSupply(db)
delta := new(big.Int).Sub(totalSupply, totalFound)
if delta.Cmp(expDiff) != 0 {
log.Error(
"supply mismatch",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", expDiff.String(),
)
if !noCheck {
return nil, fmt.Errorf("supply mismatch: %s", delta.String())
}
}
// Supply is verified.
log.Info(
"supply verified OK",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", expDiff.String(),
)
// We know we have at least a superset of all addresses here since we know that we have every
// storage slot. It's fine to have extras because they won't have any balance.
return addrs, nil
}
package ether package ether
import ( import (
"bytes"
"math/big" "math/big"
"math/rand"
"os"
"sort"
"testing" "testing"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
...@@ -12,7 +18,9 @@ import ( ...@@ -12,7 +18,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMigrateLegacyETH(t *testing.T) { func TestPreCheckBalances(t *testing.T) {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(true)))
tests := []struct { tests := []struct {
name string name string
totalSupply *big.Int totalSupply *big.Int
...@@ -21,7 +29,7 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -21,7 +29,7 @@ func TestMigrateLegacyETH(t *testing.T) {
stateAllowances map[common.Address]common.Address stateAllowances map[common.Address]common.Address
inputAddresses []common.Address inputAddresses []common.Address
inputAllowances []*crossdomain.Allowance inputAllowances []*crossdomain.Allowance
check func(t *testing.T, db *state.StateDB, err error) check func(t *testing.T, addrs FilteredOVMETHAddresses, err error)
}{ }{
{ {
name: "everything matches", name: "everything matches",
...@@ -44,13 +52,12 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -44,13 +52,12 @@ func TestMigrateLegacyETH(t *testing.T) {
To: common.HexToAddress("0x456"), To: common.HexToAddress("0x456"),
}, },
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1)) require.EqualValues(t, FilteredOVMETHAddresses{
require.Equal(t, db.GetBalance(common.HexToAddress("0x456")), big.NewInt(2)) common.HexToAddress("0x123"),
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{}) common.HexToAddress("0x456"),
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x456"))), common.Hash{}) }, addrs)
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{})
}, },
}, },
{ {
...@@ -64,11 +71,11 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -64,11 +71,11 @@ func TestMigrateLegacyETH(t *testing.T) {
common.HexToAddress("0x123"), common.HexToAddress("0x123"),
common.HexToAddress("0x456"), common.HexToAddress("0x456"),
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1)) require.EqualValues(t, FilteredOVMETHAddresses{
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{}) common.HexToAddress("0x123"),
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{}) }, addrs)
}, },
}, },
{ {
...@@ -95,11 +102,11 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -95,11 +102,11 @@ func TestMigrateLegacyETH(t *testing.T) {
To: common.HexToAddress("0x789"), To: common.HexToAddress("0x789"),
}, },
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1)) require.EqualValues(t, FilteredOVMETHAddresses{
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{}) common.HexToAddress("0x123"),
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{}) }, addrs)
}, },
}, },
{ {
...@@ -113,7 +120,7 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -113,7 +120,7 @@ func TestMigrateLegacyETH(t *testing.T) {
inputAddresses: []common.Address{ inputAddresses: []common.Address{
common.HexToAddress("0x123"), common.HexToAddress("0x123"),
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.Error(t, err) require.Error(t, err)
require.ErrorContains(t, err, "unknown storage slot") require.ErrorContains(t, err, "unknown storage slot")
}, },
...@@ -138,7 +145,7 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -138,7 +145,7 @@ func TestMigrateLegacyETH(t *testing.T) {
To: common.HexToAddress("0x456"), To: common.HexToAddress("0x456"),
}, },
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.Error(t, err) require.Error(t, err)
require.ErrorContains(t, err, "unknown storage slot") require.ErrorContains(t, err, "unknown storage slot")
}, },
...@@ -155,7 +162,7 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -155,7 +162,7 @@ func TestMigrateLegacyETH(t *testing.T) {
common.HexToAddress("0x123"), common.HexToAddress("0x123"),
common.HexToAddress("0x456"), common.HexToAddress("0x456"),
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.Error(t, err) require.Error(t, err)
require.ErrorContains(t, err, "supply mismatch") require.ErrorContains(t, err, "supply mismatch")
}, },
...@@ -172,21 +179,29 @@ func TestMigrateLegacyETH(t *testing.T) { ...@@ -172,21 +179,29 @@ func TestMigrateLegacyETH(t *testing.T) {
common.HexToAddress("0x123"), common.HexToAddress("0x123"),
common.HexToAddress("0x456"), common.HexToAddress("0x456"),
}, },
check: func(t *testing.T, db *state.StateDB, err error) { check: func(t *testing.T, addrs FilteredOVMETHAddresses, err error) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1)) require.EqualValues(t, FilteredOVMETHAddresses{
require.Equal(t, db.GetBalance(common.HexToAddress("0x456")), big.NewInt(2)) common.HexToAddress("0x123"),
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{}) common.HexToAddress("0x456"),
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x456"))), common.Hash{}) }, addrs)
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{})
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
db := makeLegacyETH(t, tt.totalSupply, tt.stateBalances, tt.stateAllowances) db := makeLegacyETH(t, tt.totalSupply, tt.stateBalances, tt.stateAllowances)
err := doMigration(db, tt.inputAddresses, tt.inputAllowances, tt.expDiff, false, false) factory := func() (*state.StateDB, error) {
tt.check(t, db, err) return db, nil
}
addrs, err := doMigration(factory, tt.inputAddresses, tt.inputAllowances, tt.expDiff, false)
// Sort the addresses since they come in in a random order.
sort.Slice(addrs, func(i, j int) bool {
return bytes.Compare(addrs[i][:], addrs[j][:]) < 0
})
tt.check(t, addrs, err)
}) })
} }
} }
...@@ -201,7 +216,7 @@ func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Addre ...@@ -201,7 +216,7 @@ func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Addre
db.CreateAccount(OVMETHAddress) db.CreateAccount(OVMETHAddress)
db.SetState(OVMETHAddress, getOVMETHTotalSupplySlot(), common.BigToHash(totalSupply)) db.SetState(OVMETHAddress, getOVMETHTotalSupplySlot(), common.BigToHash(totalSupply))
for slot := range OVMETHIgnoredSlots { for slot := range ignoredSlots {
if slot == getOVMETHTotalSupplySlot() { if slot == getOVMETHTotalSupplySlot() {
continue continue
} }
...@@ -222,3 +237,59 @@ func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Addre ...@@ -222,3 +237,59 @@ func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Addre
return db return db
} }
// TestPreCheckBalancesRandom tests that the pre-check balances function works
// with random addresses. This test makes sure that the partition logic doesn't
// miss anything.
func TestPreCheckBalancesRandom(t *testing.T) {
addresses := make([]common.Address, 0)
stateBalances := make(map[common.Address]*big.Int)
allowances := make([]*crossdomain.Allowance, 0)
stateAllowances := make(map[common.Address]common.Address)
totalSupply := big.NewInt(0)
for i := 0; i < 100; i++ {
for i := 0; i < rand.Intn(1000); i++ {
addr := randAddr(t)
addresses = append(addresses, addr)
stateBalances[addr] = big.NewInt(int64(rand.Intn(1_000_000)))
totalSupply = new(big.Int).Add(totalSupply, stateBalances[addr])
}
sort.Slice(addresses, func(i, j int) bool {
return bytes.Compare(addresses[i][:], addresses[j][:]) < 0
})
for i := 0; i < rand.Intn(1000); i++ {
addr := randAddr(t)
to := randAddr(t)
allowances = append(allowances, &crossdomain.Allowance{
From: addr,
To: to,
})
stateAllowances[addr] = to
}
db := makeLegacyETH(t, totalSupply, stateBalances, stateAllowances)
factory := func() (*state.StateDB, error) {
return db, nil
}
outAddrs, err := doMigration(factory, addresses, allowances, big.NewInt(0), false)
require.NoError(t, err)
sort.Slice(outAddrs, func(i, j int) bool {
return bytes.Compare(outAddrs[i][:], outAddrs[j][:]) < 0
})
require.EqualValues(t, addresses, outAddrs)
}
}
func randAddr(t *testing.T) common.Address {
var addr common.Address
_, err := rand.Read(addr[:])
require.NoError(t, err)
return addr
}
...@@ -7,6 +7,8 @@ import ( ...@@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-chain-ops/ether"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
...@@ -31,6 +33,7 @@ const MaxSlotChecks = 1000 ...@@ -31,6 +33,7 @@ const MaxSlotChecks = 1000
type StorageCheckMap = map[common.Hash]common.Hash type StorageCheckMap = map[common.Hash]common.Hash
var ( var (
L2XDMOwnerSlot = common.Hash{31: 0x33}
ProxyAdminOwnerSlot = common.Hash{} ProxyAdminOwnerSlot = common.Hash{}
LegacyETHCheckSlots = map[common.Hash]common.Hash{ LegacyETHCheckSlots = map[common.Hash]common.Hash{
...@@ -250,7 +253,7 @@ func PostCheckPredeploys(prevDB, currDB *state.StateDB) error { ...@@ -250,7 +253,7 @@ func PostCheckPredeploys(prevDB, currDB *state.StateDB) error {
// Balances and nonces should match legacy // Balances and nonces should match legacy
oldNonce := prevDB.GetNonce(addr) oldNonce := prevDB.GetNonce(addr)
oldBalance := prevDB.GetBalance(addr) oldBalance := ether.GetOVMETHBalance(prevDB, addr)
newNonce := currDB.GetNonce(addr) newNonce := currDB.GetNonce(addr)
newBalance := currDB.GetBalance(addr) newBalance := currDB.GetBalance(addr)
if oldNonce != newNonce { if oldNonce != newNonce {
...@@ -542,7 +545,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data crossdomain.MigrationData, l1Cros ...@@ -542,7 +545,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data crossdomain.MigrationData, l1Cros
// If the sender is _not_ the L2XDM, the value should not be migrated. // If the sender is _not_ the L2XDM, the value should not be migrated.
wd := wdsByOldSlot[key] wd := wdsByOldSlot[key]
if wd.XDomainSender == predeploys.L2CrossDomainMessengerAddr { if wd.MessageSender == predeploys.L2CrossDomainMessengerAddr {
// Make sure the value is abiTrue if this withdrawal should be migrated. // Make sure the value is abiTrue if this withdrawal should be migrated.
if migratedValue != abiTrue { if migratedValue != abiTrue {
innerErr = fmt.Errorf("expected migrated value to be true, but got %s", migratedValue) innerErr = fmt.Errorf("expected migrated value to be true, but got %s", migratedValue)
...@@ -551,7 +554,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data crossdomain.MigrationData, l1Cros ...@@ -551,7 +554,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data crossdomain.MigrationData, l1Cros
} else { } else {
// Otherwise, ensure that withdrawals from senders other than the L2XDM are _not_ migrated. // Otherwise, ensure that withdrawals from senders other than the L2XDM are _not_ migrated.
if migratedValue != abiFalse { if migratedValue != abiFalse {
innerErr = fmt.Errorf("a migration from a sender other than the L2XDM was migrated") innerErr = fmt.Errorf("a migration from a sender other than the L2XDM was migrated. sender: %s, migrated value: %s", wd.MessageSender, migratedValue)
return false return false
} }
} }
......
...@@ -82,6 +82,7 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -82,6 +82,7 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
) )
} }
dbFactory := func() (*state.StateDB, error) {
// Set up the backing store. // Set up the backing store.
underlyingDB := state.NewDatabaseWithConfig(ldb, &trie.Config{ underlyingDB := state.NewDatabaseWithConfig(ldb, &trie.Config{
Preimages: true, Preimages: true,
...@@ -94,6 +95,14 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -94,6 +95,14 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
return nil, fmt.Errorf("cannot open StateDB: %w", err) return nil, fmt.Errorf("cannot open StateDB: %w", err)
} }
return db, nil
}
db, err := dbFactory()
if err != nil {
return nil, fmt.Errorf("cannot create StateDB: %w", err)
}
// Before we do anything else, we need to ensure that all of the input configuration is correct // Before we do anything else, we need to ensure that all of the input configuration is correct
// and nothing is missing. We'll first verify the contract configuration, then we'll verify the // and nothing is missing. We'll first verify the contract configuration, then we'll verify the
// witness data for the migration. We operate under the assumption that the witness data is // witness data for the migration. We operate under the assumption that the witness data is
...@@ -134,6 +143,16 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -134,6 +143,16 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
filteredWithdrawals = crossdomain.SafeFilteredWithdrawals(unfilteredWithdrawals) filteredWithdrawals = crossdomain.SafeFilteredWithdrawals(unfilteredWithdrawals)
} }
// We also need to verify that we have all of the storage slots for the LegacyERC20ETH contract
// that we expect to have. An error will be thrown if there are any missing storage slots.
// Unlike with withdrawals, we do not need to filter out extra addresses because their balances
// would necessarily be zero and therefore not affect the migration.
log.Info("Checking addresses...", "no-check", noCheck)
addrs, err := ether.PreCheckBalances(dbFactory, migrationData.Addresses(), migrationData.OvmAllowances, int(config.L1ChainID), noCheck)
if err != nil {
return nil, fmt.Errorf("addresses mismatch: %w", err)
}
// At this point we've fully verified the witness data for the migration, so we can begin the // At this point we've fully verified the witness data for the migration, so we can begin the
// actual migration process. This involves modifying parts of the legacy database and inserting // actual migration process. This involves modifying parts of the legacy database and inserting
// a transition block. // a transition block.
...@@ -182,15 +201,10 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -182,15 +201,10 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
return nil, fmt.Errorf("cannot migrate withdrawals: %w", err) return nil, fmt.Errorf("cannot migrate withdrawals: %w", err)
} }
// We also need to verify that we have all of the storage slots for the LegacyERC20ETH contract // Finally we migrate the balances held inside the LegacyERC20ETH contract into the state trie.
// that we expect to have. An error will be thrown if there are any missing storage slots.
// Unlike with withdrawals, we do not need to filter out extra addresses because their balances
// would necessarily be zero and therefore not affect the migration.
//
// Once verified, we migrate the balances held inside the LegacyERC20ETH contract into the state trie.
// We also delete the balances from the LegacyERC20ETH contract. // We also delete the balances from the LegacyERC20ETH contract.
log.Info("Starting to migrate ERC20 ETH") log.Info("Starting to migrate ERC20 ETH")
err = ether.MigrateLegacyETH(db, migrationData.Addresses(), migrationData.OvmAllowances, int(config.L1ChainID), noCheck, commit) err = ether.MigrateLegacyETH(db, addrs, int(config.L1ChainID), noCheck)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot migrate legacy eth: %w", err) return nil, fmt.Errorf("cannot migrate legacy eth: %w", err)
} }
......
package actions
import (
"testing"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/testlog"
)
func TestShapellaL1Fork(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
activation := sd.L1Cfg.Timestamp + 24
sd.L1Cfg.Config.ShanghaiTime = &activation
log := testlog.Logger(t, log.LvlDebug)
_, _, miner, sequencer, _, verifier, _, batcher := setupReorgTestActors(t, dp, sd, log)
require.False(t, sd.L1Cfg.Config.IsShanghai(miner.l1Chain.CurrentBlock().Time()), "not active yet")
// start op-nodes
sequencer.ActL2PipelineFull(t)
verifier.ActL2PipelineFull(t)
// build empty L1 blocks, crossing the fork boundary
miner.ActEmptyBlock(t)
miner.ActEmptyBlock(t)
miner.ActEmptyBlock(t)
// verify Shanghai is active
l1Head := miner.l1Chain.CurrentBlock()
require.True(t, sd.L1Cfg.Config.IsShanghai(l1Head.Time()))
// build L2 chain up to and including L2 blocks referencing shanghai L1 blocks
sequencer.ActL1HeadSignal(t)
sequencer.ActBuildToL1Head(t)
miner.ActL1StartBlock(12)(t)
batcher.ActSubmitAll(t)
miner.ActL1IncludeTx(batcher.batcherAddr)(t)
miner.ActL1EndBlock(t)
// sync verifier
verifier.ActL1HeadSignal(t)
verifier.ActL2PipelineFull(t)
// verify verifier accepted shanghai L1 inputs
require.Equal(t, l1Head.Hash(), verifier.SyncStatus().SafeL2.L1Origin.Hash, "verifier synced L1 chain that includes shanghai headers")
require.Equal(t, sequencer.SyncStatus().UnsafeL2, verifier.SyncStatus().UnsafeL2, "verifier and sequencer agree")
}
...@@ -72,6 +72,9 @@ func (s *L1Miner) ActL1StartBlock(timeDelta uint64) Action { ...@@ -72,6 +72,9 @@ func (s *L1Miner) ActL1StartBlock(timeDelta uint64) Action {
header.GasLimit = parent.GasLimit * s.l1Cfg.Config.ElasticityMultiplier() header.GasLimit = parent.GasLimit * s.l1Cfg.Config.ElasticityMultiplier()
} }
} }
if s.l1Cfg.Config.IsShanghai(header.Time) {
header.WithdrawalsHash = &types.EmptyWithdrawalsHash
}
s.l1Building = true s.l1Building = true
s.l1BuildingHeader = header s.l1BuildingHeader = header
...@@ -135,6 +138,9 @@ func (s *L1Miner) ActL1EndBlock(t Testing) { ...@@ -135,6 +138,9 @@ func (s *L1Miner) ActL1EndBlock(t Testing) {
s.l1BuildingHeader.GasUsed = s.l1BuildingHeader.GasLimit - uint64(*s.l1GasPool) s.l1BuildingHeader.GasUsed = s.l1BuildingHeader.GasLimit - uint64(*s.l1GasPool)
s.l1BuildingHeader.Root = s.l1BuildingState.IntermediateRoot(s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number)) s.l1BuildingHeader.Root = s.l1BuildingState.IntermediateRoot(s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number))
block := types.NewBlock(s.l1BuildingHeader, s.l1Transactions, nil, s.l1Receipts, trie.NewStackTrie(nil)) block := types.NewBlock(s.l1BuildingHeader, s.l1Transactions, nil, s.l1Receipts, trie.NewStackTrie(nil))
if s.l1Cfg.Config.IsShanghai(s.l1BuildingHeader.Time) {
block = block.WithWithdrawals(make([]*types.Withdrawal, 0))
}
// Write state changes to db // Write state changes to db
root, err := s.l1BuildingState.Commit(s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number)) root, err := s.l1BuildingState.Commit(s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number))
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"math/big" "math/big"
"os" "os"
"path" "path"
"sort"
"strings" "strings"
"testing" "testing"
"time" "time"
...@@ -119,14 +120,6 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -119,14 +120,6 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
JWTFilePath: writeDefaultJWT(t), JWTFilePath: writeDefaultJWT(t),
JWTSecret: testingJWTSecret, JWTSecret: testingJWTSecret,
Nodes: map[string]*rollupNode.Config{ Nodes: map[string]*rollupNode.Config{
"verifier": {
Driver: driver.Config{
VerifierConfDepth: 0,
SequencerConfDepth: 0,
SequencerEnabled: false,
},
L1EpochPollInterval: time.Second * 4,
},
"sequencer": { "sequencer": {
Driver: driver.Config{ Driver: driver.Config{
VerifierConfDepth: 0, VerifierConfDepth: 0,
...@@ -141,6 +134,14 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -141,6 +134,14 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
}, },
L1EpochPollInterval: time.Second * 4, L1EpochPollInterval: time.Second * 4,
}, },
"verifier": {
Driver: driver.Config{
VerifierConfDepth: 0,
SequencerConfDepth: 0,
SequencerEnabled: false,
},
L1EpochPollInterval: time.Second * 4,
},
}, },
Loggers: map[string]log.Logger{ Loggers: map[string]log.Logger{
"verifier": testlog.Logger(t, log.LvlInfo).New("role", "verifier"), "verifier": testlog.Logger(t, log.LvlInfo).New("role", "verifier"),
...@@ -225,7 +226,43 @@ func (sys *System) Close() { ...@@ -225,7 +226,43 @@ func (sys *System) Close() {
sys.Mocknet.Close() sys.Mocknet.Close()
} }
func (cfg SystemConfig) Start() (*System, error) { type systemConfigHook func(sCfg *SystemConfig, s *System)
type SystemConfigOption struct {
key string
role string
action systemConfigHook
}
type SystemConfigOptions struct {
opts map[string]systemConfigHook
}
func NewSystemConfigOptions(_opts []SystemConfigOption) (SystemConfigOptions, error) {
opts := make(map[string]systemConfigHook)
for _, opt := range _opts {
if _, ok := opts[opt.key+":"+opt.role]; ok {
return SystemConfigOptions{}, fmt.Errorf("duplicate option for key %s and role %s", opt.key, opt.role)
}
opts[opt.key+":"+opt.role] = opt.action
}
return SystemConfigOptions{
opts: opts,
}, nil
}
func (s *SystemConfigOptions) Get(key, role string) (systemConfigHook, bool) {
v, ok := s.opts[key+":"+role]
return v, ok
}
func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
opts, err := NewSystemConfigOptions(_opts)
if err != nil {
return nil, err
}
sys := &System{ sys := &System{
cfg: cfg, cfg: cfg,
Nodes: make(map[string]*node.Node), Nodes: make(map[string]*node.Node),
...@@ -457,7 +494,17 @@ func (cfg SystemConfig) Start() (*System, error) { ...@@ -457,7 +494,17 @@ func (cfg SystemConfig) Start() (*System, error) {
snapLog.SetHandler(log.DiscardHandler()) snapLog.SetHandler(log.DiscardHandler())
// Rollup nodes // Rollup nodes
for name, nodeConfig := range cfg.Nodes {
// Ensure we are looping through the nodes in alphabetical order
ks := make([]string, 0, len(cfg.Nodes))
for k := range cfg.Nodes {
ks = append(ks, k)
}
// Sort strings in ascending alphabetical order
sort.Strings(ks)
for _, name := range ks {
nodeConfig := cfg.Nodes[name]
c := *nodeConfig // copy c := *nodeConfig // copy
c.Rollup = makeRollupConfig() c.Rollup = makeRollupConfig()
...@@ -482,6 +529,10 @@ func (cfg SystemConfig) Start() (*System, error) { ...@@ -482,6 +529,10 @@ func (cfg SystemConfig) Start() (*System, error) {
return nil, err return nil, err
} }
sys.RollupNodes[name] = node sys.RollupNodes[name] = node
if action, ok := opts.Get("afterRollupNodeStart", name); ok {
action(&cfg, sys)
}
} }
if cfg.P2PTopology != nil { if cfg.P2PTopology != nil {
......
...@@ -649,9 +649,94 @@ func TestSystemMockP2P(t *testing.T) { ...@@ -649,9 +649,94 @@ func TestSystemMockP2P(t *testing.T) {
require.Contains(t, received, receiptVerif.BlockHash) require.Contains(t, received, receiptVerif.BlockHash)
} }
// TestSystemMockPeerScoring sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that // TestSystemMockP2P sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that
// the nodes can sync L2 blocks before they are confirmed on L1. // the nodes can sync L2 blocks before they are confirmed on L1.
func TestSystemMockPeerScoring(t *testing.T) { //
// Test steps:
// 1. Spin up the nodes (P2P is disabled on the verifier)
// 2. Send a transaction to the sequencer.
// 3. Wait for the TX to be mined on the sequencer chain.
// 5. Wait for the verifier to detect a gap in the payload queue vs. the unsafe head
// 6. Wait for the RPC sync method to grab the block from the sequencer over RPC and insert it into the verifier's unsafe chain.
// 7. Wait for the verifier to sync the unsafe chain into the safe chain.
// 8. Verify that the TX is included in the verifier's safe chain.
func TestSystemMockAltSync(t *testing.T) {
parallel(t)
if !verboseGethNodes {
log.Root().SetHandler(log.DiscardHandler())
}
cfg := DefaultSystemConfig(t)
// slow down L1 blocks so we can see the L2 blocks arrive well before the L1 blocks do.
// Keep the seq window small so the L2 chain is started quick
cfg.DeployConfig.L1BlockTime = 10
var published, received []common.Hash
seqTracer, verifTracer := new(FnTracer), new(FnTracer)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.BlockHash)
}
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received = append(received, payload.BlockHash)
}
cfg.Nodes["sequencer"].Tracer = seqTracer
cfg.Nodes["verifier"].Tracer = verifTracer
sys, err := cfg.Start(SystemConfigOption{
key: "afterRollupNodeStart",
role: "sequencer",
action: func(sCfg *SystemConfig, system *System) {
rpc, _ := system.Nodes["sequencer"].Attach() // never errors
cfg.Nodes["verifier"].L2Sync = &rollupNode.L2SyncRPCConfig{
Rpc: client.NewBaseRPCClient(rpc),
}
},
})
require.Nil(t, err, "Error starting up system")
defer sys.Close()
l2Seq := sys.Clients["sequencer"]
l2Verif := sys.Clients["verifier"]
// Transactor Account
ethPrivKey := cfg.Secrets.Alice
// Submit a TX to L2 sequencer node
toAddr := common.Address{0xff, 0xff}
tx := types.MustSignNewTx(ethPrivKey, types.LatestSignerForChainID(cfg.L2ChainIDBig()), &types.DynamicFeeTx{
ChainID: cfg.L2ChainIDBig(),
Nonce: 0,
To: &toAddr,
Value: big.NewInt(1_000_000_000),
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(200),
Gas: 21000,
})
err = l2Seq.SendTransaction(context.Background(), tx)
require.Nil(t, err, "Sending L2 tx to sequencer")
// Wait for tx to be mined on the L2 sequencer chain
receiptSeq, err := waitForTransaction(tx.Hash(), l2Seq, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on sequencer")
// Wait for alt RPC sync to pick up the blocks on the sequencer chain
receiptVerif, err := waitForTransaction(tx.Hash(), l2Verif, 12*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier")
require.Equal(t, receiptSeq, receiptVerif)
// Verify that the tx was received via RPC sync (P2P is disabled)
require.Contains(t, received, receiptVerif.BlockHash)
// Verify that everything that was received was published
require.GreaterOrEqual(t, len(published), len(received))
require.ElementsMatch(t, received, published[:len(received)])
}
// TestSystemDenseTopology sets up a dense p2p topology with 3 verifier nodes and 1 sequencer node.
func TestSystemDenseTopology(t *testing.T) {
t.Skip("Skipping dense topology test to avoid flakiness. @refcell address in p2p scoring pr.")
parallel(t) parallel(t)
if !verboseGethNodes { if !verboseGethNodes {
log.Root().SetHandler(log.DiscardHandler()) log.Root().SetHandler(log.DiscardHandler())
...@@ -682,31 +767,11 @@ func TestSystemMockPeerScoring(t *testing.T) { ...@@ -682,31 +767,11 @@ func TestSystemMockPeerScoring(t *testing.T) {
cfg.Loggers["verifier2"] = testlog.Logger(t, log.LvlInfo).New("role", "verifier") cfg.Loggers["verifier2"] = testlog.Logger(t, log.LvlInfo).New("role", "verifier")
cfg.Loggers["verifier3"] = testlog.Logger(t, log.LvlInfo).New("role", "verifier") cfg.Loggers["verifier3"] = testlog.Logger(t, log.LvlInfo).New("role", "verifier")
// Construct a new sequencer with an invalid privkey to produce invalid gossip
// We can then test that the peer scoring system will ban the node
sequencer2PrivateKey := cfg.Secrets.Mallory
cfg.Nodes["sequencer2"] = &rollupNode.Config{
Driver: driver.Config{
VerifierConfDepth: 0,
SequencerConfDepth: 0,
SequencerEnabled: true,
},
// Submitter PrivKey is set in system start for rollup nodes where sequencer = true
RPC: rollupNode.RPCConfig{
ListenAddr: "127.0.0.1",
ListenPort: 0,
EnableAdmin: true,
},
L1EpochPollInterval: time.Second * 4,
P2PSigner: &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(sequencer2PrivateKey)},
}
cfg.Loggers["sequencer2"] = testlog.Logger(t, log.LvlInfo).New("role", "sequencer")
// connect the nodes // connect the nodes
cfg.P2PTopology = map[string][]string{ cfg.P2PTopology = map[string][]string{
"verifier": {"sequencer", "sequencer2", "verifier2", "verifier3"}, "verifier": {"sequencer", "verifier2", "verifier3"},
"verifier2": {"sequencer", "sequencer2", "verifier", "verifier3"}, "verifier2": {"sequencer", "verifier", "verifier3"},
"verifier3": {"sequencer", "sequencer2", "verifier", "verifier2"}, "verifier3": {"sequencer", "verifier", "verifier2"},
} }
// Set peer scoring for each node, but without banning // Set peer scoring for each node, but without banning
...@@ -719,15 +784,11 @@ func TestSystemMockPeerScoring(t *testing.T) { ...@@ -719,15 +784,11 @@ func TestSystemMockPeerScoring(t *testing.T) {
} }
} }
var published, published2, received1, received2, received3 []common.Hash var published, received1, received2, received3 []common.Hash
seqTracer, verifTracer, verifTracer2, verifTracer3 := new(FnTracer), new(FnTracer), new(FnTracer), new(FnTracer) seqTracer, verifTracer, verifTracer2, verifTracer3 := new(FnTracer), new(FnTracer), new(FnTracer), new(FnTracer)
seq2Tracer := new(FnTracer)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) { seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.BlockHash) published = append(published, payload.BlockHash)
} }
seq2Tracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published2 = append(published2, payload.BlockHash)
}
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) { verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received1 = append(received1, payload.BlockHash) received1 = append(received1, payload.BlockHash)
} }
...@@ -738,7 +799,6 @@ func TestSystemMockPeerScoring(t *testing.T) { ...@@ -738,7 +799,6 @@ func TestSystemMockPeerScoring(t *testing.T) {
received3 = append(received3, payload.BlockHash) received3 = append(received3, payload.BlockHash)
} }
cfg.Nodes["sequencer"].Tracer = seqTracer cfg.Nodes["sequencer"].Tracer = seqTracer
cfg.Nodes["sequencer2"].Tracer = seq2Tracer
cfg.Nodes["verifier"].Tracer = verifTracer cfg.Nodes["verifier"].Tracer = verifTracer
cfg.Nodes["verifier2"].Tracer = verifTracer2 cfg.Nodes["verifier2"].Tracer = verifTracer2
cfg.Nodes["verifier3"].Tracer = verifTracer3 cfg.Nodes["verifier3"].Tracer = verifTracer3
...@@ -748,7 +808,6 @@ func TestSystemMockPeerScoring(t *testing.T) { ...@@ -748,7 +808,6 @@ func TestSystemMockPeerScoring(t *testing.T) {
defer sys.Close() defer sys.Close()
l2Seq := sys.Clients["sequencer"] l2Seq := sys.Clients["sequencer"]
// l2Seq2 := sys.Clients["sequencer2"]
l2Verif := sys.Clients["verifier"] l2Verif := sys.Clients["verifier"]
l2Verif2 := sys.Clients["verifier2"] l2Verif2 := sys.Clients["verifier2"]
l2Verif3 := sys.Clients["verifier3"] l2Verif3 := sys.Clients["verifier3"]
...@@ -768,23 +827,23 @@ func TestSystemMockPeerScoring(t *testing.T) { ...@@ -768,23 +827,23 @@ func TestSystemMockPeerScoring(t *testing.T) {
Gas: 21000, Gas: 21000,
}) })
err = l2Seq.SendTransaction(context.Background(), tx) err = l2Seq.SendTransaction(context.Background(), tx)
require.Nil(t, err, "Sending L2 tx to sequencer") require.NoError(t, err, "Sending L2 tx to sequencer")
// Wait for tx to be mined on the L2 sequencer chain // Wait for tx to be mined on the L2 sequencer chain
receiptSeq, err := waitForTransaction(tx.Hash(), l2Seq, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second) receiptSeq, err := waitForTransaction(tx.Hash(), l2Seq, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on sequencer") require.NoError(t, err, "Waiting for L2 tx on sequencer")
// Wait until the block it was first included in shows up in the safe chain on the verifier // Wait until the block it was first included in shows up in the safe chain on the verifier
receiptVerif, err := waitForTransaction(tx.Hash(), l2Verif, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second) receiptVerif, err := waitForTransaction(tx.Hash(), l2Verif, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier") require.NoError(t, err, "Waiting for L2 tx on verifier")
require.Equal(t, receiptSeq, receiptVerif) require.Equal(t, receiptSeq, receiptVerif)
receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif2, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second) receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif2, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier2") require.NoError(t, err, "Waiting for L2 tx on verifier2")
require.Equal(t, receiptSeq, receiptVerif) require.Equal(t, receiptSeq, receiptVerif)
receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif3, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second) receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif3, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier3") require.NoError(t, err, "Waiting for L2 tx on verifier3")
require.Equal(t, receiptSeq, receiptVerif) require.Equal(t, receiptSeq, receiptVerif)
// Verify that everything that was received was published // Verify that everything that was received was published
...@@ -799,37 +858,6 @@ func TestSystemMockPeerScoring(t *testing.T) { ...@@ -799,37 +858,6 @@ func TestSystemMockPeerScoring(t *testing.T) {
require.Contains(t, received1, receiptVerif.BlockHash) require.Contains(t, received1, receiptVerif.BlockHash)
require.Contains(t, received2, receiptVerif.BlockHash) require.Contains(t, received2, receiptVerif.BlockHash)
require.Contains(t, received3, receiptVerif.BlockHash) require.Contains(t, received3, receiptVerif.BlockHash)
// Submit TX to the second (malicious) sequencer node
// toAddr = common.Address{0xff, 0xff}
// maliciousTx := types.MustSignNewTx(ethPrivKey, types.LatestSignerForChainID(cfg.L2ChainIDBig()), &types.DynamicFeeTx{
// ChainID: cfg.L2ChainIDBig(),
// Nonce: 1,
// To: &toAddr,
// Value: big.NewInt(1_000_000_000),
// GasTipCap: big.NewInt(10),
// GasFeeCap: big.NewInt(200),
// Gas: 21000,
// })
// err = l2Seq2.SendTransaction(context.Background(), maliciousTx)
// require.Nil(t, err, "Sending L2 tx to sequencer")
// Wait for tx to be mined on the L2 sequencer chain
// receiptSeq, err = waitForTransaction(maliciousTx.Hash(), l2Seq2, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on sequencer")
// Wait until the block it was first included in shows up in the safe chain on the verifier
// receiptVerif, err = waitForTransaction(maliciousTx.Hash(), l2Verif, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on verifier")
// require.Equal(t, receiptSeq, receiptVerif)
// receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif2, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on verifier2")
// require.Equal(t, receiptSeq, receiptVerif)
// receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif3, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on verifier3")
// require.Equal(t, receiptSeq, receiptVerif)
} }
func TestL1InfoContract(t *testing.T) { func TestL1InfoContract(t *testing.T) {
......
...@@ -185,6 +185,12 @@ var ( ...@@ -185,6 +185,12 @@ var (
EnvVar: prefixEnvVar("HEARTBEAT_URL"), EnvVar: prefixEnvVar("HEARTBEAT_URL"),
Value: "https://heartbeat.optimism.io", Value: "https://heartbeat.optimism.io",
} }
BackupL2UnsafeSyncRPC = cli.StringFlag{
Name: "l2.backup-unsafe-sync-rpc",
Usage: "Set the backup L2 unsafe sync RPC endpoint.",
EnvVar: prefixEnvVar("L2_BACKUP_UNSAFE_SYNC_RPC"),
Required: false,
}
) )
var requiredFlags = []cli.Flag{ var requiredFlags = []cli.Flag{
...@@ -219,6 +225,7 @@ var optionalFlags = append([]cli.Flag{ ...@@ -219,6 +225,7 @@ var optionalFlags = append([]cli.Flag{
HeartbeatEnabledFlag, HeartbeatEnabledFlag,
HeartbeatMonikerFlag, HeartbeatMonikerFlag,
HeartbeatURLFlag, HeartbeatURLFlag,
BackupL2UnsafeSyncRPC,
}, p2pFlags...) }, p2pFlags...)
// Flags contains the list of configuration options available to the binary. // Flags contains the list of configuration options available to the binary.
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
...@@ -114,7 +115,16 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*et ...@@ -114,7 +115,16 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*et
} }
var l2OutputRootVersion eth.Bytes32 // it's zero for now var l2OutputRootVersion eth.Bytes32 // it's zero for now
l2OutputRoot := rollup.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root(), proof.StorageHash) l2OutputRoot, err := rollup.ComputeL2OutputRoot(&bindings.TypesOutputRootProof{
Version: l2OutputRootVersion,
StateRoot: head.Root(),
MessagePasserStorageRoot: proof.StorageHash,
LatestBlockhash: head.Hash(),
})
if err != nil {
n.log.Error("Error computing L2 output root, nil ptr passed to hashing function")
return nil, err
}
return &eth.OutputResponse{ return &eth.OutputResponse{
Version: l2OutputRootVersion, Version: l2OutputRootVersion,
......
...@@ -19,6 +19,11 @@ type L2EndpointSetup interface { ...@@ -19,6 +19,11 @@ type L2EndpointSetup interface {
Check() error Check() error
} }
type L2SyncEndpointSetup interface {
Setup(ctx context.Context, log log.Logger) (cl client.RPC, err error)
Check() error
}
type L1EndpointSetup interface { type L1EndpointSetup interface {
// Setup a RPC client to a L1 node to pull rollup input-data from. // Setup a RPC client to a L1 node to pull rollup input-data from.
// The results of the RPC client may be trusted for faster processing, or strictly validated. // The results of the RPC client may be trusted for faster processing, or strictly validated.
...@@ -75,6 +80,50 @@ func (p *PreparedL2Endpoints) Setup(ctx context.Context, log log.Logger) (client ...@@ -75,6 +80,50 @@ func (p *PreparedL2Endpoints) Setup(ctx context.Context, log log.Logger) (client
return p.Client, nil return p.Client, nil
} }
// L2SyncEndpointConfig contains configuration for the fallback sync endpoint
type L2SyncEndpointConfig struct {
// Address of the L2 RPC to use for backup sync
L2NodeAddr string
}
var _ L2SyncEndpointSetup = (*L2SyncEndpointConfig)(nil)
func (cfg *L2SyncEndpointConfig) Setup(ctx context.Context, log log.Logger) (client.RPC, error) {
l2Node, err := client.NewRPC(ctx, log, cfg.L2NodeAddr)
if err != nil {
return nil, err
}
return l2Node, nil
}
func (cfg *L2SyncEndpointConfig) Check() error {
if cfg.L2NodeAddr == "" {
return errors.New("empty L2 Node Address")
}
return nil
}
type L2SyncRPCConfig struct {
// RPC endpoint to use for syncing
Rpc client.RPC
}
var _ L2SyncEndpointSetup = (*L2SyncRPCConfig)(nil)
func (cfg *L2SyncRPCConfig) Setup(ctx context.Context, log log.Logger) (client.RPC, error) {
return cfg.Rpc, nil
}
func (cfg *L2SyncRPCConfig) Check() error {
if cfg.Rpc == nil {
return errors.New("rpc cannot be nil")
}
return nil
}
type L1EndpointConfig struct { type L1EndpointConfig struct {
L1NodeAddr string // Address of L1 User JSON-RPC endpoint to use (eth namespace required) L1NodeAddr string // Address of L1 User JSON-RPC endpoint to use (eth namespace required)
......
...@@ -15,6 +15,7 @@ import ( ...@@ -15,6 +15,7 @@ import (
type Config struct { type Config struct {
L1 L1EndpointSetup L1 L1EndpointSetup
L2 L2EndpointSetup L2 L2EndpointSetup
L2Sync L2SyncEndpointSetup
Driver driver.Config Driver driver.Config
......
...@@ -197,7 +197,28 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger ...@@ -197,7 +197,28 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger
return err return err
} }
n.l2Driver = driver.NewDriver(&cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source, n, n.log, snapshotLog, n.metrics) var syncClient *sources.SyncClient
// If the L2 sync config is present, use it to create a sync client
if cfg.L2Sync != nil {
if err := cfg.L2Sync.Check(); err != nil {
log.Info("L2 sync config is not present, skipping L2 sync client setup", "err", err)
} else {
rpcSyncClient, err := cfg.L2Sync.Setup(ctx, n.log)
if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err)
}
// The sync client's RPC is always trusted
config := sources.SyncClientDefaultConfig(&cfg.Rollup, true)
syncClient, err = sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, config)
if err != nil {
return fmt.Errorf("failed to create sync client: %w", err)
}
}
}
n.l2Driver = driver.NewDriver(&cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source, syncClient, n, n.log, snapshotLog, n.metrics)
return nil return nil
} }
...@@ -263,13 +284,21 @@ func (n *OpNode) initP2PSigner(ctx context.Context, cfg *Config) error { ...@@ -263,13 +284,21 @@ func (n *OpNode) initP2PSigner(ctx context.Context, cfg *Config) error {
func (n *OpNode) Start(ctx context.Context) error { func (n *OpNode) Start(ctx context.Context) error {
n.log.Info("Starting execution engine driver") n.log.Info("Starting execution engine driver")
// start driving engine: sync blocks by deriving them from L1 and driving them into the engine // start driving engine: sync blocks by deriving them from L1 and driving them into the engine
err := n.l2Driver.Start() if err := n.l2Driver.Start(); err != nil {
if err != nil {
n.log.Error("Could not start a rollup node", "err", err) n.log.Error("Could not start a rollup node", "err", err)
return err return err
} }
// If the backup unsafe sync client is enabled, start its event loop
if n.l2Driver.L2SyncCl != nil {
if err := n.l2Driver.L2SyncCl.Start(); err != nil {
n.log.Error("Could not start the backup sync client", "err", err)
return err
}
}
return nil return nil
} }
...@@ -382,6 +411,13 @@ func (n *OpNode) Close() error { ...@@ -382,6 +411,13 @@ func (n *OpNode) Close() error {
if err := n.l2Driver.Close(); err != nil { if err := n.l2Driver.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to close L2 engine driver cleanly: %w", err)) result = multierror.Append(result, fmt.Errorf("failed to close L2 engine driver cleanly: %w", err))
} }
// If the L2 sync client is present & running, close it.
if n.l2Driver.L2SyncCl != nil {
if err := n.l2Driver.L2SyncCl.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to close L2 engine backup sync client cleanly: %w", err))
}
}
} }
// close L2 engine RPC client // close L2 engine RPC client
......
...@@ -77,7 +77,7 @@ func (cb *ChannelBank) prune() { ...@@ -77,7 +77,7 @@ func (cb *ChannelBank) prune() {
// Read() should be called repeatedly first, until everything has been read, before adding new data. // Read() should be called repeatedly first, until everything has been read, before adding new data.
func (cb *ChannelBank) IngestFrame(f Frame) { func (cb *ChannelBank) IngestFrame(f Frame) {
origin := cb.Origin() origin := cb.Origin()
log := log.New("origin", origin, "channel", f.ID, "length", len(f.Data), "frame_number", f.FrameNumber, "is_last", f.IsLast) log := cb.log.New("origin", origin, "channel", f.ID, "length", len(f.Data), "frame_number", f.FrameNumber, "is_last", f.IsLast)
log.Debug("channel bank got new data") log.Debug("channel bank got new data")
currentCh, ok := cb.channels[f.ID] currentCh, ok := cb.channels[f.ID]
......
...@@ -133,6 +133,7 @@ func NewEngineQueue(log log.Logger, cfg *rollup.Config, engine Engine, metrics M ...@@ -133,6 +133,7 @@ func NewEngineQueue(log log.Logger, cfg *rollup.Config, engine Engine, metrics M
unsafePayloads: PayloadsQueue{ unsafePayloads: PayloadsQueue{
MaxSize: maxUnsafePayloadsMemory, MaxSize: maxUnsafePayloadsMemory,
SizeFn: payloadMemSize, SizeFn: payloadMemSize,
blockNos: make(map[uint64]bool),
}, },
prev: prev, prev: prev,
l1Fetcher: l1Fetcher, l1Fetcher: l1Fetcher,
...@@ -485,6 +486,7 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error { ...@@ -485,6 +486,7 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error {
_, errType, err = eq.ConfirmPayload(ctx) _, errType, err = eq.ConfirmPayload(ctx)
} }
if err != nil { if err != nil {
_ = eq.CancelPayload(ctx, true)
switch errType { switch errType {
case BlockInsertTemporaryErr: case BlockInsertTemporaryErr:
// RPC errors are recoverable, we can retry the buffered payload attributes later. // RPC errors are recoverable, we can retry the buffered payload attributes later.
...@@ -661,3 +663,20 @@ func (eq *EngineQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.System ...@@ -661,3 +663,20 @@ func (eq *EngineQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.System
eq.logSyncProgress("reset derivation work") eq.logSyncProgress("reset derivation work")
return io.EOF return io.EOF
} }
// GetUnsafeQueueGap retrieves the current [start, end] range of the gap between the tip of the unsafe priority queue and the unsafe head.
// If there is no gap, the difference between end and start will be 0.
func (eq *EngineQueue) GetUnsafeQueueGap(expectedNumber uint64) (start uint64, end uint64) {
// The start of the gap is always the unsafe head + 1
start = eq.unsafeHead.Number + 1
// If the priority queue is empty, the end is the first block number at the top of the priority queue
// Otherwise, the end is the expected block number
if first := eq.unsafePayloads.Peek(); first != nil {
end = first.ID().Number
} else {
end = expectedNumber
}
return start, end
}
...@@ -77,6 +77,7 @@ type PayloadsQueue struct { ...@@ -77,6 +77,7 @@ type PayloadsQueue struct {
pq payloadsByNumber pq payloadsByNumber
currentSize uint64 currentSize uint64
MaxSize uint64 MaxSize uint64
blockNos map[uint64]bool
SizeFn func(p *eth.ExecutionPayload) uint64 SizeFn func(p *eth.ExecutionPayload) uint64
} }
...@@ -99,6 +100,9 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error { ...@@ -99,6 +100,9 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error {
if p == nil { if p == nil {
return errors.New("cannot add nil payload") return errors.New("cannot add nil payload")
} }
if upq.blockNos[p.ID().Number] {
return errors.New("cannot add duplicate payload")
}
size := upq.SizeFn(p) size := upq.SizeFn(p)
if size > upq.MaxSize { if size > upq.MaxSize {
return fmt.Errorf("cannot add payload %s, payload mem size %d is larger than max queue size %d", p.ID(), size, upq.MaxSize) return fmt.Errorf("cannot add payload %s, payload mem size %d is larger than max queue size %d", p.ID(), size, upq.MaxSize)
...@@ -111,6 +115,7 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error { ...@@ -111,6 +115,7 @@ func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error {
for upq.currentSize > upq.MaxSize { for upq.currentSize > upq.MaxSize {
upq.Pop() upq.Pop()
} }
upq.blockNos[p.ID().Number] = true
return nil return nil
} }
...@@ -132,5 +137,7 @@ func (upq *PayloadsQueue) Pop() *eth.ExecutionPayload { ...@@ -132,5 +137,7 @@ func (upq *PayloadsQueue) Pop() *eth.ExecutionPayload {
} }
ps := heap.Pop(&upq.pq).(payloadAndSize) // nosemgrep ps := heap.Pop(&upq.pq).(payloadAndSize) // nosemgrep
upq.currentSize -= ps.size upq.currentSize -= ps.size
// remove the key from the blockNos map
delete(upq.blockNos, ps.payload.ID().Number)
return ps.payload return ps.payload
} }
...@@ -77,6 +77,7 @@ func TestPayloadsQueue(t *testing.T) { ...@@ -77,6 +77,7 @@ func TestPayloadsQueue(t *testing.T) {
pq := PayloadsQueue{ pq := PayloadsQueue{
MaxSize: payloadMemFixedCost * 3, MaxSize: payloadMemFixedCost * 3,
SizeFn: payloadMemSize, SizeFn: payloadMemSize,
blockNos: make(map[uint64]bool),
} }
require.Equal(t, 0, pq.Len()) require.Equal(t, 0, pq.Len())
require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Peek()) require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Peek())
...@@ -85,6 +86,7 @@ func TestPayloadsQueue(t *testing.T) { ...@@ -85,6 +86,7 @@ func TestPayloadsQueue(t *testing.T) {
a := &eth.ExecutionPayload{BlockNumber: 3} a := &eth.ExecutionPayload{BlockNumber: 3}
b := &eth.ExecutionPayload{BlockNumber: 4} b := &eth.ExecutionPayload{BlockNumber: 4}
c := &eth.ExecutionPayload{BlockNumber: 5} c := &eth.ExecutionPayload{BlockNumber: 5}
d := &eth.ExecutionPayload{BlockNumber: 6}
bAlt := &eth.ExecutionPayload{BlockNumber: 4} bAlt := &eth.ExecutionPayload{BlockNumber: 4}
require.NoError(t, pq.Push(b)) require.NoError(t, pq.Push(b))
require.Equal(t, pq.Len(), 1) require.Equal(t, pq.Len(), 1)
...@@ -105,28 +107,33 @@ func TestPayloadsQueue(t *testing.T) { ...@@ -105,28 +107,33 @@ func TestPayloadsQueue(t *testing.T) {
require.Equal(t, pq.Pop(), a) require.Equal(t, pq.Pop(), a)
require.Equal(t, pq.Len(), 2, "expecting to pop the lowest") require.Equal(t, pq.Len(), 2, "expecting to pop the lowest")
require.NoError(t, pq.Push(bAlt)) require.Equal(t, pq.Peek(), b, "expecting b to be lowest, compared to c")
require.Equal(t, pq.Len(), 3)
require.Equal(t, pq.Peek(), b, "expecting b to be lowest, compared to bAlt and c")
require.Equal(t, pq.Pop(), b) require.Equal(t, pq.Pop(), b)
require.Equal(t, pq.Len(), 2)
require.Equal(t, pq.MemSize(), 2*payloadMemFixedCost)
require.Equal(t, pq.Pop(), bAlt)
require.Equal(t, pq.Len(), 1) require.Equal(t, pq.Len(), 1)
require.Equal(t, pq.Peek(), c, "expecting c to only remain") require.Equal(t, pq.MemSize(), payloadMemFixedCost)
require.Equal(t, pq.Pop(), c)
require.Equal(t, pq.Len(), 0, "expecting no items to remain")
d := &eth.ExecutionPayload{BlockNumber: 5, Transactions: []eth.Data{make([]byte, payloadMemFixedCost*3+1)}} e := &eth.ExecutionPayload{BlockNumber: 5, Transactions: []eth.Data{make([]byte, payloadMemFixedCost*3+1)}}
require.Error(t, pq.Push(d), "cannot add payloads that are too large") require.Error(t, pq.Push(e), "cannot add payloads that are too large")
require.NoError(t, pq.Push(b)) require.NoError(t, pq.Push(b))
require.Equal(t, pq.Len(), 1, "expecting b")
require.Equal(t, pq.Peek(), b)
require.NoError(t, pq.Push(c))
require.Equal(t, pq.Len(), 2, "expecting b, c") require.Equal(t, pq.Len(), 2, "expecting b, c")
require.Equal(t, pq.Peek(), b) require.Equal(t, pq.Peek(), b)
require.NoError(t, pq.Push(a)) require.NoError(t, pq.Push(a))
require.Equal(t, pq.Len(), 3, "expecting a, b, c") require.Equal(t, pq.Len(), 3, "expecting a, b, c")
require.Equal(t, pq.Peek(), a) require.Equal(t, pq.Peek(), a)
require.NoError(t, pq.Push(bAlt))
require.Equal(t, pq.Len(), 3, "expecting b, bAlt, c") // No duplicates allowed
require.Error(t, pq.Push(bAlt))
require.NoError(t, pq.Push(d))
require.Equal(t, pq.Len(), 3)
require.Equal(t, pq.Peek(), b, "expecting b, c, d")
require.NotContainsf(t, pq.pq[:], a, "a should be dropped after 3 items already exist under max size constraint") require.NotContainsf(t, pq.pq[:], a, "a should be dropped after 3 items already exist under max size constraint")
} }
...@@ -51,6 +51,7 @@ type EngineQueueStage interface { ...@@ -51,6 +51,7 @@ type EngineQueueStage interface {
Finalize(l1Origin eth.L1BlockRef) Finalize(l1Origin eth.L1BlockRef)
AddUnsafePayload(payload *eth.ExecutionPayload) AddUnsafePayload(payload *eth.ExecutionPayload)
GetUnsafeQueueGap(expectedNumber uint64) (uint64, uint64)
Step(context.Context) error Step(context.Context) error
} }
...@@ -160,6 +161,12 @@ func (dp *DerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayload) { ...@@ -160,6 +161,12 @@ func (dp *DerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayload) {
dp.eng.AddUnsafePayload(payload) dp.eng.AddUnsafePayload(payload)
} }
// GetUnsafeQueueGap retrieves the current [start, end] range of the gap between the tip of the unsafe priority queue and the unsafe head.
// If there is no gap, the start and end will be 0.
func (dp *DerivationPipeline) GetUnsafeQueueGap(expectedNumber uint64) (uint64, uint64) {
return dp.eng.GetUnsafeQueueGap(expectedNumber)
}
// Step tries to progress the buffer. // Step tries to progress the buffer.
// An EOF is returned if there pipeline is blocked by waiting for new L1 data. // An EOF is returned if there pipeline is blocked by waiting for new L1 data.
// If ctx errors no error is returned, but the step may exit early in a state that can still be continued. // If ctx errors no error is returned, but the step may exit early in a state that can still be continued.
......
package test
import (
"math/rand"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie"
)
// RandomL2Block returns a random block whose first transaction is a random
// L1 Info Deposit transaction.
func RandomL2Block(rng *rand.Rand, txCount int) (*types.Block, []*types.Receipt) {
l1Block := types.NewBlock(testutils.RandomHeader(rng),
nil, nil, nil, trie.NewStackTrie(nil))
l1InfoTx, err := derive.L1InfoDeposit(0, l1Block, eth.SystemConfig{}, testutils.RandomBool(rng))
if err != nil {
panic("L1InfoDeposit: " + err.Error())
}
return testutils.RandomBlockPrependTxs(rng, txCount, types.NewTx(l1InfoTx))
}
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
) )
type Metrics interface { type Metrics interface {
...@@ -48,6 +49,7 @@ type DerivationPipeline interface { ...@@ -48,6 +49,7 @@ type DerivationPipeline interface {
Reset() Reset()
Step(ctx context.Context) error Step(ctx context.Context) error
AddUnsafePayload(payload *eth.ExecutionPayload) AddUnsafePayload(payload *eth.ExecutionPayload)
GetUnsafeQueueGap(expectedNumber uint64) (uint64, uint64)
Finalize(ref eth.L1BlockRef) Finalize(ref eth.L1BlockRef)
FinalizedL1() eth.L1BlockRef FinalizedL1() eth.L1BlockRef
Finalized() eth.L2BlockRef Finalized() eth.L2BlockRef
...@@ -80,7 +82,7 @@ type Network interface { ...@@ -80,7 +82,7 @@ type Network interface {
} }
// NewDriver composes an events handler that tracks L1 state, triggers L2 derivation, and optionally sequences new L2 blocks. // NewDriver composes an events handler that tracks L1 state, triggers L2 derivation, and optionally sequences new L2 blocks.
func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, network Network, log log.Logger, snapshotLog log.Logger, metrics Metrics) *Driver { func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, syncClient *sources.SyncClient, network Network, log log.Logger, snapshotLog log.Logger, metrics Metrics) *Driver {
l1State := NewL1State(log, metrics) l1State := NewL1State(log, metrics)
sequencerConfDepth := NewConfDepth(driverCfg.SequencerConfDepth, l1State.L1Head, l1) sequencerConfDepth := NewConfDepth(driverCfg.SequencerConfDepth, l1State.L1Head, l1)
findL1Origin := NewL1OriginSelector(log, cfg, sequencerConfDepth) findL1Origin := NewL1OriginSelector(log, cfg, sequencerConfDepth)
...@@ -112,5 +114,6 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, ne ...@@ -112,5 +114,6 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, ne
l1SafeSig: make(chan eth.L1BlockRef, 10), l1SafeSig: make(chan eth.L1BlockRef, 10),
l1FinalizedSig: make(chan eth.L1BlockRef, 10), l1FinalizedSig: make(chan eth.L1BlockRef, 10),
unsafeL2Payloads: make(chan *eth.ExecutionPayload, 10), unsafeL2Payloads: make(chan *eth.ExecutionPayload, 10),
L2SyncCl: syncClient,
} }
} }
...@@ -16,6 +16,7 @@ import ( ...@@ -16,6 +16,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-service/backoff" "github.com/ethereum-optimism/optimism/op-service/backoff"
) )
...@@ -63,7 +64,11 @@ type Driver struct { ...@@ -63,7 +64,11 @@ type Driver struct {
l1SafeSig chan eth.L1BlockRef l1SafeSig chan eth.L1BlockRef
l1FinalizedSig chan eth.L1BlockRef l1FinalizedSig chan eth.L1BlockRef
// Backup unsafe sync client
L2SyncCl *sources.SyncClient
// L2 Signals: // L2 Signals:
unsafeL2Payloads chan *eth.ExecutionPayload unsafeL2Payloads chan *eth.ExecutionPayload
l1 L1Chain l1 L1Chain
...@@ -195,6 +200,12 @@ func (s *Driver) eventLoop() { ...@@ -195,6 +200,12 @@ func (s *Driver) eventLoop() {
sequencerTimer.Reset(delay) sequencerTimer.Reset(delay)
} }
// Create a ticker to check if there is a gap in the engine queue every 15 seconds
// If there is, we send requests to the backup RPC to retrieve the missing payloads
// and add them to the unsafe queue.
altSyncTicker := time.NewTicker(15 * time.Second)
defer altSyncTicker.Stop()
for { for {
// If we are sequencing, and the L1 state is ready, update the trigger for the next sequencer action. // If we are sequencing, and the L1 state is ready, update the trigger for the next sequencer action.
// This may adjust at any time based on fork-choice changes or previous errors. // This may adjust at any time based on fork-choice changes or previous errors.
...@@ -223,6 +234,12 @@ func (s *Driver) eventLoop() { ...@@ -223,6 +234,12 @@ func (s *Driver) eventLoop() {
} }
} }
planSequencerAction() // schedule the next sequencer action to keep the sequencing looping planSequencerAction() // schedule the next sequencer action to keep the sequencing looping
case <-altSyncTicker.C:
// Check if there is a gap in the current unsafe payload queue. If there is, attempt to fetch
// missing payloads from the backup RPC (if it is configured).
if s.L2SyncCl != nil {
s.checkForGapInUnsafeQueue(ctx)
}
case payload := <-s.unsafeL2Payloads: case payload := <-s.unsafeL2Payloads:
s.snapshot("New unsafe payload") s.snapshot("New unsafe payload")
s.log.Info("Optimistically queueing unsafe L2 execution payload", "id", payload.ID()) s.log.Info("Optimistically queueing unsafe L2 execution payload", "id", payload.ID())
...@@ -442,3 +459,36 @@ type hashAndErrorChannel struct { ...@@ -442,3 +459,36 @@ type hashAndErrorChannel struct {
hash common.Hash hash common.Hash
err chan error err chan error
} }
// checkForGapInUnsafeQueue checks if there is a gap in the unsafe queue and attempts to retrieve the missing payloads from the backup RPC.
// WARNING: The sync client's attempt to retrieve the missing payloads is not guaranteed to succeed, and it will fail silently (besides
// emitting warning logs) if the requests fail.
func (s *Driver) checkForGapInUnsafeQueue(ctx context.Context) {
// subtract genesis time from wall clock to get the time elapsed since genesis, and then divide that
// difference by the block time to get the expected L2 block number at the current time. If the
// unsafe head does not have this block number, then there is a gap in the queue.
wallClock := uint64(time.Now().Unix())
genesisTimestamp := s.config.Genesis.L2Time
wallClockGenesisDiff := wallClock - genesisTimestamp
expectedL2Block := wallClockGenesisDiff / s.config.BlockTime
start, end := s.derivation.GetUnsafeQueueGap(expectedL2Block)
size := end - start
// Check if there is a gap between the unsafe head and the expected L2 block number at the current time.
if size > 0 {
s.log.Warn("Gap in payload queue tip and expected unsafe chain detected", "start", start, "end", end, "size", size)
s.log.Info("Attempting to fetch missing payloads from backup RPC", "start", start, "end", end, "size", size)
// Attempt to fetch the missing payloads from the backup unsafe sync RPC concurrently.
// Concurrent requests are safe here due to the engine queue being a priority queue.
for blockNumber := start; blockNumber <= end; blockNumber++ {
select {
case s.L2SyncCl.FetchUnsafeBlock <- blockNumber:
// Do nothing- the block number was successfully sent into the channel
default:
return // If the channel is full, return and wait for the next iteration of the event loop
}
}
}
}
package rollup package rollup
import ( import (
"errors"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
) )
// ComputeL2OutputRoot computes the L2 output root var NilProof = errors.New("Output root proof is nil")
func ComputeL2OutputRoot(l2OutputRootVersion eth.Bytes32, blockHash common.Hash, blockRoot common.Hash, storageRoot common.Hash) eth.Bytes32 {
digest := crypto.Keccak256Hash( // ComputeL2OutputRoot computes the L2 output root by hashing an output root proof.
l2OutputRootVersion[:], func ComputeL2OutputRoot(proofElements *bindings.TypesOutputRootProof) (eth.Bytes32, error) {
blockRoot.Bytes(), if proofElements == nil {
storageRoot[:], return eth.Bytes32{}, NilProof
blockHash.Bytes(), }
)
return eth.Bytes32(digest)
}
// HashOutputRootProof computes the hash of the output root proof digest := crypto.Keccak256Hash(
func HashOutputRootProof(proof *bindings.TypesOutputRootProof) eth.Bytes32 { proofElements.Version[:],
return ComputeL2OutputRoot( proofElements.StateRoot[:],
proof.Version, proofElements.MessagePasserStorageRoot[:],
proof.StateRoot, proofElements.LatestBlockhash[:],
proof.MessagePasserStorageRoot,
proof.LatestBlockhash,
) )
return eth.Bytes32(digest), nil
} }
...@@ -36,10 +36,7 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { ...@@ -36,10 +36,7 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) {
return nil, err return nil, err
} }
driverConfig, err := NewDriverConfig(ctx) driverConfig := NewDriverConfig(ctx)
if err != nil {
return nil, err
}
p2pSignerSetup, err := p2pcli.LoadSignerSetup(ctx) p2pSignerSetup, err := p2pcli.LoadSignerSetup(ctx)
if err != nil { if err != nil {
...@@ -51,19 +48,19 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { ...@@ -51,19 +48,19 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) {
return nil, fmt.Errorf("failed to load p2p config: %w", err) return nil, fmt.Errorf("failed to load p2p config: %w", err)
} }
l1Endpoint, err := NewL1EndpointConfig(ctx) l1Endpoint := NewL1EndpointConfig(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load l1 endpoint info: %w", err)
}
l2Endpoint, err := NewL2EndpointConfig(ctx, log) l2Endpoint, err := NewL2EndpointConfig(ctx, log)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load l2 endpoints info: %w", err) return nil, fmt.Errorf("failed to load l2 endpoints info: %w", err)
} }
l2SyncEndpoint := NewL2SyncEndpointConfig(ctx)
cfg := &node.Config{ cfg := &node.Config{
L1: l1Endpoint, L1: l1Endpoint,
L2: l2Endpoint, L2: l2Endpoint,
L2Sync: l2SyncEndpoint,
Rollup: *rollupConfig, Rollup: *rollupConfig,
Driver: *driverConfig, Driver: *driverConfig,
RPC: node.RPCConfig{ RPC: node.RPCConfig{
...@@ -96,12 +93,12 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { ...@@ -96,12 +93,12 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) {
return cfg, nil return cfg, nil
} }
func NewL1EndpointConfig(ctx *cli.Context) (*node.L1EndpointConfig, error) { func NewL1EndpointConfig(ctx *cli.Context) *node.L1EndpointConfig {
return &node.L1EndpointConfig{ return &node.L1EndpointConfig{
L1NodeAddr: ctx.GlobalString(flags.L1NodeAddr.Name), L1NodeAddr: ctx.GlobalString(flags.L1NodeAddr.Name),
L1TrustRPC: ctx.GlobalBool(flags.L1TrustRPC.Name), L1TrustRPC: ctx.GlobalBool(flags.L1TrustRPC.Name),
L1RPCKind: sources.RPCProviderKind(strings.ToLower(ctx.GlobalString(flags.L1RPCProviderKind.Name))), L1RPCKind: sources.RPCProviderKind(strings.ToLower(ctx.GlobalString(flags.L1RPCProviderKind.Name))),
}, nil }
} }
func NewL2EndpointConfig(ctx *cli.Context, log log.Logger) (*node.L2EndpointConfig, error) { func NewL2EndpointConfig(ctx *cli.Context, log log.Logger) (*node.L2EndpointConfig, error) {
...@@ -134,13 +131,21 @@ func NewL2EndpointConfig(ctx *cli.Context, log log.Logger) (*node.L2EndpointConf ...@@ -134,13 +131,21 @@ func NewL2EndpointConfig(ctx *cli.Context, log log.Logger) (*node.L2EndpointConf
}, nil }, nil
} }
func NewDriverConfig(ctx *cli.Context) (*driver.Config, error) { // NewL2SyncEndpointConfig returns a pointer to a L2SyncEndpointConfig if the
// flag is set, otherwise nil.
func NewL2SyncEndpointConfig(ctx *cli.Context) *node.L2SyncEndpointConfig {
return &node.L2SyncEndpointConfig{
L2NodeAddr: ctx.GlobalString(flags.BackupL2UnsafeSyncRPC.Name),
}
}
func NewDriverConfig(ctx *cli.Context) *driver.Config {
return &driver.Config{ return &driver.Config{
VerifierConfDepth: ctx.GlobalUint64(flags.VerifierL1Confs.Name), VerifierConfDepth: ctx.GlobalUint64(flags.VerifierL1Confs.Name),
SequencerConfDepth: ctx.GlobalUint64(flags.SequencerL1Confs.Name), SequencerConfDepth: ctx.GlobalUint64(flags.SequencerL1Confs.Name),
SequencerEnabled: ctx.GlobalBool(flags.SequencerEnabledFlag.Name), SequencerEnabled: ctx.GlobalBool(flags.SequencerEnabledFlag.Name),
SequencerStopped: ctx.GlobalBool(flags.SequencerStoppedFlag.Name), SequencerStopped: ctx.GlobalBool(flags.SequencerStoppedFlag.Name),
}, nil }
} }
func NewRollupConfig(ctx *cli.Context) (*rollup.Config, error) { func NewRollupConfig(ctx *cli.Context) (*rollup.Config, error) {
......
package sources
import (
"context"
"errors"
"sync"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/sources/caching"
"github.com/ethereum/go-ethereum/log"
"github.com/libp2p/go-libp2p/core/peer"
)
var ErrNoUnsafeL2PayloadChannel = errors.New("unsafeL2Payloads channel must not be nil")
// RpcSyncPeer is a mock PeerID for the RPC sync client.
var RpcSyncPeer peer.ID = "ALT_RPC_SYNC"
type receivePayload = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error
type SyncClientInterface interface {
Start() error
Close() error
fetchUnsafeBlockFromRpc(ctx context.Context, blockNumber uint64)
}
type SyncClient struct {
*L2Client
FetchUnsafeBlock chan uint64
done chan struct{}
receivePayload receivePayload
wg sync.WaitGroup
}
var _ SyncClientInterface = (*SyncClient)(nil)
type SyncClientConfig struct {
L2ClientConfig
}
func SyncClientDefaultConfig(config *rollup.Config, trustRPC bool) *SyncClientConfig {
return &SyncClientConfig{
*L2ClientDefaultConfig(config, trustRPC),
}
}
func NewSyncClient(receiver receivePayload, client client.RPC, log log.Logger, metrics caching.Metrics, config *SyncClientConfig) (*SyncClient, error) {
l2Client, err := NewL2Client(client, log, metrics, &config.L2ClientConfig)
if err != nil {
return nil, err
}
return &SyncClient{
L2Client: l2Client,
FetchUnsafeBlock: make(chan uint64, 128),
done: make(chan struct{}),
receivePayload: receiver,
}, nil
}
// Start starts up the state loop.
// The loop will have been started if err is not nil.
func (s *SyncClient) Start() error {
s.wg.Add(1)
go s.eventLoop()
return nil
}
// Close sends a signal to the event loop to stop.
func (s *SyncClient) Close() error {
s.done <- struct{}{}
s.wg.Wait()
return nil
}
// eventLoop is the main event loop for the sync client.
func (s *SyncClient) eventLoop() {
defer s.wg.Done()
s.log.Info("Starting sync client event loop")
for {
select {
case <-s.done:
return
case blockNumber := <-s.FetchUnsafeBlock:
s.fetchUnsafeBlockFromRpc(context.Background(), blockNumber)
}
}
}
// fetchUnsafeBlockFromRpc attempts to fetch an unsafe execution payload from the backup unsafe sync RPC.
// WARNING: This function fails silently (aside from warning logs).
//
// Post Shanghai hardfork, the engine API's `PayloadBodiesByRange` method will be much more efficient, but for now,
// the `eth_getBlockByNumber` method is more widely available.
func (s *SyncClient) fetchUnsafeBlockFromRpc(ctx context.Context, blockNumber uint64) {
s.log.Info("Requesting unsafe payload from backup RPC", "block number", blockNumber)
payload, err := s.PayloadByNumber(ctx, blockNumber)
if err != nil {
s.log.Warn("Failed to convert block to execution payload", "block number", blockNumber, "err", err)
return
}
// Signature validation is not necessary here since the backup RPC is trusted.
if _, ok := payload.CheckBlockHash(); !ok {
s.log.Warn("Received invalid payload from backup RPC; invalid block hash", "payload", payload.ID())
return
}
s.log.Info("Received unsafe payload from backup RPC", "payload", payload.ID())
// Send the retrieved payload to the `unsafeL2Payloads` channel.
if err = s.receivePayload(ctx, RpcSyncPeer, payload); err != nil {
s.log.Warn("Failed to send payload into the driver's unsafeL2Payloads channel", "payload", payload.ID(), "err", err)
return
} else {
s.log.Info("Sent received payload into the driver's unsafeL2Payloads channel", "payload", payload.ID())
}
}
...@@ -47,6 +47,9 @@ type HeaderInfo struct { ...@@ -47,6 +47,9 @@ type HeaderInfo struct {
txHash common.Hash txHash common.Hash
receiptHash common.Hash receiptHash common.Hash
gasUsed uint64 gasUsed uint64
// withdrawalsRoot was added in Shapella and is thus optional
withdrawalsRoot *common.Hash
} }
var _ eth.BlockInfo = (*HeaderInfo)(nil) var _ eth.BlockInfo = (*HeaderInfo)(nil)
...@@ -113,7 +116,10 @@ type rpcHeader struct { ...@@ -113,7 +116,10 @@ type rpcHeader struct {
Nonce types.BlockNonce `json:"nonce"` Nonce types.BlockNonce `json:"nonce"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers. // BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` BaseFee *hexutil.Big `json:"baseFeePerGas"`
// WithdrawalsRoot was added by EIP-4895 and is ignored in legacy headers.
WithdrawalsRoot *common.Hash `json:"withdrawalsRoot"`
// untrusted info included by RPC, may have to be checked // untrusted info included by RPC, may have to be checked
Hash common.Hash `json:"hash"` Hash common.Hash `json:"hash"`
...@@ -160,6 +166,7 @@ func (hdr *rpcHeader) computeBlockHash() common.Hash { ...@@ -160,6 +166,7 @@ func (hdr *rpcHeader) computeBlockHash() common.Hash {
MixDigest: hdr.MixDigest, MixDigest: hdr.MixDigest,
Nonce: hdr.Nonce, Nonce: hdr.Nonce,
BaseFee: (*big.Int)(hdr.BaseFee), BaseFee: (*big.Int)(hdr.BaseFee),
WithdrawalsHash: hdr.WithdrawalsRoot,
} }
return gethHeader.Hash() return gethHeader.Hash()
} }
...@@ -188,6 +195,7 @@ func (hdr *rpcHeader) Info(trustCache bool, mustBePostMerge bool) (*HeaderInfo, ...@@ -188,6 +195,7 @@ func (hdr *rpcHeader) Info(trustCache bool, mustBePostMerge bool) (*HeaderInfo,
txHash: hdr.TxHash, txHash: hdr.TxHash,
receiptHash: hdr.ReceiptHash, receiptHash: hdr.ReceiptHash,
gasUsed: uint64(hdr.GasUsed), gasUsed: uint64(hdr.GasUsed),
withdrawalsRoot: hdr.WithdrawalsRoot,
} }
return &info, nil return &info, nil
} }
......
package sources
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestBlockJSON(t *testing.T) {
testCases := []struct {
Name string
OK bool
Data string
}{
{Name: "pre-Shanghai good tx", OK: true, Data: `{"number":"0x840249","hash":"0x9ef7cd2241202b919a0e51240818a8666c73f7ce4b908931e3ae6d26d30f7663","transactions":["0x39c666d9b5cec429accad7b0f94f789ca2ebeb5294b8b129c1b76f552daf57d3","0x2ca7289ab3738d17e0f5093bd96c97c06c9a2ea4c22fc84a6a7fbfda93ce55ee","0xb0085de1476530de3efc6928c4683e7c40f8fac18875f74cbcc47df159de17d9","0xe01c8631c86ded63af95b8dbc0c8aac5d31254c14d6ecb4cc51d98259d838e52","0x69414a126a6f07ab5e31ad2f9069fb986b7c490e096898473873e41ece6af783","0xa2fef1133ee726533c7f190f246fede123e3706a03933c1febc92618f90d2804","0x6585ec5c4c2bbf1f683f90f58e18f3b38d875e94457fe4cbb7bc5bf6581f83af","0x1db276b864fbf01dcf8cededf8d597553ecb0eb9438edfaf2f5bd0cc93297c66","0xcbe7ed31654af4e191ca53445b82de040ae2cd92459a3f951bdcce423d780f08","0x808ba5211f03cc78a732ff0f9383c6355e63c83ae8c6035ced2ba6f7c331dc63","0xdd66f1f26672849ef54c420210f479c9f0c46924d8e9f7b210981ffe8d3fac82","0x254abb2f8cdcffe9ef62ab924312a1e4142578db87e4f7c199fd35991e92f014","0xa7b7c654e7073b8043b680b7ffc95d3f2099abaa0b0578d6f954a2a7c99404e1","0x7ccdfa698c8acf47ab9316ed078eb40819ff575bcf612c6f59f29e7726df3f96","0xa0b035ef315824a6f6a6565fa8de27042ade3af9cf0583a36dea83d6e01bf2a8","0x1ebad7f3e8cb3543d4963686a94d99f61839f666831eab9c9c1b4711de11d3d9","0x501750278e91d8b5be1ccf60e793d4bbcd9b3bb3ccc518d3634a71caeac65f48","0xd80ff8af29ae163d5811ba511e60b3a87a279f677bb3872a0f1aa6d0a226e880","0x096acab3b3fe47b149d375782d1eb00b9fef7904076d60c54b3c197b04e6bf82","0xbe9d1738af74a22400591a9a808fb01a25ab41e2e56f202dd7251eb113e8ceeb","0x0834c720e55cccd97aaf4f8fb0cb66afb9881fb6a762c0f70473ec53f98a712e","0x51a0c33c9b37245b416575bdd2751c0d8a5d8bead49585ac427bfc873d4016af","0x531c25d51ccda59aa9ea82e85c99be9dd4e285af9b8973cbab9ac4a38e26e55a","0x93ac6c08d21cb1b61ff59e5e2d6fa3f9ad54008b0a66c669199050bef219f6e3","0x3792db6dd6285f409e4281951e9f78dad16c4a78072ff1c909dfadea5658d857","0xd2d51764c01e8c0a43fbe362704388df5bacf7e5e620c3864e242530ffb3e828","0x516b0227d9e64eb6e0de6862764d40f5376b5f12fec878436fea3479b4c36bb8","0x81b0abc78b82840adb666775b182a9e292f663b64bcd35004c04436ed3c8281c","0xd0287570d431d2baea96ecc81cb890e7f4f06ab5df02f9b4067768abca19acb5","0x76ddab2674369f34946c5fa2f05e2aa8566d86235b83e808e9b27bc106e04ac7","0x34a5c74011a2c8a00103bc91bfbfd94aa99cd569be69066e4bf64d188fe8714e","0x7b9730ead1b9f59b206d0ddea87be9383ba3fc7b496c7863b0cb847889b86617","0x77166ee0409ba86bd26e7c03ad1a927abaf5af8a8a37149e725cd37512091dd6","0x3c2b6c2ae505c5c36d5f316c1fcb5f54f7346ed35ae35c93462991ded7968a68","0xf99a792837e13827b5e0a8915fb59c760babc95d242feca99a5594e64ff6b6e2","0x522313f5d923f048ae5bd0b5595c1f4fc883bc0b3cf3cb0939d3fcf8b08c829c","0x471ceb0e85af594aa56deca54cb8198567b2afd8406722ea530077aaa6b641b3","0x3e9dca502e9039ae0c6d642f62e9562ff00010c6bfbb8234a6135712ba70dfda","0xc95cac67267f4accb9b5950316ac64772f7d082bed6b712c09cf2da0bdc237b7","0xfca28fdbd13fc16daf7aec7d4a2ad2c6b5f0b2a7b0fb1d9167c09b5e115ff26e","0xc73124ca798b2f7a5df2ea4d568efab2f41b135130ea5cc41d4bcb4b5c57d5bd","0x29abb76b5e7a5ce137bf9c22474d386eb58d249f43178d2b2e15c16dfdc5ca80","0x03e5ab25a58bd44fb9dd0c698b323eab8b8363479dfcbcbb16d0a0bd983880ae","0x3c8ee80ddea7fa2d2b75e44563c10c10756f598e8ad252a49c5d3e8a5c8e6cbf","0xaffa73b68bc7ab0c3f5e28377f5ca0a5df33c0a485f64dc094b7f6ae23353203","0xc66c9c66fbc8fe97fcc16506cde7a58689af1004a18c6171cfe763bcd94f50b2","0x80fec96707519172b53790610d5800cd09a4243aca9bacfa956c56337d06f820","0x61b33bfcf11214906dcdce7d7ed83ad82f38184c03ded07f7782059d02eeedea","0x5d4138d4e28a8327e506cb012346b1b38b65f615a2b991d35cf5d4de244b3e6d","0x875a142b6dfcf10ffb71a7afe0ce4672c047fc7e162ba0383390516d6334d45d","0x79b6df832bfbd04085d0b005a6e3ad8f00fc8717eed59280aa8107268b71e7e0","0xcb2fb25d268f65dc9312e89bd3c328c9847a3c9da282026793c54a745f825ab5","0xe483d4a36ad19fd5eacb7f6d9ad3ce080ad70ac673273e710f6e3d5acbc6559c","0x0564242c37d5013b671ef4864394cc0f3924c589f8aad64118223a9af2f164f6","0x48db358e80b278c3a46c2a166339797060a40f33984a5d974992cd9722139d5d","0x69d7758db91fae31fa35ecbed4d40897c5087f45dc796cd796b8ceead21f972e","0x2951478916ecd27a8e808d08f85be4bf2c0b0e0546f21f4e309145dd96eb8df1","0xaca9028cb5d55bbf71b7bff9884a9a3b0b38a575ffc8f8807ce345cf8bd298ef","0xc7f625a19ee41a1750eac9428b4394a9a2476b8ea2d31b4c2f9f5b4fcb86cae3","0x45499074aa521ac4151138f0aad969bcc2dfc1648d22ff8c42e51c74cb77414d","0x00b5b05c6d1a2eb8abe2c383da600516515e383fc8a29953bb6e6d167e9705b2","0x6fc411f24c7b4b8d821b45de32b9edc5ac998d1ac748a98abe8e983c6f39fc19"],"difficulty":"0x0","extraData":"0xd883010b02846765746888676f312e32302e31856c696e7578","gasLimit":"0x1c9c380","gasUsed":"0xa79638","logsBloom":"0xb034000008010014411408c080a0018440087220211154100005a1388807241142a2504080034a00111212a47f05008520200000280202a12800538cc06488486a0141989c7800c0c848011f02249661800e08449145b040a252d18082c009000641004052c80102000804ac10901c24032000980010438a01e50a90a0d8008c138c21204040000b20425000833041028000148124c2012d0aa8d1d0548301808228002015184090000224021040d68220100210220480420308455c382a40020130dc42502986080600000115034c0401c81828490410308005610048026b822e10b4228071ba00bdd20140621b2000c02012300808084181ac308200000011","miner":"0x0000000000000000000000000000000000000000","mixHash":"0x31f0c0305fc07a93b1a33da339c79aadbe8d9811c78d2b514cd18d64e1328f25","nonce":"0x0000000000000000","parentHash":"0x2303b55af4add799b19275a491b150c1a03075395f87a7856a4e3327595ed7df","receiptsRoot":"0x99da71b17ae1929db912c3315ebe349d37f2bb600454616fdde0ee90d6dbc59e","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xea6d","stateRoot":"0xd12bf4cf3941cf48be329a939b13d3403d326841c69cdcc9a9c13ab2f227e904","timestamp":"0x640fdeb0","totalDifficulty":"0xa4a470","transactionsRoot":"0x1ad3212eca045505cfc4cacf675b5fa2e7dc7b9f9cee88191464f97d1c9fbca4","uncles":[],"baseFeePerGas":"0x7ccf990f8"}
`},
{Name: "pre-Shanghai bad receipts root", OK: false, Data: `{"number":"0x840249","hash":"0x9ef7cd2241202b919a0e51240818a8666c73f7ce4b908931e3ae6d26d30f7663","transactions":["0x39c666d9b5cec429accad7b0f94f789ca2ebeb5294b8b129c1b76f552daf57d3","0x2ca7289ab3738d17e0f5093bd96c97c06c9a2ea4c22fc84a6a7fbfda93ce55ee","0xb0085de1476530de3efc6928c4683e7c40f8fac18875f74cbcc47df159de17d9","0xe01c8631c86ded63af95b8dbc0c8aac5d31254c14d6ecb4cc51d98259d838e52","0x69414a126a6f07ab5e31ad2f9069fb986b7c490e096898473873e41ece6af783","0xa2fef1133ee726533c7f190f246fede123e3706a03933c1febc92618f90d2804","0x6585ec5c4c2bbf1f683f90f58e18f3b38d875e94457fe4cbb7bc5bf6581f83af","0x1db276b864fbf01dcf8cededf8d597553ecb0eb9438edfaf2f5bd0cc93297c66","0xcbe7ed31654af4e191ca53445b82de040ae2cd92459a3f951bdcce423d780f08","0x808ba5211f03cc78a732ff0f9383c6355e63c83ae8c6035ced2ba6f7c331dc63","0xdd66f1f26672849ef54c420210f479c9f0c46924d8e9f7b210981ffe8d3fac82","0x254abb2f8cdcffe9ef62ab924312a1e4142578db87e4f7c199fd35991e92f014","0xa7b7c654e7073b8043b680b7ffc95d3f2099abaa0b0578d6f954a2a7c99404e1","0x7ccdfa698c8acf47ab9316ed078eb40819ff575bcf612c6f59f29e7726df3f96","0xa0b035ef315824a6f6a6565fa8de27042ade3af9cf0583a36dea83d6e01bf2a8","0x1ebad7f3e8cb3543d4963686a94d99f61839f666831eab9c9c1b4711de11d3d9","0x501750278e91d8b5be1ccf60e793d4bbcd9b3bb3ccc518d3634a71caeac65f48","0xd80ff8af29ae163d5811ba511e60b3a87a279f677bb3872a0f1aa6d0a226e880","0x096acab3b3fe47b149d375782d1eb00b9fef7904076d60c54b3c197b04e6bf82","0xbe9d1738af74a22400591a9a808fb01a25ab41e2e56f202dd7251eb113e8ceeb","0x0834c720e55cccd97aaf4f8fb0cb66afb9881fb6a762c0f70473ec53f98a712e","0x51a0c33c9b37245b416575bdd2751c0d8a5d8bead49585ac427bfc873d4016af","0x531c25d51ccda59aa9ea82e85c99be9dd4e285af9b8973cbab9ac4a38e26e55a","0x93ac6c08d21cb1b61ff59e5e2d6fa3f9ad54008b0a66c669199050bef219f6e3","0x3792db6dd6285f409e4281951e9f78dad16c4a78072ff1c909dfadea5658d857","0xd2d51764c01e8c0a43fbe362704388df5bacf7e5e620c3864e242530ffb3e828","0x516b0227d9e64eb6e0de6862764d40f5376b5f12fec878436fea3479b4c36bb8","0x81b0abc78b82840adb666775b182a9e292f663b64bcd35004c04436ed3c8281c","0xd0287570d431d2baea96ecc81cb890e7f4f06ab5df02f9b4067768abca19acb5","0x76ddab2674369f34946c5fa2f05e2aa8566d86235b83e808e9b27bc106e04ac7","0x34a5c74011a2c8a00103bc91bfbfd94aa99cd569be69066e4bf64d188fe8714e","0x7b9730ead1b9f59b206d0ddea87be9383ba3fc7b496c7863b0cb847889b86617","0x77166ee0409ba86bd26e7c03ad1a927abaf5af8a8a37149e725cd37512091dd6","0x3c2b6c2ae505c5c36d5f316c1fcb5f54f7346ed35ae35c93462991ded7968a68","0xf99a792837e13827b5e0a8915fb59c760babc95d242feca99a5594e64ff6b6e2","0x522313f5d923f048ae5bd0b5595c1f4fc883bc0b3cf3cb0939d3fcf8b08c829c","0x471ceb0e85af594aa56deca54cb8198567b2afd8406722ea530077aaa6b641b3","0x3e9dca502e9039ae0c6d642f62e9562ff00010c6bfbb8234a6135712ba70dfda","0xc95cac67267f4accb9b5950316ac64772f7d082bed6b712c09cf2da0bdc237b7","0xfca28fdbd13fc16daf7aec7d4a2ad2c6b5f0b2a7b0fb1d9167c09b5e115ff26e","0xc73124ca798b2f7a5df2ea4d568efab2f41b135130ea5cc41d4bcb4b5c57d5bd","0x29abb76b5e7a5ce137bf9c22474d386eb58d249f43178d2b2e15c16dfdc5ca80","0x03e5ab25a58bd44fb9dd0c698b323eab8b8363479dfcbcbb16d0a0bd983880ae","0x3c8ee80ddea7fa2d2b75e44563c10c10756f598e8ad252a49c5d3e8a5c8e6cbf","0xaffa73b68bc7ab0c3f5e28377f5ca0a5df33c0a485f64dc094b7f6ae23353203","0xc66c9c66fbc8fe97fcc16506cde7a58689af1004a18c6171cfe763bcd94f50b2","0x80fec96707519172b53790610d5800cd09a4243aca9bacfa956c56337d06f820","0x61b33bfcf11214906dcdce7d7ed83ad82f38184c03ded07f7782059d02eeedea","0x5d4138d4e28a8327e506cb012346b1b38b65f615a2b991d35cf5d4de244b3e6d","0x875a142b6dfcf10ffb71a7afe0ce4672c047fc7e162ba0383390516d6334d45d","0x79b6df832bfbd04085d0b005a6e3ad8f00fc8717eed59280aa8107268b71e7e0","0xcb2fb25d268f65dc9312e89bd3c328c9847a3c9da282026793c54a745f825ab5","0xe483d4a36ad19fd5eacb7f6d9ad3ce080ad70ac673273e710f6e3d5acbc6559c","0x0564242c37d5013b671ef4864394cc0f3924c589f8aad64118223a9af2f164f6","0x48db358e80b278c3a46c2a166339797060a40f33984a5d974992cd9722139d5d","0x69d7758db91fae31fa35ecbed4d40897c5087f45dc796cd796b8ceead21f972e","0x2951478916ecd27a8e808d08f85be4bf2c0b0e0546f21f4e309145dd96eb8df1","0xaca9028cb5d55bbf71b7bff9884a9a3b0b38a575ffc8f8807ce345cf8bd298ef","0xc7f625a19ee41a1750eac9428b4394a9a2476b8ea2d31b4c2f9f5b4fcb86cae3","0x45499074aa521ac4151138f0aad969bcc2dfc1648d22ff8c42e51c74cb77414d","0x00b5b05c6d1a2eb8abe2c383da600516515e383fc8a29953bb6e6d167e9705b2","0x6fc411f24c7b4b8d821b45de32b9edc5ac998d1ac748a98abe8e983c6f39fc19"],"difficulty":"0x0","extraData":"0xd883010b02846765746888676f312e32302e31856c696e7578","gasLimit":"0x1c9c380","gasUsed":"0xa79638","logsBloom":"0xb034000008010014411408c080a0018440087220211154100005a1388807241142a2504080034a00111212a47f05008520200000280202a12800538cc06488486a0141989c7800c0c848011f02249661800e08449145b040a252d18082c009000641004052c80102000804ac10901c24032000980010438a01e50a90a0d8008c138c21204040000b20425000833041028000148124c2012d0aa8d1d0548301808228002015184090000224021040d68220100210220480420308455c382a40020130dc42502986080600000115034c0401c81828490410308005610048026b822e10b4228071ba00bdd20140621b2000c02012300808084181ac308200000011","miner":"0x0000000000000000000000000000000000000000","mixHash":"0x31f0c0305fc07a93b1a33da339c79aadbe8d9811c78d2b514cd18d64e1328f25","nonce":"0x0000000000000000","parentHash":"0x2303b55af4add799b19275a491b150c1a03075395f87a7856a4e3327595ed7df","receiptsRoot":"0x99da71b17ae1929db912c3315ebe349d37f2bb600454616fdde0ee90d6dbc59f","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xea6d","stateRoot":"0xd12bf4cf3941cf48be329a939b13d3403d326841c69cdcc9a9c13ab2f227e904","timestamp":"0x640fdeb0","totalDifficulty":"0xa4a470","transactionsRoot":"0x1ad3212eca045505cfc4cacf675b5fa2e7dc7b9f9cee88191464f97d1c9fbca4","uncles":[],"baseFeePerGas":"0x7ccf990f8"}
`},
{Name: "Shanghai good tx", OK: true, Data: `{"baseFeePerGas":"0x3fb7c357","difficulty":"0x0","extraData":"0x","gasLimit":"0x1c9c380","gasUsed":"0x18f759","hash":"0xa16c6bcda4fdca88b5761965c4d724f7afc6a6900d9051a204e544870adb3452","logsBloom":"0x020010404000001a0000021000000080001100410000100001000010040200980220400000008806200200000100000000000000000000008000000400042000000050000040000112080808800002044000040004042008800480002000000000000002020020000042002400000820000080040000000010200010020010100101212050000008000000008000001010200c80000112010000438040020400000000202400000000002002a0210402000622010000000001700144000040000000002204000000c000410105024010000808000000002004002000000261000000822200200800881000000012500400400000000000000040010000800000","miner":"0x000095e79eac4d76aab57cb2c1f091d553b36ca0","mixHash":"0x5b53dc49cbab268ef9950b1d81b5e36a1b2f1b97aee1b7ff6e4db0e06c29a8b0","nonce":"0x0000000000000000","number":"0x84161e","parentHash":"0x72d92c1498e05952988d4e79a695928a6bcbd37239f8a1734051263b4d3504b8","receiptsRoot":"0xaff90ae18dcc35924a4bddb68d403b8b7812c10c3ea2a114f34105c87d75bcdb","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x2a51","stateRoot":"0xc56738518b2c7854a640ae25996d2211c9ef0dd2e4dd9e59e9d9cacef39622da","timestamp":"0x64110a5c","totalDifficulty":"0xa4a470","transactions":["0x1e8f148a9aea7d8d16ea6e9446723b8f262e8bcd89c7c961d52046ebd43b4598","0xab5c870f4c367012bd763172afbfbe68fbf35336a66ae41aff3f2c9dbf4ea3f8","0xa81fd92b2d0f0bbd3cc355f869cca3243c98c5e2641db9ecf3eeabb3b13bff6a","0xa92c7b720c08c83f1a0ed7e4c163200e30a3a8c03fcc5a51e685ea20cd0cb577","0x6921b429ad2ec1e97d3457049ad2e893b5a0349beba47ca1c74a9540af75347a","0xf776b2da0b835dde05d0d8b76fd19385d61e7055036cf637f804b36dc94f2384","0x9a08d899cd14ebb930ed59fa774afdb88a22615b3a931e930931ea54d26dc0bc","0x0fe0d97e25d5eb11a33a3e8278584c3780941fc2675bdf8fc547cee3d1fd3b17","0xef47a60f57f177a683c723c658137efab66d311e1c5abbc4d74f653535144d03","0xe23a5b35faae5335adc5aca38c5d633b00438b798c2053104b8df48406c9b141","0xd8cea4ba619b317bc05d58534af73beec6c2548b31b24d4dc61c9bbd29cfa17a","0x79a4b9d90b02c768baaad305f266281213cc75062cbe99a13222cc0c4b509498","0x6790a3bbddbeb21fcb736a59b3775755051c3a6344d8390cf8ca27f2e8a814f0","0x87ec7ace5442db252b5751ffddd38dcb04b088d36b6b0e526ff25607a4293c81","0x40cb487ecffda94f97ce7fc0f7163f2f024235df2c8291169edc80dac063e6d0","0xb76bb3d88c9b30d927c45ccfcf8d5b0054411ac8501ad588822a7d04690cccf6","0x798ebe823209869347c08bd81e04fbf60e9bdfe44b1cc923215182d0cf3d4edb","0xbe68a7e02725f799a65ebb069ccc83a014ac7c40e4119bf7c220a2f6ddfee295","0xc90c3a72efe81331727fcce4b5bd4906066da314ca9a0b44023a6b09ea7e8114","0x619a6cbd43cde074d314c19623bd66d9fb1e13c158d7138775236f798dc1245e","0xca5a56cd77b9e5b0e79020cc6346edf205bc11e901984d805125f28c2e6686e6","0x999c9ddeed67c6ef6fbf02a6e977a6c1b68e18d24814e51643c7157b87a43e0a","0x47c8f5d0b3778e4c34eba7fcc356fa04a5afd954ccf484728e72c002764dd3c4","0x396797ae0ebcdb72ff1f96fd08b6128f78acc7417353f142f1a5facd425a33e6","0x454aa43d6546a6f62246826c16b7a49c6c704238c18802ef0d659922f23a573c","0x317ecb5bd19caa42a69f836d41556ebb0e0e00e1c6cd2dee230e6e6192612527","0xc879285db5ef0a6bce98021584d16f134c1dc0aed8cc988802c4f72ba6877ff6","0xecaa2d6f597608307e5084854854ba6dc1e69395e2abea14f2c6a2fa1d6faf9a","0x4dd69b69a568ff30ae439e2ded72fbd7f2e7aaa345836703663f155c749c5eed"],"transactionsRoot":"0x4a87d0cf5990b1c5bac631583e5965c2ba943858bebb2e07f74d0b697f73821a","uncles":[],"withdrawals":[{"index":"0x1170","validatorIndex":"0x38c2c","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x66edfd65"},{"index":"0x1171","validatorIndex":"0x38c2d","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6cd228e4"},{"index":"0x1172","validatorIndex":"0x38c2e","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x77f3431b"},{"index":"0x1173","validatorIndex":"0x38c2f","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6b61f268"},{"index":"0x1174","validatorIndex":"0x38c30","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6e10bb21"},{"index":"0x1175","validatorIndex":"0x38c31","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6eb115a5"},{"index":"0x1176","validatorIndex":"0x38c32","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x7caead1d"},{"index":"0x1177","validatorIndex":"0x38c33","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x772c0ddf"},{"index":"0x1178","validatorIndex":"0x38c34","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x75930a95"},{"index":"0x1179","validatorIndex":"0x38c35","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x76a4db09"},{"index":"0x117a","validatorIndex":"0x38c36","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x7e692b27"},{"index":"0x117b","validatorIndex":"0x38c37","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x72038ae6"},{"index":"0x117c","validatorIndex":"0x38c38","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6ccce352"},{"index":"0x117d","validatorIndex":"0x38c39","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x79ef6898"},{"index":"0x117e","validatorIndex":"0x38c3a","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6d58977d"},{"index":"0x117f","validatorIndex":"0x38c3b","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x76f7d208"}],"withdrawalsRoot":"0xbe712c930a0665264b025ced87cc7839eef95a3cbc26dadc93e9e185a350ad28"}
`},
{Name: "Shanghai bad withdrawals root", OK: false, Data: `{"baseFeePerGas":"0x3fb7c357","difficulty":"0x0","extraData":"0x","gasLimit":"0x1c9c380","gasUsed":"0x18f759","hash":"0xa16c6bcda4fdca88b5761965c4d724f7afc6a6900d9051a204e544870adb3452","logsBloom":"0x020010404000001a0000021000000080001100410000100001000010040200980220400000008806200200000100000000000000000000008000000400042000000050000040000112080808800002044000040004042008800480002000000000000002020020000042002400000820000080040000000010200010020010100101212050000008000000008000001010200c80000112010000438040020400000000202400000000002002a0210402000622010000000001700144000040000000002204000000c000410105024010000808000000002004002000000261000000822200200800881000000012500400400000000000000040010000800000","miner":"0x000095e79eac4d76aab57cb2c1f091d553b36ca0","mixHash":"0x5b53dc49cbab268ef9950b1d81b5e36a1b2f1b97aee1b7ff6e4db0e06c29a8b0","nonce":"0x0000000000000000","number":"0x84161e","parentHash":"0x72d92c1498e05952988d4e79a695928a6bcbd37239f8a1734051263b4d3504b8","receiptsRoot":"0xaff90ae18dcc35924a4bddb68d403b8b7812c10c3ea2a114f34105c87d75bcdb","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x2a51","stateRoot":"0xc56738518b2c7854a640ae25996d2211c9ef0dd2e4dd9e59e9d9cacef39622da","timestamp":"0x64110a5c","totalDifficulty":"0xa4a470","transactions":["0x1e8f148a9aea7d8d16ea6e9446723b8f262e8bcd89c7c961d52046ebd43b4598","0xab5c870f4c367012bd763172afbfbe68fbf35336a66ae41aff3f2c9dbf4ea3f8","0xa81fd92b2d0f0bbd3cc355f869cca3243c98c5e2641db9ecf3eeabb3b13bff6a","0xa92c7b720c08c83f1a0ed7e4c163200e30a3a8c03fcc5a51e685ea20cd0cb577","0x6921b429ad2ec1e97d3457049ad2e893b5a0349beba47ca1c74a9540af75347a","0xf776b2da0b835dde05d0d8b76fd19385d61e7055036cf637f804b36dc94f2384","0x9a08d899cd14ebb930ed59fa774afdb88a22615b3a931e930931ea54d26dc0bc","0x0fe0d97e25d5eb11a33a3e8278584c3780941fc2675bdf8fc547cee3d1fd3b17","0xef47a60f57f177a683c723c658137efab66d311e1c5abbc4d74f653535144d03","0xe23a5b35faae5335adc5aca38c5d633b00438b798c2053104b8df48406c9b141","0xd8cea4ba619b317bc05d58534af73beec6c2548b31b24d4dc61c9bbd29cfa17a","0x79a4b9d90b02c768baaad305f266281213cc75062cbe99a13222cc0c4b509498","0x6790a3bbddbeb21fcb736a59b3775755051c3a6344d8390cf8ca27f2e8a814f0","0x87ec7ace5442db252b5751ffddd38dcb04b088d36b6b0e526ff25607a4293c81","0x40cb487ecffda94f97ce7fc0f7163f2f024235df2c8291169edc80dac063e6d0","0xb76bb3d88c9b30d927c45ccfcf8d5b0054411ac8501ad588822a7d04690cccf6","0x798ebe823209869347c08bd81e04fbf60e9bdfe44b1cc923215182d0cf3d4edb","0xbe68a7e02725f799a65ebb069ccc83a014ac7c40e4119bf7c220a2f6ddfee295","0xc90c3a72efe81331727fcce4b5bd4906066da314ca9a0b44023a6b09ea7e8114","0x619a6cbd43cde074d314c19623bd66d9fb1e13c158d7138775236f798dc1245e","0xca5a56cd77b9e5b0e79020cc6346edf205bc11e901984d805125f28c2e6686e6","0x999c9ddeed67c6ef6fbf02a6e977a6c1b68e18d24814e51643c7157b87a43e0a","0x47c8f5d0b3778e4c34eba7fcc356fa04a5afd954ccf484728e72c002764dd3c4","0x396797ae0ebcdb72ff1f96fd08b6128f78acc7417353f142f1a5facd425a33e6","0x454aa43d6546a6f62246826c16b7a49c6c704238c18802ef0d659922f23a573c","0x317ecb5bd19caa42a69f836d41556ebb0e0e00e1c6cd2dee230e6e6192612527","0xc879285db5ef0a6bce98021584d16f134c1dc0aed8cc988802c4f72ba6877ff6","0xecaa2d6f597608307e5084854854ba6dc1e69395e2abea14f2c6a2fa1d6faf9a","0x4dd69b69a568ff30ae439e2ded72fbd7f2e7aaa345836703663f155c749c5eed"],"transactionsRoot":"0x4a87d0cf5990b1c5bac631583e5965c2ba943858bebb2e07f74d0b697f73821a","uncles":[],"withdrawals":[{"index":"0x1170","validatorIndex":"0x38c2c","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x66edfd65"},{"index":"0x1171","validatorIndex":"0x38c2d","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6cd228e4"},{"index":"0x1172","validatorIndex":"0x38c2e","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x77f3431b"},{"index":"0x1173","validatorIndex":"0x38c2f","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6b61f268"},{"index":"0x1174","validatorIndex":"0x38c30","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6e10bb21"},{"index":"0x1175","validatorIndex":"0x38c31","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6eb115a5"},{"index":"0x1176","validatorIndex":"0x38c32","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x7caead1d"},{"index":"0x1177","validatorIndex":"0x38c33","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x772c0ddf"},{"index":"0x1178","validatorIndex":"0x38c34","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x75930a95"},{"index":"0x1179","validatorIndex":"0x38c35","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x76a4db09"},{"index":"0x117a","validatorIndex":"0x38c36","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x7e692b27"},{"index":"0x117b","validatorIndex":"0x38c37","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x72038ae6"},{"index":"0x117c","validatorIndex":"0x38c38","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6ccce352"},{"index":"0x117d","validatorIndex":"0x38c39","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x79ef6898"},{"index":"0x117e","validatorIndex":"0x38c3a","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x6d58977d"},{"index":"0x117f","validatorIndex":"0x38c3b","address":"0x8f0844fd51e31ff6bf5babe21dccf7328e19fd9f","amount":"0x76f7d208"}],"withdrawalsRoot":"0xbe712c930a0665264b025ced87cc7839eef95a3cbc26dadc93e9e185a350ad27"}
`},
}
for _, testCase := range testCases {
var x rpcHeader
require.NoError(t, json.Unmarshal([]byte(testCase.Data), &x))
h := x.computeBlockHash()
if testCase.OK {
require.Equal(t, h, x.Hash, "blockhash should verify ok")
} else {
require.NotEqual(t, h, x.Hash, "expecting verification error")
}
}
}
...@@ -206,13 +206,21 @@ func RandomHeader(rng *rand.Rand) *types.Header { ...@@ -206,13 +206,21 @@ func RandomHeader(rng *rand.Rand) *types.Header {
} }
func RandomBlock(rng *rand.Rand, txCount uint64) (*types.Block, []*types.Receipt) { func RandomBlock(rng *rand.Rand, txCount uint64) (*types.Block, []*types.Receipt) {
return RandomBlockPrependTxs(rng, int(txCount))
}
// RandomBlockPrependTxs returns a random block with txCount randomly generated
// transactions and additionally the transactions ptxs prepended. So the total
// number of transactions is len(ptxs) + txCount.
func RandomBlockPrependTxs(rng *rand.Rand, txCount int, ptxs ...*types.Transaction) (*types.Block, []*types.Receipt) {
header := RandomHeader(rng) header := RandomHeader(rng)
signer := types.NewLondonSigner(big.NewInt(rng.Int63n(1000))) signer := types.NewLondonSigner(big.NewInt(rng.Int63n(1000)))
txs := make([]*types.Transaction, 0, txCount) txs := make([]*types.Transaction, 0, txCount+len(ptxs))
for i := uint64(0); i < txCount; i++ { txs = append(txs, ptxs...)
for i := 0; i < txCount; i++ {
txs = append(txs, RandomTx(rng, header.BaseFee, signer)) txs = append(txs, RandomTx(rng, header.BaseFee, signer))
} }
receipts := make([]*types.Receipt, 0, txCount) receipts := make([]*types.Receipt, 0, len(txs))
cumulativeGasUsed := uint64(0) cumulativeGasUsed := uint64(0)
for i, tx := range txs { for i, tx := range txs {
r := RandomReceipt(rng, signer, tx, uint64(i), cumulativeGasUsed) r := RandomReceipt(rng, signer, tx, uint64(i), cumulativeGasUsed)
......
...@@ -211,8 +211,15 @@ export abstract class BaseServiceV2< ...@@ -211,8 +211,15 @@ export abstract class BaseServiceV2<
// Since BCFG turns everything into lower case, we're required to turn all of the input option // Since BCFG turns everything into lower case, we're required to turn all of the input option
// names into lower case for the validation step. We'll turn the names back into their original // names into lower case for the validation step. We'll turn the names back into their original
// names when we're done. // names when we're done.
const lowerCaseOptions = Object.entries(params.options).reduce(
(acc, [key, val]) => {
acc[key.toLowerCase()] = val
return acc
},
{}
)
const cleaned = cleanEnv<TOptions>( const cleaned = cleanEnv<TOptions>(
{ ...config.env, ...config.args, ...(params.options || {}) }, { ...config.env, ...config.args, ...(lowerCaseOptions || {}) },
Object.entries(params.optionsSpec || {}).reduce((acc, [key, val]) => { Object.entries(params.optionsSpec || {}).reduce((acc, [key, val]) => {
acc[key.toLowerCase()] = val.validator({ acc[key.toLowerCase()] = val.validator({
desc: val.desc, desc: val.desc,
......
import { validators } from '../dist'
import { BaseServiceV2 } from '../src'
type ServiceOptions = {
camelCase: string
}
class Service extends BaseServiceV2<ServiceOptions, {}, {}> {
constructor(options?: Partial<ServiceOptions>) {
super({
name: 'test-service',
version: '0.0',
options,
optionsSpec: {
camelCase: { validator: validators.str, desc: 'test' },
},
metricsSpec: {},
})
}
protected async main() {
/* eslint-disable @typescript-eslint/no-empty-function */
}
}
describe('BaseServiceV2', () => {
it('base service ctor does not throw on camel case options', async () => {
new Service({ camelCase: 'test' })
})
})
...@@ -7,3 +7,7 @@ PRIVATE_KEY_DEPLOYER= ...@@ -7,3 +7,7 @@ PRIVATE_KEY_DEPLOYER=
# Optional Tenderly details for a simulation link during deployment # Optional Tenderly details for a simulation link during deployment
TENDERLY_PROJECT= TENDERLY_PROJECT=
TENDERLY_USERNAME= TENDERLY_USERNAME=
# Optional boolean to define if cast commands should be printed.
# Useful during migration testing
CAST_COMMANDS=1
...@@ -266,9 +266,9 @@ OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifOutp ...@@ -266,9 +266,9 @@ OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifOutp
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifOutputTimestampIsNotFinalized_reverts() (gas: 207520) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifOutputTimestampIsNotFinalized_reverts() (gas: 207520)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() (gas: 41753) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() (gas: 41753)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifWithdrawalProofNotOldEnough_reverts() (gas: 199464) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_ifWithdrawalProofNotOldEnough_reverts() (gas: 199464)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onInsufficientGas_reverts() (gas: 206360) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onInsufficientGas_reverts() (gas: 205818)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onRecentWithdrawal_reverts() (gas: 180229) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onRecentWithdrawal_reverts() (gas: 180229)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onReentrancy_reverts() (gas: 244377) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onReentrancy_reverts() (gas: 243835)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onReplay_reverts() (gas: 245528) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_onReplay_reverts() (gas: 245528)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_paused_reverts() (gas: 53555) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_paused_reverts() (gas: 53555)
OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_provenWithdrawalHash_succeeds() (gas: 234941) OptimismPortal_FinalizeWithdrawal_Test:test_finalizeWithdrawalTransaction_provenWithdrawalHash_succeeds() (gas: 234941)
......
...@@ -12,3 +12,4 @@ deployments/mainnet-forked ...@@ -12,3 +12,4 @@ deployments/mainnet-forked
deploy-config/mainnet-forked.json deploy-config/mainnet-forked.json
test-case-generator/fuzz test-case-generator/fuzz
.resource-metering.csv .resource-metering.csv
scripts/differential-testing/differential-testing
...@@ -477,16 +477,15 @@ contract FFIInterface is Test { ...@@ -477,16 +477,15 @@ contract FFIInterface is Test {
bytes[] memory bytes[] memory
) )
{ {
string[] memory cmds = new string[](9); string[] memory cmds = new string[](8);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "getProveWithdrawalTransactionInputs";
cmds[2] = "getProveWithdrawalTransactionInputs"; cmds[2] = vm.toString(_tx.nonce);
cmds[3] = vm.toString(_tx.nonce); cmds[3] = vm.toString(_tx.sender);
cmds[4] = vm.toString(_tx.sender); cmds[4] = vm.toString(_tx.target);
cmds[5] = vm.toString(_tx.target); cmds[5] = vm.toString(_tx.value);
cmds[6] = vm.toString(_tx.value); cmds[6] = vm.toString(_tx.gasLimit);
cmds[7] = vm.toString(_tx.gasLimit); cmds[7] = vm.toString(_tx.data);
cmds[8] = vm.toString(_tx.data);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
( (
...@@ -508,16 +507,15 @@ contract FFIInterface is Test { ...@@ -508,16 +507,15 @@ contract FFIInterface is Test {
uint256 _gasLimit, uint256 _gasLimit,
bytes memory _data bytes memory _data
) external returns (bytes32) { ) external returns (bytes32) {
string[] memory cmds = new string[](9); string[] memory cmds = new string[](8);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "hashCrossDomainMessage";
cmds[2] = "hashCrossDomainMessage"; cmds[2] = vm.toString(_nonce);
cmds[3] = vm.toString(_nonce); cmds[3] = vm.toString(_sender);
cmds[4] = vm.toString(_sender); cmds[4] = vm.toString(_target);
cmds[5] = vm.toString(_target); cmds[5] = vm.toString(_value);
cmds[6] = vm.toString(_value); cmds[6] = vm.toString(_gasLimit);
cmds[7] = vm.toString(_gasLimit); cmds[7] = vm.toString(_data);
cmds[8] = vm.toString(_data);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (bytes32)); return abi.decode(result, (bytes32));
...@@ -531,16 +529,15 @@ contract FFIInterface is Test { ...@@ -531,16 +529,15 @@ contract FFIInterface is Test {
uint256 _gasLimit, uint256 _gasLimit,
bytes memory _data bytes memory _data
) external returns (bytes32) { ) external returns (bytes32) {
string[] memory cmds = new string[](9); string[] memory cmds = new string[](8);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "hashWithdrawal";
cmds[2] = "hashWithdrawal"; cmds[2] = vm.toString(_nonce);
cmds[3] = vm.toString(_nonce); cmds[3] = vm.toString(_sender);
cmds[4] = vm.toString(_sender); cmds[4] = vm.toString(_target);
cmds[5] = vm.toString(_target); cmds[5] = vm.toString(_value);
cmds[6] = vm.toString(_value); cmds[6] = vm.toString(_gasLimit);
cmds[7] = vm.toString(_gasLimit); cmds[7] = vm.toString(_data);
cmds[8] = vm.toString(_data);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (bytes32)); return abi.decode(result, (bytes32));
...@@ -552,14 +549,13 @@ contract FFIInterface is Test { ...@@ -552,14 +549,13 @@ contract FFIInterface is Test {
bytes32 _messagePasserStorageRoot, bytes32 _messagePasserStorageRoot,
bytes32 _latestBlockhash bytes32 _latestBlockhash
) external returns (bytes32) { ) external returns (bytes32) {
string[] memory cmds = new string[](7); string[] memory cmds = new string[](6);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "hashOutputRootProof";
cmds[2] = "hashOutputRootProof"; cmds[2] = Strings.toHexString(uint256(_version));
cmds[3] = Strings.toHexString(uint256(_version)); cmds[3] = Strings.toHexString(uint256(_stateRoot));
cmds[4] = Strings.toHexString(uint256(_stateRoot)); cmds[4] = Strings.toHexString(uint256(_messagePasserStorageRoot));
cmds[5] = Strings.toHexString(uint256(_messagePasserStorageRoot)); cmds[5] = Strings.toHexString(uint256(_latestBlockhash));
cmds[6] = Strings.toHexString(uint256(_latestBlockhash));
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (bytes32)); return abi.decode(result, (bytes32));
...@@ -572,20 +568,19 @@ contract FFIInterface is Test { ...@@ -572,20 +568,19 @@ contract FFIInterface is Test {
uint256 _value, uint256 _value,
uint64 _gas, uint64 _gas,
bytes memory _data, bytes memory _data,
uint256 _logIndex uint64 _logIndex
) external returns (bytes32) { ) external returns (bytes32) {
string[] memory cmds = new string[](11); string[] memory cmds = new string[](10);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "hashDepositTransaction";
cmds[2] = "hashDepositTransaction"; cmds[2] = "0x0000000000000000000000000000000000000000000000000000000000000000";
cmds[3] = "0x0000000000000000000000000000000000000000000000000000000000000000"; cmds[3] = vm.toString(_logIndex);
cmds[4] = vm.toString(_logIndex); cmds[4] = vm.toString(_from);
cmds[5] = vm.toString(_from); cmds[5] = vm.toString(_to);
cmds[6] = vm.toString(_to); cmds[6] = vm.toString(_mint);
cmds[7] = vm.toString(_mint); cmds[7] = vm.toString(_value);
cmds[8] = vm.toString(_value); cmds[8] = vm.toString(_gas);
cmds[9] = vm.toString(_gas); cmds[9] = vm.toString(_data);
cmds[10] = vm.toString(_data);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (bytes32)); return abi.decode(result, (bytes32));
...@@ -595,19 +590,18 @@ contract FFIInterface is Test { ...@@ -595,19 +590,18 @@ contract FFIInterface is Test {
external external
returns (bytes memory) returns (bytes memory)
{ {
string[] memory cmds = new string[](12); string[] memory cmds = new string[](11);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "encodeDepositTransaction";
cmds[2] = "encodeDepositTransaction"; cmds[2] = vm.toString(txn.from);
cmds[3] = vm.toString(txn.from); cmds[3] = vm.toString(txn.to);
cmds[4] = vm.toString(txn.to); cmds[4] = vm.toString(txn.value);
cmds[5] = vm.toString(txn.value); cmds[5] = vm.toString(txn.mint);
cmds[6] = vm.toString(txn.mint); cmds[6] = vm.toString(txn.gasLimit);
cmds[7] = vm.toString(txn.gasLimit); cmds[7] = vm.toString(txn.isCreation);
cmds[8] = vm.toString(txn.isCreation); cmds[8] = vm.toString(txn.data);
cmds[9] = vm.toString(txn.data); cmds[9] = vm.toString(txn.l1BlockHash);
cmds[10] = vm.toString(txn.l1BlockHash); cmds[10] = vm.toString(txn.logIndex);
cmds[11] = vm.toString(txn.logIndex);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (bytes)); return abi.decode(result, (bytes));
...@@ -621,27 +615,25 @@ contract FFIInterface is Test { ...@@ -621,27 +615,25 @@ contract FFIInterface is Test {
uint256 _gasLimit, uint256 _gasLimit,
bytes memory _data bytes memory _data
) external returns (bytes memory) { ) external returns (bytes memory) {
string[] memory cmds = new string[](9); string[] memory cmds = new string[](8);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "encodeCrossDomainMessage";
cmds[2] = "encodeCrossDomainMessage"; cmds[2] = vm.toString(_nonce);
cmds[3] = vm.toString(_nonce); cmds[3] = vm.toString(_sender);
cmds[4] = vm.toString(_sender); cmds[4] = vm.toString(_target);
cmds[5] = vm.toString(_target); cmds[5] = vm.toString(_value);
cmds[6] = vm.toString(_value); cmds[6] = vm.toString(_gasLimit);
cmds[7] = vm.toString(_gasLimit); cmds[7] = vm.toString(_data);
cmds[8] = vm.toString(_data);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (bytes)); return abi.decode(result, (bytes));
} }
function decodeVersionedNonce(uint256 nonce) external returns (uint256, uint256) { function decodeVersionedNonce(uint256 nonce) external returns (uint256, uint256) {
string[] memory cmds = new string[](4); string[] memory cmds = new string[](3);
cmds[0] = "node"; cmds[0] = "scripts/differential-testing/differential-testing";
cmds[1] = "dist/scripts/differential-testing.js"; cmds[1] = "decodeVersionedNonce";
cmds[2] = "decodeVersionedNonce"; cmds[2] = vm.toString(nonce);
cmds[3] = vm.toString(nonce);
bytes memory result = vm.ffi(cmds); bytes memory result = vm.ffi(cmds);
return abi.decode(result, (uint256, uint256)); return abi.decode(result, (uint256, uint256));
......
...@@ -91,7 +91,7 @@ contract Encoding_Test is CommonTest { ...@@ -91,7 +91,7 @@ contract Encoding_Test is CommonTest {
uint64 _gas, uint64 _gas,
bool isCreate, bool isCreate,
bytes memory _data, bytes memory _data,
uint256 _logIndex uint64 _logIndex
) external { ) external {
Types.UserDepositTransaction memory t = Types.UserDepositTransaction( Types.UserDepositTransaction memory t = Types.UserDepositTransaction(
_from, _from,
......
...@@ -129,7 +129,7 @@ contract Hashing_hashDepositTransaction_Test is CommonTest { ...@@ -129,7 +129,7 @@ contract Hashing_hashDepositTransaction_Test is CommonTest {
uint256 _value, uint256 _value,
uint64 _gas, uint64 _gas,
bytes memory _data, bytes memory _data,
uint256 _logIndex uint64 _logIndex
) external { ) external {
assertEq( assertEq(
Hashing.hashDepositTransaction( Hashing.hashDepositTransaction(
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
doStep, doStep,
jsonifyTransaction, jsonifyTransaction,
getTenderlySimulationLink, getTenderlySimulationLink,
getCastCommand,
} from '../src/deploy-utils' } from '../src/deploy-utils'
const deployFn: DeployFunction = async (hre) => { const deployFn: DeployFunction = async (hre) => {
...@@ -98,6 +99,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -98,6 +99,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`) console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`) console.log(`JSON:`)
console.log(jsonifyTransaction(tx)) console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx)) console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
} }
...@@ -135,6 +137,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -135,6 +137,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`) console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`) console.log(`JSON:`)
console.log(jsonifyTransaction(tx)) console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx)) console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
} }
...@@ -172,6 +175,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -172,6 +175,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`) console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`) console.log(`JSON:`)
console.log(jsonifyTransaction(tx)) console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx)) console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
} }
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
isStep, isStep,
doStep, doStep,
getTenderlySimulationLink, getTenderlySimulationLink,
getCastCommand,
} from '../src/deploy-utils' } from '../src/deploy-utils'
const deployFn: DeployFunction = async (hre) => { const deployFn: DeployFunction = async (hre) => {
...@@ -194,6 +195,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -194,6 +195,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`) console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`) console.log(`JSON:`)
console.log(jsonifyTransaction(tx)) console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx)) console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
} }
...@@ -305,6 +307,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -305,6 +307,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`OptimismPortal address: ${OptimismPortal.address}`) console.log(`OptimismPortal address: ${OptimismPortal.address}`)
console.log(`JSON:`) console.log(`JSON:`)
console.log(jsonifyTransaction(tx)) console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx)) console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
} }
...@@ -334,6 +337,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -334,6 +337,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`) console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`) console.log(`JSON:`)
console.log(jsonifyTransaction(tx)) console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx)) console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
} }
......
{
"address": "0x5086d1eEF304eb5284A0f6720f79403b4e9bE294",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "_libAddressManager",
"type": "address"
},
{
"internalType": "string",
"name": "_implementationName",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"stateMutability": "payable",
"type": "fallback"
}
],
"transactionHash": "0xc547cd677c4bcb87deead498c827b1dfcfd5d14826f58a0f7416a46024a03e85",
"receipt": {
"to": null,
"from": "0x3a605B442055DF2898E18cF518feb2e2A6BD0D31",
"contractAddress": "0x5086d1eEF304eb5284A0f6720f79403b4e9bE294",
"transactionIndex": 13,
"gasUsed": "291461",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"blockHash": "0x3e18927a7be75d3ac2b6f54f8c40d781756e78327f9cedd8dff5e79dd49403ff",
"transactionHash": "0xc547cd677c4bcb87deead498c827b1dfcfd5d14826f58a0f7416a46024a03e85",
"logs": [],
"blockNumber": 7017129,
"cumulativeGasUsed": "1033610",
"status": 1,
"byzantium": true
},
"args": [
"0xa6f73589243a6A7a9023b1Fa0651b1d89c177111",
"OVM_L1CrossDomainMessenger"
],
"numDeployments": 1,
"solcInputHash": "76c096070f4b72a86045eb6ab63709ed",
"metadata": "{\"compiler\":{\"version\":\"0.8.9+commit.e5eed63a\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_libAddressManager\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"_implementationName\",\"type\":\"string\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"constructor\":{\"params\":{\"_implementationName\":\"implementationName of the contract to proxy to.\",\"_libAddressManager\":\"Address of the Lib_AddressManager.\"}}},\"title\":\"Lib_ResolvedDelegateProxy\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/libraries/resolver/Lib_ResolvedDelegateProxy.sol\":\"Lib_ResolvedDelegateProxy\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":10000},\"remappings\":[]},\"sources\":{\"@openzeppelin/contracts/access/Ownable.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n\\npragma solidity ^0.8.0;\\n\\nimport \\\"../utils/Context.sol\\\";\\n\\n/**\\n * @dev Contract module which provides a basic access control mechanism, where\\n * there is an account (an owner) that can be granted exclusive access to\\n * specific functions.\\n *\\n * By default, the owner account will be the one that deploys the contract. This\\n * can later be changed with {transferOwnership}.\\n *\\n * This module is used through inheritance. It will make available the modifier\\n * `onlyOwner`, which can be applied to your functions to restrict their use to\\n * the owner.\\n */\\nabstract contract Ownable is Context {\\n address private _owner;\\n\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /**\\n * @dev Initializes the contract setting the deployer as the initial owner.\\n */\\n constructor() {\\n _setOwner(_msgSender());\\n }\\n\\n /**\\n * @dev Returns the address of the current owner.\\n */\\n function owner() public view virtual returns (address) {\\n return _owner;\\n }\\n\\n /**\\n * @dev Throws if called by any account other than the owner.\\n */\\n modifier onlyOwner() {\\n require(owner() == _msgSender(), \\\"Ownable: caller is not the owner\\\");\\n _;\\n }\\n\\n /**\\n * @dev Leaves the contract without owner. It will not be possible to call\\n * `onlyOwner` functions anymore. Can only be called by the current owner.\\n *\\n * NOTE: Renouncing ownership will leave the contract without an owner,\\n * thereby removing any functionality that is only available to the owner.\\n */\\n function renounceOwnership() public virtual onlyOwner {\\n _setOwner(address(0));\\n }\\n\\n /**\\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\\n * Can only be called by the current owner.\\n */\\n function transferOwnership(address newOwner) public virtual onlyOwner {\\n require(newOwner != address(0), \\\"Ownable: new owner is the zero address\\\");\\n _setOwner(newOwner);\\n }\\n\\n function _setOwner(address newOwner) private {\\n address oldOwner = _owner;\\n _owner = newOwner;\\n emit OwnershipTransferred(oldOwner, newOwner);\\n }\\n}\\n\",\"keccak256\":\"0x6bb804a310218875e89d12c053e94a13a4607cdf7cc2052f3e52bd32a0dc50a1\",\"license\":\"MIT\"},\"@openzeppelin/contracts/utils/Context.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n\\npragma solidity ^0.8.0;\\n\\n/**\\n * @dev Provides information about the current execution context, including the\\n * sender of the transaction and its data. While these are generally available\\n * via msg.sender and msg.data, they should not be accessed in such a direct\\n * manner, since when dealing with meta-transactions the account sending and\\n * paying for execution may not be the actual sender (as far as an application\\n * is concerned).\\n *\\n * This contract is only required for intermediate, library-like contracts.\\n */\\nabstract contract Context {\\n function _msgSender() internal view virtual returns (address) {\\n return msg.sender;\\n }\\n\\n function _msgData() internal view virtual returns (bytes calldata) {\\n return msg.data;\\n }\\n}\\n\",\"keccak256\":\"0x90565a39ae45c80f0468dc96c7b20d0afc3055f344c8203a0c9258239f350b9f\",\"license\":\"MIT\"},\"contracts/libraries/resolver/Lib_AddressManager.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.9;\\n\\n/* External Imports */\\nimport { Ownable } from \\\"@openzeppelin/contracts/access/Ownable.sol\\\";\\n\\n/**\\n * @title Lib_AddressManager\\n */\\ncontract Lib_AddressManager is Ownable {\\n /**********\\n * Events *\\n **********/\\n\\n event AddressSet(string indexed _name, address _newAddress, address _oldAddress);\\n\\n /*************\\n * Variables *\\n *************/\\n\\n mapping(bytes32 => address) private addresses;\\n\\n /********************\\n * Public Functions *\\n ********************/\\n\\n /**\\n * Changes the address associated with a particular name.\\n * @param _name String name to associate an address with.\\n * @param _address Address to associate with the name.\\n */\\n function setAddress(string memory _name, address _address) external onlyOwner {\\n bytes32 nameHash = _getNameHash(_name);\\n address oldAddress = addresses[nameHash];\\n addresses[nameHash] = _address;\\n\\n emit AddressSet(_name, _address, oldAddress);\\n }\\n\\n /**\\n * Retrieves the address associated with a given name.\\n * @param _name Name to retrieve an address for.\\n * @return Address associated with the given name.\\n */\\n function getAddress(string memory _name) external view returns (address) {\\n return addresses[_getNameHash(_name)];\\n }\\n\\n /**********************\\n * Internal Functions *\\n **********************/\\n\\n /**\\n * Computes the hash of a name.\\n * @param _name Name to compute a hash for.\\n * @return Hash of the given name.\\n */\\n function _getNameHash(string memory _name) internal pure returns (bytes32) {\\n return keccak256(abi.encodePacked(_name));\\n }\\n}\\n\",\"keccak256\":\"0xcde9b29429d512c549f7c1b8a033f161fa71c18cda08b241748663854196ae14\",\"license\":\"MIT\"},\"contracts/libraries/resolver/Lib_ResolvedDelegateProxy.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.9;\\n\\n/* Library Imports */\\nimport { Lib_AddressManager } from \\\"./Lib_AddressManager.sol\\\";\\n\\n/**\\n * @title Lib_ResolvedDelegateProxy\\n */\\ncontract Lib_ResolvedDelegateProxy {\\n /*************\\n * Variables *\\n *************/\\n\\n // Using mappings to store fields to avoid overwriting storage slots in the\\n // implementation contract. For example, instead of storing these fields at\\n // storage slot `0` & `1`, they are stored at `keccak256(key + slot)`.\\n // See: https://solidity.readthedocs.io/en/v0.7.0/internals/layout_in_storage.html\\n // NOTE: Do not use this code in your own contract system.\\n // There is a known flaw in this contract, and we will remove it from the repository\\n // in the near future. Due to the very limited way that we are using it, this flaw is\\n // not an issue in our system.\\n mapping(address => string) private implementationName;\\n mapping(address => Lib_AddressManager) private addressManager;\\n\\n /***************\\n * Constructor *\\n ***************/\\n\\n /**\\n * @param _libAddressManager Address of the Lib_AddressManager.\\n * @param _implementationName implementationName of the contract to proxy to.\\n */\\n constructor(address _libAddressManager, string memory _implementationName) {\\n addressManager[address(this)] = Lib_AddressManager(_libAddressManager);\\n implementationName[address(this)] = _implementationName;\\n }\\n\\n /*********************\\n * Fallback Function *\\n *********************/\\n\\n fallback() external payable {\\n address target = addressManager[address(this)].getAddress(\\n (implementationName[address(this)])\\n );\\n\\n require(target != address(0), \\\"Target address must be initialized.\\\");\\n\\n // slither-disable-next-line controlled-delegatecall\\n (bool success, bytes memory returndata) = target.delegatecall(msg.data);\\n\\n if (success == true) {\\n assembly {\\n return(add(returndata, 0x20), mload(returndata))\\n }\\n } else {\\n assembly {\\n revert(add(returndata, 0x20), mload(returndata))\\n }\\n }\\n }\\n}\\n\",\"keccak256\":\"0x987774d18365ed25f5be61198e8b241728db6f97c6f2496f4a35bf9dbe0bda2b\",\"license\":\"MIT\"}},\"version\":1}",
"bytecode": "0x608060405234801561001057600080fd5b506040516105b53803806105b583398101604081905261002f91610125565b30600090815260016020908152604080832080546001600160a01b0319166001600160a01b038716179055828252909120825161006e92840190610076565b505050610252565b82805461008290610217565b90600052602060002090601f0160209004810192826100a457600085556100ea565b82601f106100bd57805160ff19168380011785556100ea565b828001600101855582156100ea579182015b828111156100ea5782518255916020019190600101906100cf565b506100f69291506100fa565b5090565b5b808211156100f657600081556001016100fb565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561013857600080fd5b82516001600160a01b038116811461014f57600080fd5b602084810151919350906001600160401b038082111561016e57600080fd5b818601915086601f83011261018257600080fd5b8151818111156101945761019461010f565b604051601f8201601f19908116603f011681019083821181831017156101bc576101bc61010f565b8160405282815289868487010111156101d457600080fd5b600093505b828410156101f657848401860151818501870152928501926101d9565b828411156102075760008684830101525b8096505050505050509250929050565b600181811c9082168061022b57607f821691505b6020821081141561024c57634e487b7160e01b600052602260045260246000fd5b50919050565b610354806102616000396000f3fe608060408181523060009081526001602090815282822054908290529181207fbf40fac1000000000000000000000000000000000000000000000000000000009093529173ffffffffffffffffffffffffffffffffffffffff9091169063bf40fac19061006d9060846101f2565b60206040518083038186803b15801561008557600080fd5b505afa158015610099573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100bd91906102d1565b905073ffffffffffffffffffffffffffffffffffffffff8116610166576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f5461726765742061646472657373206d75737420626520696e697469616c697a60448201527f65642e0000000000000000000000000000000000000000000000000000000000606482015260840160405180910390fd5b6000808273ffffffffffffffffffffffffffffffffffffffff1660003660405161019192919061030e565b600060405180830381855af49150503d80600081146101cc576040519150601f19603f3d011682016040523d82523d6000602084013e6101d1565b606091505b509092509050600182151514156101ea57805160208201f35b805160208201fd5b600060208083526000845481600182811c91508083168061021457607f831692505b85831081141561024b577f4e487b710000000000000000000000000000000000000000000000000000000085526022600452602485fd5b8786018381526020018180156102685760018114610297576102c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008616825287820196506102c2565b60008b81526020902060005b868110156102bc578154848201529085019089016102a3565b83019750505b50949998505050505050505050565b6000602082840312156102e357600080fd5b815173ffffffffffffffffffffffffffffffffffffffff8116811461030757600080fd5b9392505050565b818382376000910190815291905056fea2646970667358221220d66a7dad92a7f7528f41181719174e1d244423b8bb730d2884645c76cfa0944064736f6c63430008090033",
"deployedBytecode": "0x608060408181523060009081526001602090815282822054908290529181207fbf40fac1000000000000000000000000000000000000000000000000000000009093529173ffffffffffffffffffffffffffffffffffffffff9091169063bf40fac19061006d9060846101f2565b60206040518083038186803b15801561008557600080fd5b505afa158015610099573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100bd91906102d1565b905073ffffffffffffffffffffffffffffffffffffffff8116610166576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f5461726765742061646472657373206d75737420626520696e697469616c697a60448201527f65642e0000000000000000000000000000000000000000000000000000000000606482015260840160405180910390fd5b6000808273ffffffffffffffffffffffffffffffffffffffff1660003660405161019192919061030e565b600060405180830381855af49150503d80600081146101cc576040519150601f19603f3d011682016040523d82523d6000602084013e6101d1565b606091505b509092509050600182151514156101ea57805160208201f35b805160208201fd5b600060208083526000845481600182811c91508083168061021457607f831692505b85831081141561024b577f4e487b710000000000000000000000000000000000000000000000000000000085526022600452602485fd5b8786018381526020018180156102685760018114610297576102c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008616825287820196506102c2565b60008b81526020902060005b868110156102bc578154848201529085019089016102a3565b83019750505b50949998505050505050505050565b6000602082840312156102e357600080fd5b815173ffffffffffffffffffffffffffffffffffffffff8116811461030757600080fd5b9392505050565b818382376000910190815291905056fea2646970667358221220d66a7dad92a7f7528f41181719174e1d244423b8bb730d2884645c76cfa0944064736f6c63430008090033",
"devdoc": {
"kind": "dev",
"methods": {
"constructor": {
"params": {
"_implementationName": "implementationName of the contract to proxy to.",
"_libAddressManager": "Address of the Lib_AddressManager."
}
}
},
"title": "Lib_ResolvedDelegateProxy",
"version": 1
},
"userdoc": {
"kind": "user",
"methods": {},
"version": 1
},
"storageLayout": {
"storage": [
{
"astId": 7129,
"contract": "contracts/libraries/resolver/Lib_ResolvedDelegateProxy.sol:Lib_ResolvedDelegateProxy",
"label": "implementationName",
"offset": 0,
"slot": "0",
"type": "t_mapping(t_address,t_string_storage)"
},
{
"astId": 7134,
"contract": "contracts/libraries/resolver/Lib_ResolvedDelegateProxy.sol:Lib_ResolvedDelegateProxy",
"label": "addressManager",
"offset": 0,
"slot": "1",
"type": "t_mapping(t_address,t_contract(Lib_AddressManager)7084)"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_contract(Lib_AddressManager)7084": {
"encoding": "inplace",
"label": "contract Lib_AddressManager",
"numberOfBytes": "20"
},
"t_mapping(t_address,t_contract(Lib_AddressManager)7084)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => contract Lib_AddressManager)",
"numberOfBytes": "32",
"value": "t_contract(Lib_AddressManager)7084"
},
"t_mapping(t_address,t_string_storage)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => string)",
"numberOfBytes": "32",
"value": "t_string_storage"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
"numberOfBytes": "32"
}
}
}
}
\ No newline at end of file
{
"address": "0x636Af16bf2f682dD3109e60102b8E1A089FedAa8",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"stateMutability": "payable",
"type": "fallback"
},
{
"inputs": [],
"name": "getImplementation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_code",
"type": "bytes"
}
],
"name": "setCode",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_key",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_value",
"type": "bytes32"
}
],
"name": "setStorage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
],
"transactionHash": "0xd57130025124776619229f980ae08c3097156ab127c897fa9ef17e29bf757a16",
"receipt": {
"to": null,
"from": "0x3a605B442055DF2898E18cF518feb2e2A6BD0D31",
"contractAddress": "0x636Af16bf2f682dD3109e60102b8E1A089FedAa8",
"transactionIndex": 8,
"gasUsed": "614417",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"blockHash": "0x85ce123b23be93051e83bc9d6dd2bf7817ee0fd338b936684c821020b8285393",
"transactionHash": "0xd57130025124776619229f980ae08c3097156ab127c897fa9ef17e29bf757a16",
"logs": [],
"blockNumber": 7017137,
"cumulativeGasUsed": "5543459",
"status": 1,
"byzantium": true
},
"args": [
"0x3a605B442055DF2898E18cF518feb2e2A6BD0D31"
],
"numDeployments": 1,
"solcInputHash": "0a41276e1e61949b5de1e4f1cd89fb6c",
"metadata": "{\"compiler\":{\"version\":\"0.8.9+commit.e5eed63a\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[],\"name\":\"getImplementation\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"_code\",\"type\":\"bytes\"}],\"name\":\"setCode\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_key\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"_value\",\"type\":\"bytes32\"}],\"name\":\"setStorage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Basic ChugSplash proxy contract for L1. Very close to being a normal proxy but has added functions `setCode` and `setStorage` for changing the code or storage of the contract. Nifty! Note for future developers: do NOT make anything in this contract 'public' unless you know what you're doing. Anything public can potentially have a function signature that conflicts with a signature attached to the implementation contract. Public functions SHOULD always have the 'proxyCallIfNotOwner' modifier unless there's some *really* good reason not to have that modifier. And there almost certainly is not a good reason to not have that modifier. Beware!\",\"kind\":\"dev\",\"methods\":{\"constructor\":{\"params\":{\"_owner\":\"Address of the initial contract owner.\"}},\"getImplementation()\":{\"returns\":{\"_0\":\"Implementation address.\"}},\"getOwner()\":{\"returns\":{\"_0\":\"Owner address.\"}},\"setCode(bytes)\":{\"params\":{\"_code\":\"New contract code to run inside this contract.\"}},\"setOwner(address)\":{\"params\":{\"_owner\":\"New owner of the proxy contract.\"}},\"setStorage(bytes32,bytes32)\":{\"params\":{\"_key\":\"Storage key to modify.\",\"_value\":\"New value for the storage key.\"}}},\"title\":\"L1ChugSplashProxy\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"getImplementation()\":{\"notice\":\"Queries the implementation address. Can only be called by the owner OR by making an eth_call and setting the \\\"from\\\" address to address(0).\"},\"getOwner()\":{\"notice\":\"Queries the owner of the proxy contract. Can only be called by the owner OR by making an eth_call and setting the \\\"from\\\" address to address(0).\"},\"setCode(bytes)\":{\"notice\":\"Sets the code that should be running behind this proxy. Note that this scheme is a bit different from the standard proxy scheme where one would typically deploy the code separately and then set the implementation address. We're doing it this way because it gives us a lot more freedom on the client side. Can only be triggered by the contract owner.\"},\"setOwner(address)\":{\"notice\":\"Changes the owner of the proxy contract. Only callable by the owner.\"},\"setStorage(bytes32,bytes32)\":{\"notice\":\"Modifies some storage slot within the proxy contract. Gives us a lot of power to perform upgrades in a more transparent way. Only callable by the owner.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/chugsplash/L1ChugSplashProxy.sol\":\"L1ChugSplashProxy\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":10000},\"remappings\":[]},\"sources\":{\"contracts/chugsplash/L1ChugSplashProxy.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.9;\\n\\nimport { iL1ChugSplashDeployer } from \\\"./interfaces/iL1ChugSplashDeployer.sol\\\";\\n\\n/**\\n * @title L1ChugSplashProxy\\n * @dev Basic ChugSplash proxy contract for L1. Very close to being a normal proxy but has added\\n * functions `setCode` and `setStorage` for changing the code or storage of the contract. Nifty!\\n *\\n * Note for future developers: do NOT make anything in this contract 'public' unless you know what\\n * you're doing. Anything public can potentially have a function signature that conflicts with a\\n * signature attached to the implementation contract. Public functions SHOULD always have the\\n * 'proxyCallIfNotOwner' modifier unless there's some *really* good reason not to have that\\n * modifier. And there almost certainly is not a good reason to not have that modifier. Beware!\\n */\\ncontract L1ChugSplashProxy {\\n /*************\\n * Constants *\\n *************/\\n\\n // \\\"Magic\\\" prefix. When prepended to some arbitrary bytecode and used to create a contract, the\\n // appended bytecode will be deployed as given.\\n bytes13 internal constant DEPLOY_CODE_PREFIX = 0x600D380380600D6000396000f3;\\n\\n // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)\\n bytes32 internal constant IMPLEMENTATION_KEY =\\n 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\\n\\n // bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)\\n bytes32 internal constant OWNER_KEY =\\n 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;\\n\\n /***************\\n * Constructor *\\n ***************/\\n\\n /**\\n * @param _owner Address of the initial contract owner.\\n */\\n constructor(address _owner) {\\n _setOwner(_owner);\\n }\\n\\n /**********************\\n * Function Modifiers *\\n **********************/\\n\\n /**\\n * Blocks a function from being called when the parent signals that the system should be paused\\n * via an isUpgrading function.\\n */\\n modifier onlyWhenNotPaused() {\\n address owner = _getOwner();\\n\\n // We do a low-level call because there's no guarantee that the owner actually *is* an\\n // L1ChugSplashDeployer contract and Solidity will throw errors if we do a normal call and\\n // it turns out that it isn't the right type of contract.\\n (bool success, bytes memory returndata) = owner.staticcall(\\n abi.encodeWithSelector(iL1ChugSplashDeployer.isUpgrading.selector)\\n );\\n\\n // If the call was unsuccessful then we assume that there's no \\\"isUpgrading\\\" method and we\\n // can just continue as normal. We also expect that the return value is exactly 32 bytes\\n // long. If this isn't the case then we can safely ignore the result.\\n if (success && returndata.length == 32) {\\n // Although the expected value is a *boolean*, it's safer to decode as a uint256 in the\\n // case that the isUpgrading function returned something other than 0 or 1. But we only\\n // really care about the case where this value is 0 (= false).\\n uint256 ret = abi.decode(returndata, (uint256));\\n require(ret == 0, \\\"L1ChugSplashProxy: system is currently being upgraded\\\");\\n }\\n\\n _;\\n }\\n\\n /**\\n * Makes a proxy call instead of triggering the given function when the caller is either the\\n * owner or the zero address. Caller can only ever be the zero address if this function is\\n * being called off-chain via eth_call, which is totally fine and can be convenient for\\n * client-side tooling. Avoids situations where the proxy and implementation share a sighash\\n * and the proxy function ends up being called instead of the implementation one.\\n *\\n * Note: msg.sender == address(0) can ONLY be triggered off-chain via eth_call. If there's a\\n * way for someone to send a transaction with msg.sender == address(0) in any real context then\\n * we have much bigger problems. Primary reason to include this additional allowed sender is\\n * because the owner address can be changed dynamically and we do not want clients to have to\\n * keep track of the current owner in order to make an eth_call that doesn't trigger the\\n * proxied contract.\\n */\\n // slither-disable-next-line incorrect-modifier\\n modifier proxyCallIfNotOwner() {\\n if (msg.sender == _getOwner() || msg.sender == address(0)) {\\n _;\\n } else {\\n // This WILL halt the call frame on completion.\\n _doProxyCall();\\n }\\n }\\n\\n /*********************\\n * Fallback Function *\\n *********************/\\n\\n // slither-disable-next-line locked-ether\\n fallback() external payable {\\n // Proxy call by default.\\n _doProxyCall();\\n }\\n\\n /********************\\n * Public Functions *\\n ********************/\\n\\n /**\\n * Sets the code that should be running behind this proxy. Note that this scheme is a bit\\n * different from the standard proxy scheme where one would typically deploy the code\\n * separately and then set the implementation address. We're doing it this way because it gives\\n * us a lot more freedom on the client side. Can only be triggered by the contract owner.\\n * @param _code New contract code to run inside this contract.\\n */\\n // slither-disable-next-line external-function\\n function setCode(bytes memory _code) public proxyCallIfNotOwner {\\n // Get the code hash of the current implementation.\\n address implementation = _getImplementation();\\n\\n // If the code hash matches the new implementation then we return early.\\n if (keccak256(_code) == _getAccountCodeHash(implementation)) {\\n return;\\n }\\n\\n // Create the deploycode by appending the magic prefix.\\n bytes memory deploycode = abi.encodePacked(DEPLOY_CODE_PREFIX, _code);\\n\\n // Deploy the code and set the new implementation address.\\n address newImplementation;\\n assembly {\\n newImplementation := create(0x0, add(deploycode, 0x20), mload(deploycode))\\n }\\n\\n // Check that the code was actually deployed correctly. I'm not sure if you can ever\\n // actually fail this check. Should only happen if the contract creation from above runs\\n // out of gas but this parent execution thread does NOT run out of gas. Seems like we\\n // should be doing this check anyway though.\\n require(\\n _getAccountCodeHash(newImplementation) == keccak256(_code),\\n \\\"L1ChugSplashProxy: code was not correctly deployed.\\\"\\n );\\n\\n _setImplementation(newImplementation);\\n }\\n\\n /**\\n * Modifies some storage slot within the proxy contract. Gives us a lot of power to perform\\n * upgrades in a more transparent way. Only callable by the owner.\\n * @param _key Storage key to modify.\\n * @param _value New value for the storage key.\\n */\\n // slither-disable-next-line external-function\\n function setStorage(bytes32 _key, bytes32 _value) public proxyCallIfNotOwner {\\n assembly {\\n sstore(_key, _value)\\n }\\n }\\n\\n /**\\n * Changes the owner of the proxy contract. Only callable by the owner.\\n * @param _owner New owner of the proxy contract.\\n */\\n // slither-disable-next-line external-function\\n function setOwner(address _owner) public proxyCallIfNotOwner {\\n _setOwner(_owner);\\n }\\n\\n /**\\n * Queries the owner of the proxy contract. Can only be called by the owner OR by making an\\n * eth_call and setting the \\\"from\\\" address to address(0).\\n * @return Owner address.\\n */\\n // slither-disable-next-line external-function\\n function getOwner() public proxyCallIfNotOwner returns (address) {\\n return _getOwner();\\n }\\n\\n /**\\n * Queries the implementation address. Can only be called by the owner OR by making an\\n * eth_call and setting the \\\"from\\\" address to address(0).\\n * @return Implementation address.\\n */\\n // slither-disable-next-line external-function\\n function getImplementation() public proxyCallIfNotOwner returns (address) {\\n return _getImplementation();\\n }\\n\\n /**********************\\n * Internal Functions *\\n **********************/\\n\\n /**\\n * Sets the implementation address.\\n * @param _implementation New implementation address.\\n */\\n function _setImplementation(address _implementation) internal {\\n assembly {\\n sstore(IMPLEMENTATION_KEY, _implementation)\\n }\\n }\\n\\n /**\\n * Queries the implementation address.\\n * @return Implementation address.\\n */\\n function _getImplementation() internal view returns (address) {\\n address implementation;\\n assembly {\\n implementation := sload(IMPLEMENTATION_KEY)\\n }\\n return implementation;\\n }\\n\\n /**\\n * Changes the owner of the proxy contract.\\n * @param _owner New owner of the proxy contract.\\n */\\n function _setOwner(address _owner) internal {\\n assembly {\\n sstore(OWNER_KEY, _owner)\\n }\\n }\\n\\n /**\\n * Queries the owner of the proxy contract.\\n * @return Owner address.\\n */\\n function _getOwner() internal view returns (address) {\\n address owner;\\n assembly {\\n owner := sload(OWNER_KEY)\\n }\\n return owner;\\n }\\n\\n /**\\n * Gets the code hash for a given account.\\n * @param _account Address of the account to get a code hash for.\\n * @return Code hash for the account.\\n */\\n function _getAccountCodeHash(address _account) internal view returns (bytes32) {\\n bytes32 codeHash;\\n assembly {\\n codeHash := extcodehash(_account)\\n }\\n return codeHash;\\n }\\n\\n /**\\n * Performs the proxy call via a delegatecall.\\n */\\n function _doProxyCall() internal onlyWhenNotPaused {\\n address implementation = _getImplementation();\\n\\n require(implementation != address(0), \\\"L1ChugSplashProxy: implementation is not set yet\\\");\\n\\n assembly {\\n // Copy calldata into memory at 0x0....calldatasize.\\n calldatacopy(0x0, 0x0, calldatasize())\\n\\n // Perform the delegatecall, make sure to pass all available gas.\\n let success := delegatecall(gas(), implementation, 0x0, calldatasize(), 0x0, 0x0)\\n\\n // Copy returndata into memory at 0x0....returndatasize. Note that this *will*\\n // overwrite the calldata that we just copied into memory but that doesn't really\\n // matter because we'll be returning in a second anyway.\\n returndatacopy(0x0, 0x0, returndatasize())\\n\\n // Success == 0 means a revert. We'll revert too and pass the data up.\\n if iszero(success) {\\n revert(0x0, returndatasize())\\n }\\n\\n // Otherwise we'll just return and pass the data up.\\n return(0x0, returndatasize())\\n }\\n }\\n}\\n\",\"keccak256\":\"0xc3cb52dfdc2706992572dd5621ae89ba919fd20539b73488a455d564f16f1b8d\",\"license\":\"MIT\"},\"contracts/chugsplash/interfaces/iL1ChugSplashDeployer.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.9;\\n\\n/**\\n * @title iL1ChugSplashDeployer\\n */\\ninterface iL1ChugSplashDeployer {\\n function isUpgrading() external view returns (bool);\\n}\\n\",\"keccak256\":\"0x9a496d99f111c1091f0c33d6bfc7802a522baa7235614b0014f35e4bbe280e57\",\"license\":\"MIT\"}},\"version\":1}",
"bytecode": "0x608060405234801561001057600080fd5b50604051610a5d380380610a5d83398101604081905261002f9161005d565b610057817fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610355565b5061008d565b60006020828403121561006f57600080fd5b81516001600160a01b038116811461008657600080fd5b9392505050565b6109c18061009c6000396000f3fe60806040526004361061005a5760003560e01c8063893d20e811610043578063893d20e8146100a45780639b0b0fda146100e2578063aaf10f42146101025761005a565b806313af4035146100645780636c5d4ad014610084575b610062610117565b005b34801561007057600080fd5b5061006261007f366004610792565b6103ba565b34801561009057600080fd5b5061006261009f3660046107fe565b61044b565b3480156100b057600080fd5b506100b9610601565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b3480156100ee57600080fd5b506100626100fd3660046108cd565b610698565b34801561010e57600080fd5b506100b9610706565b60006101417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b60408051600481526024810182526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fb7947262000000000000000000000000000000000000000000000000000000001790529051919250600091829173ffffffffffffffffffffffffffffffffffffffff8516916101c3919061092a565b600060405180830381855afa9150503d80600081146101fe576040519150601f19603f3d011682016040523d82523d6000602084013e610203565b606091505b5091509150818015610216575080516020145b156102c8576000818060200190518101906102319190610936565b905080156102c6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603560248201527f4c314368756753706c61736850726f78793a2073797374656d2069732063757260448201527f72656e746c79206265696e67207570677261646564000000000000000000000060648201526084015b60405180910390fd5b505b60006102f27f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610397576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4c314368756753706c61736850726f78793a20696d706c656d656e746174696f60448201527f6e206973206e6f7420736574207965740000000000000000000000000000000060648201526084016102bd565b3660008037600080366000845af43d6000803e806103b4573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610413575033155b1561044357610440817fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610355565b50565b610440610117565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806104a4575033155b156104435760006104d37f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b9050803f8251602084012014156104e8575050565b60405160009061051e907f600d380380600d6000396000f30000000000000000000000000000000000000090859060200161094f565b604051602081830303815290604052905060008151602083016000f084516020860120909150813f146105d3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603360248201527f4c314368756753706c61736850726f78793a20636f646520776173206e6f742060448201527f636f72726563746c79206465706c6f7965642e0000000000000000000000000060648201526084016102bd565b6105fb817f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc55565b50505050565b600061062b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610662575033155b1561068d57507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b610695610117565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806106f1575033155b156106fa579055565b610702610117565b5050565b60006107307fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610767575033155b1561068d57507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6000602082840312156107a457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff811681146107c857600080fd5b9392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b60006020828403121561081057600080fd5b813567ffffffffffffffff8082111561082857600080fd5b818401915084601f83011261083c57600080fd5b81358181111561084e5761084e6107cf565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715610894576108946107cf565b816040528281528760208487010111156108ad57600080fd5b826020860160208301376000928101602001929092525095945050505050565b600080604083850312156108e057600080fd5b50508035926020909101359150565b6000815160005b8181101561091057602081850181015186830152016108f6565b8181111561091f576000828601525b509290920192915050565b60006107c882846108ef565b60006020828403121561094857600080fd5b5051919050565b7fffffffffffffffffffffffffff00000000000000000000000000000000000000831681526000610983600d8301846108ef565b94935050505056fea2646970667358221220aea34fd8cdcf3a9cced029d5f7b1e628f42ad1514501878e0040df2afddb6e7164736f6c63430008090033",
"deployedBytecode": "0x60806040526004361061005a5760003560e01c8063893d20e811610043578063893d20e8146100a45780639b0b0fda146100e2578063aaf10f42146101025761005a565b806313af4035146100645780636c5d4ad014610084575b610062610117565b005b34801561007057600080fd5b5061006261007f366004610792565b6103ba565b34801561009057600080fd5b5061006261009f3660046107fe565b61044b565b3480156100b057600080fd5b506100b9610601565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b3480156100ee57600080fd5b506100626100fd3660046108cd565b610698565b34801561010e57600080fd5b506100b9610706565b60006101417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b60408051600481526024810182526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fb7947262000000000000000000000000000000000000000000000000000000001790529051919250600091829173ffffffffffffffffffffffffffffffffffffffff8516916101c3919061092a565b600060405180830381855afa9150503d80600081146101fe576040519150601f19603f3d011682016040523d82523d6000602084013e610203565b606091505b5091509150818015610216575080516020145b156102c8576000818060200190518101906102319190610936565b905080156102c6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603560248201527f4c314368756753706c61736850726f78793a2073797374656d2069732063757260448201527f72656e746c79206265696e67207570677261646564000000000000000000000060648201526084015b60405180910390fd5b505b60006102f27f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610397576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4c314368756753706c61736850726f78793a20696d706c656d656e746174696f60448201527f6e206973206e6f7420736574207965740000000000000000000000000000000060648201526084016102bd565b3660008037600080366000845af43d6000803e806103b4573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610413575033155b1561044357610440817fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610355565b50565b610440610117565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806104a4575033155b156104435760006104d37f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b9050803f8251602084012014156104e8575050565b60405160009061051e907f600d380380600d6000396000f30000000000000000000000000000000000000090859060200161094f565b604051602081830303815290604052905060008151602083016000f084516020860120909150813f146105d3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603360248201527f4c314368756753706c61736850726f78793a20636f646520776173206e6f742060448201527f636f72726563746c79206465706c6f7965642e0000000000000000000000000060648201526084016102bd565b6105fb817f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc55565b50505050565b600061062b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610662575033155b1561068d57507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b610695610117565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806106f1575033155b156106fa579055565b610702610117565b5050565b60006107307fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610767575033155b1561068d57507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6000602082840312156107a457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff811681146107c857600080fd5b9392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b60006020828403121561081057600080fd5b813567ffffffffffffffff8082111561082857600080fd5b818401915084601f83011261083c57600080fd5b81358181111561084e5761084e6107cf565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715610894576108946107cf565b816040528281528760208487010111156108ad57600080fd5b826020860160208301376000928101602001929092525095945050505050565b600080604083850312156108e057600080fd5b50508035926020909101359150565b6000815160005b8181101561091057602081850181015186830152016108f6565b8181111561091f576000828601525b509290920192915050565b60006107c882846108ef565b60006020828403121561094857600080fd5b5051919050565b7fffffffffffffffffffffffffff00000000000000000000000000000000000000831681526000610983600d8301846108ef565b94935050505056fea2646970667358221220aea34fd8cdcf3a9cced029d5f7b1e628f42ad1514501878e0040df2afddb6e7164736f6c63430008090033",
"devdoc": {
"details": "Basic ChugSplash proxy contract for L1. Very close to being a normal proxy but has added functions `setCode` and `setStorage` for changing the code or storage of the contract. Nifty! Note for future developers: do NOT make anything in this contract 'public' unless you know what you're doing. Anything public can potentially have a function signature that conflicts with a signature attached to the implementation contract. Public functions SHOULD always have the 'proxyCallIfNotOwner' modifier unless there's some *really* good reason not to have that modifier. And there almost certainly is not a good reason to not have that modifier. Beware!",
"kind": "dev",
"methods": {
"constructor": {
"params": {
"_owner": "Address of the initial contract owner."
}
},
"getImplementation()": {
"returns": {
"_0": "Implementation address."
}
},
"getOwner()": {
"returns": {
"_0": "Owner address."
}
},
"setCode(bytes)": {
"params": {
"_code": "New contract code to run inside this contract."
}
},
"setOwner(address)": {
"params": {
"_owner": "New owner of the proxy contract."
}
},
"setStorage(bytes32,bytes32)": {
"params": {
"_key": "Storage key to modify.",
"_value": "New value for the storage key."
}
}
},
"title": "L1ChugSplashProxy",
"version": 1
},
"userdoc": {
"kind": "user",
"methods": {
"getImplementation()": {
"notice": "Queries the implementation address. Can only be called by the owner OR by making an eth_call and setting the \"from\" address to address(0)."
},
"getOwner()": {
"notice": "Queries the owner of the proxy contract. Can only be called by the owner OR by making an eth_call and setting the \"from\" address to address(0)."
},
"setCode(bytes)": {
"notice": "Sets the code that should be running behind this proxy. Note that this scheme is a bit different from the standard proxy scheme where one would typically deploy the code separately and then set the implementation address. We're doing it this way because it gives us a lot more freedom on the client side. Can only be triggered by the contract owner."
},
"setOwner(address)": {
"notice": "Changes the owner of the proxy contract. Only callable by the owner."
},
"setStorage(bytes32,bytes32)": {
"notice": "Modifies some storage slot within the proxy contract. Gives us a lot of power to perform upgrades in a more transparent way. Only callable by the owner."
}
},
"version": 1
},
"storageLayout": {
"storage": [],
"types": null
}
}
\ No newline at end of file
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
"bindings": "cd ../../op-bindings && make", "bindings": "cd ../../op-bindings && make",
"build:forge": "forge build", "build:forge": "forge build",
"build:with-metadata": "FOUNDRY_PROFILE=echidna yarn build:forge", "build:with-metadata": "FOUNDRY_PROFILE=echidna yarn build:forge",
"build:differential": "tsc scripts/differential-testing.ts --outDir dist --moduleResolution node --esModuleInterop", "build:differential": "go build -o ./scripts/differential-testing/differential-testing ./scripts/differential-testing",
"build:fuzz": "(cd test-case-generator && go build ./cmd/fuzz.go)", "build:fuzz": "(cd test-case-generator && go build ./cmd/fuzz.go)",
"prebuild": "yarn ts-node scripts/verify-foundry-install.ts", "prebuild": "yarn ts-node scripts/verify-foundry-install.ts",
"build": "hardhat compile && yarn autogen:artifacts && yarn build:ts && yarn typechain", "build": "hardhat compile && yarn autogen:artifacts && yarn build:ts && yarn typechain",
...@@ -59,8 +59,6 @@ ...@@ -59,8 +59,6 @@
}, },
"devDependencies": { "devDependencies": {
"@eth-optimism/hardhat-deploy-config": "^0.2.5", "@eth-optimism/hardhat-deploy-config": "^0.2.5",
"@ethereumjs/trie": "^5.0.0-beta.1",
"@ethereumjs/util": "^8.0.0-beta.1",
"@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0",
"ethereumjs-wallet": "^1.0.2", "ethereumjs-wallet": "^1.0.2",
......
import { BigNumber, utils, constants } from 'ethers'
import {
decodeVersionedNonce,
hashCrossDomainMessage,
DepositTx,
SourceHashDomain,
encodeCrossDomainMessage,
hashWithdrawal,
hashOutputRootProof,
} from '@eth-optimism/core-utils'
import { SecureTrie } from '@ethereumjs/trie'
import { Account, Address, toBuffer, bufferToHex } from '@ethereumjs/util'
import { predeploys } from '../src'
const { hexZeroPad, keccak256 } = utils
const args = process.argv.slice(2)
const command = args[0]
;(async () => {
switch (command) {
case 'decodeVersionedNonce': {
const input = BigNumber.from(args[1])
const { nonce, version } = decodeVersionedNonce(input)
const output = utils.defaultAbiCoder.encode(
['uint256', 'uint256'],
[nonce.toHexString(), version.toHexString()]
)
process.stdout.write(output)
break
}
case 'encodeCrossDomainMessage': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gasLimit = BigNumber.from(args[5])
const data = args[6]
const encoding = encodeCrossDomainMessage(
nonce,
sender,
target,
value,
gasLimit,
data
)
const output = utils.defaultAbiCoder.encode(['bytes'], [encoding])
process.stdout.write(output)
break
}
case 'hashCrossDomainMessage': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gasLimit = BigNumber.from(args[5])
const data = args[6]
const hash = hashCrossDomainMessage(
nonce,
sender,
target,
value,
gasLimit,
data
)
const output = utils.defaultAbiCoder.encode(['bytes32'], [hash])
process.stdout.write(output)
break
}
case 'hashDepositTransaction': {
// The solidity transaction hash computation currently only works with
// user deposits. System deposit transaction hashing is not supported.
const l1BlockHash = args[1]
const logIndex = BigNumber.from(args[2])
const from = args[3]
const to = args[4]
const mint = BigNumber.from(args[5])
const value = BigNumber.from(args[6])
const gas = BigNumber.from(args[7])
const data = args[8]
const tx = new DepositTx({
l1BlockHash,
logIndex,
from,
to,
mint,
value,
gas,
data,
isSystemTransaction: false,
domain: SourceHashDomain.UserDeposit,
})
const digest = tx.hash()
const output = utils.defaultAbiCoder.encode(['bytes32'], [digest])
process.stdout.write(output)
break
}
case 'encodeDepositTransaction': {
const from = args[1]
const to = args[2]
const value = BigNumber.from(args[3])
const mint = BigNumber.from(args[4])
const gasLimit = BigNumber.from(args[5])
const isCreate = args[6] === 'true' ? true : false
const data = args[7]
const l1BlockHash = args[8]
const logIndex = BigNumber.from(args[9])
const tx = new DepositTx({
from,
to: isCreate ? null : to,
value,
mint,
gas: gasLimit,
data,
l1BlockHash,
logIndex,
domain: SourceHashDomain.UserDeposit,
})
const raw = tx.encode()
const output = utils.defaultAbiCoder.encode(['bytes'], [raw])
process.stdout.write(output)
break
}
case 'hashWithdrawal': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gas = BigNumber.from(args[5])
const data = args[6]
const hash = hashWithdrawal(nonce, sender, target, value, gas, data)
const output = utils.defaultAbiCoder.encode(['bytes32'], [hash])
process.stdout.write(output)
break
}
case 'hashOutputRootProof': {
const version = hexZeroPad(BigNumber.from(args[1]).toHexString(), 32)
const stateRoot = hexZeroPad(BigNumber.from(args[2]).toHexString(), 32)
const messagePasserStorageRoot = hexZeroPad(
BigNumber.from(args[3]).toHexString(),
32
)
const latestBlockhash = hexZeroPad(
BigNumber.from(args[4]).toHexString(),
32
)
const hash = hashOutputRootProof({
version,
stateRoot,
messagePasserStorageRoot,
latestBlockhash,
})
const output = utils.defaultAbiCoder.encode(['bytes32'], [hash])
process.stdout.write(output)
break
}
case 'getProveWithdrawalTransactionInputs': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gas = BigNumber.from(args[5])
const data = args[6]
// Compute the withdrawalHash
const withdrawalHash = hashWithdrawal(
nonce,
sender,
target,
value,
gas,
data
)
// Compute the storage slot the withdrawalHash will be stored in
const slot = utils.defaultAbiCoder.encode(
['bytes32', 'bytes32'],
[withdrawalHash, utils.hexZeroPad('0x', 32)]
)
const key = keccak256(slot)
// Create the account storage trie
const storage = new SecureTrie()
// Put a bool "true" into storage
await storage.put(toBuffer(key), toBuffer('0x01'))
// Put the storage root into the L2ToL1MessagePasser storage
const address = Address.fromString(predeploys.L2ToL1MessagePasser)
const account = Account.fromAccountData({
nonce: 0,
balance: 0,
stateRoot: storage.root,
})
const world = new SecureTrie()
await world.put(address.toBuffer(), account.serialize())
const proof = await SecureTrie.createProof(storage, toBuffer(key))
const outputRoot = hashOutputRootProof({
version: constants.HashZero,
stateRoot: bufferToHex(world.root),
messagePasserStorageRoot: bufferToHex(storage.root),
latestBlockhash: constants.HashZero,
})
const output = utils.defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'bytes32', 'bytes[]'],
[world.root, storage.root, outputRoot, withdrawalHash, proof]
)
process.stdout.write(output)
break
}
}
})().catch((err: Error) => {
console.error(err)
process.stdout.write('')
})
package main
import (
"bytes"
"fmt"
"math/big"
"os"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/trie"
)
// ABI types
var (
// Plain dynamic dynBytes type
dynBytes, _ = abi.NewType("bytes", "", nil)
bytesArgs = abi.Arguments{
{Type: dynBytes},
}
// Plain fixed bytes32 type
fixedBytes, _ = abi.NewType("bytes32", "", nil)
fixedBytesArgs = abi.Arguments{
{Type: fixedBytes},
}
// Decoded nonce tuple (nonce, version)
decodedNonce, _ = abi.NewType("tuple", "DecodedNonce", []abi.ArgumentMarshaling{
{Name: "nonce", Type: "uint256"},
{Name: "version", Type: "uint256"},
})
decodedNonceArgs = abi.Arguments{
{Name: "encodedNonce", Type: decodedNonce},
}
// WithdrawalHash slot tuple (bytes32, bytes32)
withdrawalSlot, _ = abi.NewType("tuple", "SlotHash", []abi.ArgumentMarshaling{
{Name: "withdrawalHash", Type: "bytes32"},
{Name: "zeroPadding", Type: "bytes32"},
})
withdrawalSlotArgs = abi.Arguments{
{Name: "slotHash", Type: withdrawalSlot},
}
// Prove withdrawal inputs tuple (bytes32, bytes32, bytes32, bytes32, bytes[])
proveWithdrawalInputs, _ = abi.NewType("tuple", "ProveWithdrawalInputs", []abi.ArgumentMarshaling{
{Name: "worldRoot", Type: "bytes32"},
{Name: "stateRoot", Type: "bytes32"},
{Name: "outputRoot", Type: "bytes32"},
{Name: "withdrawalHash", Type: "bytes32"},
{Name: "proof", Type: "bytes[]"},
})
proveWithdrawalInputsArgs = abi.Arguments{
{Name: "inputs", Type: proveWithdrawalInputs},
}
)
func main() {
args := os.Args[1:]
// This command requires arguments
if len(args) == 0 {
panic("Error: No arguments provided")
}
switch args[0] {
case "decodeVersionedNonce":
// Parse input arguments
input, ok := new(big.Int).SetString(args[1], 10)
checkOk(ok)
// Decode versioned nonce
nonce, version := crossdomain.DecodeVersionedNonce(input)
// ABI encode output
packArgs := struct {
Nonce *big.Int
Version *big.Int
}{
nonce,
version,
}
packed, err := decodedNonceArgs.Pack(&packArgs)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "encodeCrossDomainMessage":
// Parse input arguments
nonce, ok := new(big.Int).SetString(args[1], 10)
checkOk(ok)
sender := common.HexToAddress(args[2])
target := common.HexToAddress(args[3])
value, ok := new(big.Int).SetString(args[4], 10)
checkOk(ok)
gasLimit, ok := new(big.Int).SetString(args[5], 10)
checkOk(ok)
data := common.FromHex(args[6])
// Encode cross domain message
encoded, err := encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data)
checkErr(err, "Error encoding cross domain message")
// Pack encoded cross domain message
packed, err := bytesArgs.Pack(&encoded)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "hashCrossDomainMessage":
// Parse input arguments
nonce, ok := new(big.Int).SetString(args[1], 10)
checkOk(ok)
sender := common.HexToAddress(args[2])
target := common.HexToAddress(args[3])
value, ok := new(big.Int).SetString(args[4], 10)
checkOk(ok)
gasLimit, ok := new(big.Int).SetString(args[5], 10)
checkOk(ok)
data := common.FromHex(args[6])
// Encode cross domain message
encoded, err := encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data)
checkErr(err, "Error encoding cross domain message")
// Hash encoded cross domain message
hash := crypto.Keccak256Hash(encoded)
// Pack hash
packed, err := fixedBytesArgs.Pack(&hash)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "hashDepositTransaction":
// Parse input arguments
l1BlockHash := common.HexToHash(args[1])
logIndex, ok := new(big.Int).SetString(args[2], 10)
checkOk(ok)
from := common.HexToAddress(args[3])
to := common.HexToAddress(args[4])
mint, ok := new(big.Int).SetString(args[5], 10)
checkOk(ok)
value, ok := new(big.Int).SetString(args[6], 10)
checkOk(ok)
gasLimit, ok := new(big.Int).SetString(args[7], 10)
checkOk(ok)
data := common.FromHex(args[8])
// Create deposit transaction
depositTx := makeDepositTx(from, to, value, mint, gasLimit, false, data, l1BlockHash, logIndex)
// RLP encode deposit transaction
encoded, err := types.NewTx(&depositTx).MarshalBinary()
checkErr(err, "Error encoding deposit transaction")
// Hash encoded deposit transaction
hash := crypto.Keccak256Hash(encoded)
// Pack hash
packed, err := fixedBytesArgs.Pack(&hash)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "encodeDepositTransaction":
// Parse input arguments
from := common.HexToAddress(args[1])
to := common.HexToAddress(args[2])
value, ok := new(big.Int).SetString(args[3], 10)
checkOk(ok)
mint, ok := new(big.Int).SetString(args[4], 10)
checkOk(ok)
gasLimit, ok := new(big.Int).SetString(args[5], 10)
checkOk(ok)
isCreate := args[6] == "true"
data := common.FromHex(args[7])
l1BlockHash := common.HexToHash(args[8])
logIndex, ok := new(big.Int).SetString(args[9], 10)
checkOk(ok)
depositTx := makeDepositTx(from, to, value, mint, gasLimit, isCreate, data, l1BlockHash, logIndex)
// RLP encode deposit transaction
encoded, err := types.NewTx(&depositTx).MarshalBinary()
checkErr(err, "Failed to RLP encode deposit transaction")
// Pack rlp encoded deposit transaction
packed, err := bytesArgs.Pack(&encoded)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "hashWithdrawal":
// Parse input arguments
nonce, ok := new(big.Int).SetString(args[1], 10)
checkOk(ok)
sender := common.HexToAddress(args[2])
target := common.HexToAddress(args[3])
value, ok := new(big.Int).SetString(args[4], 10)
checkOk(ok)
gasLimit, ok := new(big.Int).SetString(args[5], 10)
checkOk(ok)
data := common.FromHex(args[6])
// Hash withdrawal
hash, err := hashWithdrawal(nonce, sender, target, value, gasLimit, data)
checkErr(err, "Error hashing withdrawal")
// Pack hash
packed, err := fixedBytesArgs.Pack(&hash)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "hashOutputRootProof":
// Parse input arguments
version := common.HexToHash(args[1])
stateRoot := common.HexToHash(args[2])
messagePasserStorageRoot := common.HexToHash(args[3])
latestBlockHash := common.HexToHash(args[4])
// Hash the output root proof
hash, err := hashOutputRootProof(version, stateRoot, messagePasserStorageRoot, latestBlockHash)
checkErr(err, "Error hashing output root proof")
// Pack hash
packed, err := fixedBytesArgs.Pack(&hash)
checkErr(err, "Error encoding output")
fmt.Print(hexutil.Encode(packed))
case "getProveWithdrawalTransactionInputs":
// Parse input arguments
nonce, ok := new(big.Int).SetString(args[1], 10)
checkOk(ok)
sender := common.HexToAddress(args[2])
target := common.HexToAddress(args[3])
value, ok := new(big.Int).SetString(args[4], 10)
checkOk(ok)
gasLimit, ok := new(big.Int).SetString(args[5], 10)
checkOk(ok)
data := common.FromHex(args[6])
wdHash, err := hashWithdrawal(nonce, sender, target, value, gasLimit, data)
checkErr(err, "Error hashing withdrawal")
// Compute the storage slot the withdrawalHash will be stored in
slot := struct {
WithdrawalHash common.Hash
ZeroPadding common.Hash
}{
WithdrawalHash: wdHash,
ZeroPadding: common.Hash{},
}
packed, err := withdrawalSlotArgs.Pack(&slot)
checkErr(err, "Error packing withdrawal slot")
// Compute the storage slot the withdrawalHash will be stored in
hash := crypto.Keccak256Hash(packed)
// Create a secure trie for state
state, err := trie.NewStateTrie(
trie.TrieID(types.EmptyRootHash),
trie.NewDatabase(rawdb.NewMemoryDatabase()),
)
checkErr(err, "Error creating secure trie")
// Put a "true" bool in the storage slot
state.Update(hash.Bytes(), []byte{0x01})
// Create a secure trie for the world state
world, err := trie.NewStateTrie(
trie.TrieID(types.EmptyRootHash),
trie.NewDatabase(rawdb.NewMemoryDatabase()),
)
checkErr(err, "Error creating secure trie")
// Put the put the rlp encoded account in the world trie
account := types.StateAccount{
Nonce: 0,
Balance: big.NewInt(0),
Root: state.Hash(),
}
writer := new(bytes.Buffer)
checkErr(account.EncodeRLP(writer), "Error encoding account")
world.Update(predeploys.L2ToL1MessagePasserAddr.Bytes(), writer.Bytes())
// Get the proof
var proof proofList
checkErr(state.Prove(predeploys.L2ToL1MessagePasserAddr.Bytes(), 0, &proof), "Error getting proof")
// Get the output root
outputRoot, err := hashOutputRootProof(common.Hash{}, world.Hash(), state.Hash(), common.Hash{})
checkErr(err, "Error hashing output root proof")
// Pack the output
output := struct {
WorldRoot common.Hash
StateRoot common.Hash
OutputRoot common.Hash
WithdrawalHash common.Hash
Proof proofList
}{
WorldRoot: world.Hash(),
StateRoot: state.Hash(),
OutputRoot: outputRoot,
WithdrawalHash: wdHash,
Proof: proof,
}
packed, err = proveWithdrawalInputsArgs.Pack(&output)
checkErr(err, "Error encoding output")
// Print the output
fmt.Print(hexutil.Encode(packed[32:]))
default:
panic(fmt.Errorf("Unknown command: %s", args[0]))
}
}
package main
import (
"errors"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
var UnknownNonceVersion = errors.New("Unknown nonce version")
// checkOk checks if ok is false, and panics if so.
// Shorthand to ease go's god awful error handling
func checkOk(ok bool) {
if !ok {
panic(fmt.Errorf("checkOk failed"))
}
}
// checkErr checks if err is not nil, and throws if so.
// Shorthand to ease go's god awful error handling
func checkErr(err error, failReason string) {
if err != nil {
panic(fmt.Errorf("%s: %s", failReason, err))
}
}
// encodeCrossDomainMessage encodes a versioned cross domain message into a byte array.
func encodeCrossDomainMessage(nonce *big.Int, sender common.Address, target common.Address, value *big.Int, gasLimit *big.Int, data []byte) ([]byte, error) {
_, version := crossdomain.DecodeVersionedNonce(nonce)
var encoded []byte
var err error
if version.Cmp(big.NewInt(0)) == 0 {
// Encode cross domain message V0
encoded, err = crossdomain.EncodeCrossDomainMessageV0(target, sender, data, nonce)
} else if version.Cmp(big.NewInt(1)) == 0 {
// Encode cross domain message V1
encoded, err = crossdomain.EncodeCrossDomainMessageV1(nonce, sender, target, value, gasLimit, data)
} else {
return nil, UnknownNonceVersion
}
return encoded, err
}
// hashWithdrawal hashes a withdrawal transaction.
func hashWithdrawal(nonce *big.Int, sender common.Address, target common.Address, value *big.Int, gasLimit *big.Int, data []byte) (common.Hash, error) {
wd := crossdomain.Withdrawal{
Nonce: nonce,
Sender: &sender,
Target: &target,
Value: value,
GasLimit: gasLimit,
Data: data,
}
return wd.Hash()
}
// hashOutputRootProof hashes an output root proof.
func hashOutputRootProof(version common.Hash, stateRoot common.Hash, messagePasserStorageRoot common.Hash, latestBlockHash common.Hash) (common.Hash, error) {
hash, err := rollup.ComputeL2OutputRoot(&bindings.TypesOutputRootProof{
Version: version,
StateRoot: stateRoot,
MessagePasserStorageRoot: messagePasserStorageRoot,
LatestBlockhash: latestBlockHash,
})
if err != nil {
return common.Hash{}, err
}
return common.Hash(hash), nil
}
// makeDepositTx creates a deposit transaction type.
func makeDepositTx(
from common.Address,
to common.Address,
value *big.Int,
mint *big.Int,
gasLimit *big.Int,
isCreate bool,
data []byte,
l1BlockHash common.Hash,
logIndex *big.Int,
) types.DepositTx {
// Create deposit transaction source
udp := derive.UserDepositSource{
L1BlockHash: l1BlockHash,
LogIndex: logIndex.Uint64(),
}
// Create deposit transaction
depositTx := types.DepositTx{
SourceHash: udp.SourceHash(),
From: from,
Value: value,
Gas: gasLimit.Uint64(),
IsSystemTransaction: false, // This will never be a system transaction in the tests.
Data: data,
}
// Fill optional fields
if mint.Cmp(big.NewInt(0)) == 1 {
depositTx.Mint = mint
}
if !isCreate {
depositTx.To = &to
}
return depositTx
}
// Custom type to write the generated proof to
type proofList [][]byte
func (n *proofList) Put(key []byte, value []byte) error {
*n = append(*n, value)
return nil
}
func (n *proofList) Delete(key []byte) error {
panic("not supported")
}
...@@ -181,7 +181,6 @@ export const getContractFromArtifact = async ( ...@@ -181,7 +181,6 @@ export const getContractFromArtifact = async (
} = {} } = {}
): Promise<ethers.Contract> => { ): Promise<ethers.Contract> => {
const artifact = await hre.deployments.get(name) const artifact = await hre.deployments.get(name)
await hre.ethers.provider.waitForTransaction(artifact.receipt.transactionHash)
// Get the deployed contract's interface. // Get the deployed contract's interface.
let iface = new hre.ethers.utils.Interface(artifact.abi) let iface = new hre.ethers.utils.Interface(artifact.abi)
...@@ -396,3 +395,15 @@ export const getTenderlySimulationLink = async ( ...@@ -396,3 +395,15 @@ export const getTenderlySimulationLink = async (
}).toString()}` }).toString()}`
} }
} }
/**
* Returns a cast commmand for submitting a given transaction.
*
* @param tx Ethers transaction object.
* @returns the cast command
*/
export const getCastCommand = (tx: ethers.PopulatedTransaction): string => {
if (process.env.CAST_COMMANDS) {
return `cast send ${tx.to} ${tx.data} --from ${tx.from} --value ${tx.value}`
}
}
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier",
"chai"
]
# URL for an L1 RPC provider, used to query L2 output proposals
TWO_STEP_MONITOR__L1_RPC_PROVIDER=
# URL for an L2 RPC provider, used to query canonical L2 state
TWO_STEP_MONITOR__L2_RPC_PROVIDER=
TWO_STEP_MONITOR__HOSTNAME=
TWO_STEP_MONITOR__PORT=
TWO_STEP_MONITOR__START_BATCH_INDEX=
TWO_STEP_MONITOR__LOOP_INTERVAL_MS=
module.exports = {
extends: '../../.eslintrc.js',
}
module.exports = {
...require('../../.prettierrc.js'),
};
(The MIT License)
Copyright 2020-2021 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.
# @eth-optimism/two-step-monitor
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=two-step-monitor-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
The `two-step-monitor` is a simple service for detecting discrepancies between withdrawals created on L2, and
withdrawals proven on L1.
## Installation
Clone, install, and build the Optimism monorepo:
```
git clone https://github.com/ethereum-optimism/optimism.git
yarn install
yarn build
```
## Running the service
Copy `.env.example` into a new file named `.env`, then set the environment variables listed there.
Once your environment variables have been set, run the service via:
```
yarn start
```
## Ports
- API is exposed at `$TWO_STEP_MONITOR__HOSTNAME:$TWO_STEP_MONITOR__PORT/api`
- Metrics are exposed at `$TWO_STEP_MONITOR__HOSTNAME:$TWO_STEP_MONITOR__PORT/metrics`
- `$TWO_STEP_MONITOR__HOSTNAME` defaults to `0.0.0.0`
- `$TWO_STEP_MONITOR__PORT` defaults to `7300`
## What this service does
The `two-step-monitor` detects when a withdrawal is proven on L1, and verifies that a corresponding withdrawal
has been created on L2.
We export a series of Prometheus metrics that you can use to trigger alerting when issues are detected.
Check the list of available metrics via `yarn start --help`:
```sh
> yarn start --help
yarn run v1.22.19
$ ts-node ./src/service.ts --help
Usage: service [options]
Options:
--l1rpcprovider Provider for interacting with L1 (env: TWO_STEP_MONITOR__L1_RPC_PROVIDER)
--l2rpcprovider Provider for interacting with L2 (env: TWO_STEP_MONITOR__L2_RPC_PROVIDER)
--port Port for the app server (env: TWO_STEP_MONITOR__PORT)
--hostname Hostname for the app server (env: TWO_STEP_MONITOR__HOSTNAME)
-h, --help display help for command
Metrics:
l1_node_connection_failures Number of times L1 node connection has failed (type: Gauge)
l2_node_connection_failures Number of times L2 node connection has failed (type: Gauge)
metadata Service metadata (type: Gauge)
unhandled_errors Unhandled errors (type: Counter)
Done in 2.19s.
```
import { HardhatUserConfig } from 'hardhat/types'
// Hardhat plugins
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
const config: HardhatUserConfig = {
mocha: {
timeout: 50000,
},
}
export default config
{
"private": true,
"name": "@eth-optimism/two-step-monitor",
"version": "0.5.0",
"description": "[Optimism] Service for detecting faulty L2 output proposals",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*"
],
"scripts": {
"start": "ts-node ./src/service.ts",
"test": "hardhat test",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json",
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint . --max-warnings=0"
},
"keywords": [
"optimism",
"ethereum",
"fault",
"detector"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/two-step-monitor#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.6",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@types/chai": "^4.3.1",
"chai-as-promised": "^7.1.1",
"ethers": "^5.7.0",
"hardhat": "^2.9.6",
"ts-node": "^10.9.1"
}
}
export const todo = 'implement me'
import chai = require('chai')
import chaiAsPromised from 'chai-as-promised'
// Chai plugins go here.
chai.use(chaiAsPromised)
const should = chai.should()
const expect = chai.expect
export { should, expect }
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"package.json",
"src/**/*"
]
}
...@@ -928,10 +928,11 @@ follows: ...@@ -928,10 +928,11 @@ follows:
- `timestamp` is set to the batch's timestamp. - `timestamp` is set to the batch's timestamp.
- `random` is set to the `prev_randao` L1 block attribute. - `random` is set to the `prev_randao` L1 block attribute.
- `suggestedFeeRecipient` is set to an address determined by the sequencer. - `suggestedFeeRecipient` is set to the Sequencer Fee Vault address. See [Fee Vaults] specification.
- `transactions` is the array of the derived transactions: deposited transactions and sequenced transactions, all - `transactions` is the array of the derived transactions: deposited transactions and sequenced transactions, all
encoded with [EIP-2718]. encoded with [EIP-2718].
- `noTxPool` is set to `true`, to use the exact above `transactions` list when constructing the block. - `noTxPool` is set to `true`, to use the exact above `transactions` list when constructing the block.
- `gasLimit` is set to the current `gasLimit` value in the [system configuration][g-system-config] of this payload. - `gasLimit` is set to the current `gasLimit` value in the [system configuration][g-system-config] of this payload.
[extended-attributes]: exec-engine.md#extended-payloadattributesv1 [extended-attributes]: exec-engine.md#extended-payloadattributesv1
[Fee Vaults]: exec-engine.md#fee-vaults
...@@ -6,6 +6,11 @@ ...@@ -6,6 +6,11 @@
- [Deposited transaction processing](#deposited-transaction-processing) - [Deposited transaction processing](#deposited-transaction-processing)
- [Deposited transaction boundaries](#deposited-transaction-boundaries) - [Deposited transaction boundaries](#deposited-transaction-boundaries)
- [Fees](#fees)
- [Fee Vaults](#fee-vaults)
- [Priority fees (Sequencer Fee Vault)](#priority-fees-sequencer-fee-vault)
- [Base fees (Base Fee Vault)](#base-fees-base-fee-vault)
- [L1-Cost fees (L1 Fee Vault)](#l1-cost-fees-l1-fee-vault)
- [Engine API](#engine-api) - [Engine API](#engine-api)
- [`engine_forkchoiceUpdatedV1`](#engine_forkchoiceupdatedv1) - [`engine_forkchoiceUpdatedV1`](#engine_forkchoiceupdatedv1)
- [Extended PayloadAttributesV1](#extended-payloadattributesv1) - [Extended PayloadAttributesV1](#extended-payloadattributesv1)
...@@ -46,6 +51,74 @@ To process deposited transactions safely, the deposits MUST be authenticated fir ...@@ -46,6 +51,74 @@ To process deposited transactions safely, the deposits MUST be authenticated fir
Deposited transactions MUST never be consumed from the transaction pool. Deposited transactions MUST never be consumed from the transaction pool.
*The transaction pool can be disabled in a deposits-only rollup* *The transaction pool can be disabled in a deposits-only rollup*
## Fees
Sequenced transactions (i.e. not applicable to deposits) are charged with 3 types of fees:
priority fees, base fees, and L1-cost fees.
### Fee Vaults
The three types of fees are collected in 3 distinct L2 fee-vault deployments for accounting purposes:
fee payments are not registered as internal EVM calls, and thus distinguished better this way.
These are hardcoded addresses, pointing at pre-deployed proxy contracts.
The proxies are backed by vault contract deployments, based on `FeeVault`, to route vault funds to L1 securely.
| Vault Name | Predeploy |
|---------------------|----------------------------------------------------------|
| Sequencer Fee Vault | [`SequencerFeeVault`](./predeploys.md#SequencerFeeVault) |
| Base Fee Vault | [`BaseFeeVault`](./predeploys.md#BaseFeeVault) |
| L1 Fee Vault | [`L1FeeVault`](./predeploys.md#L1FeeVault) |
### Priority fees (Sequencer Fee Vault)
Priority fees follow the [eip-1559] specification, and are collected by the fee-recipient of the L2 block.
The block fee-recipient (a.k.a. coinbase address) is set to the Sequencer Fee Vault address.
### Base fees (Base Fee Vault)
Base fees largely follow the [eip-1559] specification, with the exception that base fees are not burned,
but add up to the Base Fee Vault ETH account balance.
### L1-Cost fees (L1 Fee Vault)
The protocol funds batch-submission of sequenced L2 transactions by charging L2 users an additional fee
based on the estimated batch-submission costs.
This fee is charged from the L2 transaction-sender ETH balance, and collected into the L1 Fee Vault.
The exact L1 cost function to determine the L1-cost fee component of a L2 transaction is calculated as:
`(rollupDataGas + l1FeeOverhead) * l1Basefee * l1FeeScalar / 1000000`
(big-int computation, result in Wei and `uint256` range)
Where:
- `rollupDataGas` is determined from the *full* encoded transaction
(standard EIP-2718 transaction encoding, including signature fields):
- Before Regolith fork: `rollupDataGas = zeroes * 4 + (ones + 68) * 16`
- The addition of `68` non-zero bytes is a remnant of a pre-Bedrock L1-cost accounting function,
which accounted for the worst-case non-zero bytes addition to complement unsigned transactions, unlike Bedrock.
- With Regolith fork: `rollupDataGas = zeroes * 4 + ones * 16`
- `l1FeeOverhead` is the Gas Price Oracle `overhead` value.
- `l1FeeScalar` is the Gas Price Oracle `scalar` value.
- `l1Basefee` is the L1 Base fee of the latest L1 origin registered in the L2 chain.
Note that the `rollupDataGas` uses the same byte cost accounting as defined in [eip-2028],
except the full L2 transaction now counts towards the bytes charged in the L1 calldata.
This behavior matches pre-Bedrock L1-cost estimation of L2 transactions.
Compression, batching, and intrinsic gas costs of the batch transactions are accounted for by the protocol
with the Gas Price Oracle `overhead` and `scalar` parameters.
The Gas Price Oracle `l1FeeOverhead` and `l1FeeScalar`, as well as the `l1Basefee` of the L1 origin,
can be accessed in two interchangeable ways:
- read from the deposited L1 attributes (`l1FeeOverhead`, `l1FeeScalar`, `basefee`) of the current L2 block
- read from the L1 Block Info contract (`0x4200000000000000000000000000000000000015`)
- using the respective solidity `uint256`-getter functions (`l1FeeOverhead`, `l1FeeScalar`, `basefee`)
- using direct storage-reads:
- L1 basefee as big-endian `uint256` in slot `1`
- Overhead as big-endian `uint256` in slot `5`
- Scalar as big-endian `uint256` in slot `6`
## Engine API ## Engine API
<!-- <!--
...@@ -190,6 +263,8 @@ the operation within the engine is the exact same as with L1 (although with an E ...@@ -190,6 +263,8 @@ the operation within the engine is the exact same as with L1 (although with an E
[rollup node spec]: rollup-node.md [rollup node spec]: rollup-node.md
[eip-1559]: https://eips.ethereum.org/EIPS/eip-1559
[eip-2028]: https://eips.ethereum.org/EIPS/eip-2028
[eip-2718]: https://eips.ethereum.org/EIPS/eip-2718 [eip-2718]: https://eips.ethereum.org/EIPS/eip-2718
[eip-2718-transactions]: https://eips.ethereum.org/EIPS/eip-2718#transactions [eip-2718-transactions]: https://eips.ethereum.org/EIPS/eip-2718#transactions
[exec-api-data]: https://github.com/ethereum/execution-apis/blob/769c53c94c4e487337ad0edea9ee0dce49c79bfa/src/engine/specification.md#structures [exec-api-data]: https://github.com/ethereum/execution-apis/blob/769c53c94c4e487337ad0edea9ee0dce49c79bfa/src/engine/specification.md#structures
......
...@@ -80,8 +80,10 @@ Summary of changes: ...@@ -80,8 +80,10 @@ Summary of changes:
including the `contractAddress` field of deposits that deploy contracts. including the `contractAddress` field of deposits that deploy contracts.
- The `gas` and `depositNonce` data is committed to as part of the consensus-representation of the receipt, - The `gas` and `depositNonce` data is committed to as part of the consensus-representation of the receipt,
enabling the data to be safely synced between independent L2 nodes. enabling the data to be safely synced between independent L2 nodes.
- The L1-cost function was corrected to more closely match pre-Bedrock behavior.
The [deposit specification](./deposits.md) specifies the changes of the Regolith upgrade in more detail. The [deposit specification](./deposits.md) specifies the deposit changes of the Regolith upgrade in more detail.
The [execution engine specification](./exec-engine.md) specifies the L1 cost function difference.
The Regolith upgrade uses a *L2 block-timestamp* activation-rule, and is specified in both the The Regolith upgrade uses a *L2 block-timestamp* activation-rule, and is specified in both the
rollup-node (`regolith_time`) and execution engine (`config.regolithTime`). rollup-node (`regolith_time`) and execution engine (`config.regolithTime`).
...@@ -1105,20 +1105,6 @@ ...@@ -1105,20 +1105,6 @@
ethereumjs-util "^7.1.1" ethereumjs-util "^7.1.1"
miller-rabin "^4.0.0" miller-rabin "^4.0.0"
"@ethereumjs/trie@^5.0.0-beta.1":
version "5.0.0-beta.1"
resolved "https://registry.yarnpkg.com/@ethereumjs/trie/-/trie-5.0.0-beta.1.tgz#79d1108222b45bc3576d62583364c96626ce4175"
integrity sha512-OjTzt9fK5aMzm84GRSe+C7bO2zorbEWRueLbxOMlS7lHCiXA7akIQ3mzz9VBSMjT7m01hZ1r3fZIOGHzQVCHtw==
dependencies:
"@ethereumjs/util" "8.0.0-beta.1"
abstract-level "^1.0.3"
ethereum-cryptography "^1.0.3"
level "^8.0.0"
memory-level "^1.0.0"
readable-stream "^3.6.0"
rlp "4.0.0-beta.1"
semaphore-async-await "^1.5.1"
"@ethereumjs/tx@^3.2.1": "@ethereumjs/tx@^3.2.1":
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.0.tgz#14ed1b7fa0f28e1cd61e3ecbdab824205f6a4378" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.0.tgz#14ed1b7fa0f28e1cd61e3ecbdab824205f6a4378"
...@@ -1143,14 +1129,6 @@ ...@@ -1143,14 +1129,6 @@
"@ethereumjs/common" "^2.6.3" "@ethereumjs/common" "^2.6.3"
ethereumjs-util "^7.1.4" ethereumjs-util "^7.1.4"
"@ethereumjs/util@8.0.0-beta.1", "@ethereumjs/util@^8.0.0-beta.1":
version "8.0.0-beta.1"
resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.0.0-beta.1.tgz#369526faf6e9f1cadfd39c7741cc07cf33d128f8"
integrity sha512-yUg3TdJm25HiamAXbNuOagXQPmgdSrV3oEH0h+Adsxt6D7qHw8HyHLA8C+tNrLP2YwcjF1dGJ+F7WtOibzEp9g==
dependencies:
ethereum-cryptography "^1.0.3"
rlp "4.0.0-beta.1"
"@ethereumjs/vm@^5.9.0": "@ethereumjs/vm@^5.9.0":
version "5.9.0" version "5.9.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.9.0.tgz#54e485097c6dbb42554d541ef8d84d06b7ddf12f" resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.9.0.tgz#54e485097c6dbb42554d541ef8d84d06b7ddf12f"
...@@ -5534,19 +5512,6 @@ abort-controller@^3.0.0: ...@@ -5534,19 +5512,6 @@ abort-controller@^3.0.0:
dependencies: dependencies:
event-target-shim "^5.0.0" event-target-shim "^5.0.0"
abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-1.0.3.tgz#78a67d3d84da55ee15201486ab44c09560070741"
integrity sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA==
dependencies:
buffer "^6.0.3"
catering "^2.1.0"
is-buffer "^2.0.5"
level-supports "^4.0.0"
level-transcoder "^1.0.1"
module-error "^1.0.1"
queue-microtask "^1.2.3"
abstract-leveldown@3.0.0: abstract-leveldown@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-3.0.0.tgz#5cb89f958a44f526779d740d1440e743e0c30a57" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-3.0.0.tgz#5cb89f958a44f526779d740d1440e743e0c30a57"
...@@ -7086,16 +7051,6 @@ brorand@^1.0.1, brorand@^1.1.0: ...@@ -7086,16 +7051,6 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-level@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-1.0.1.tgz#36e8c3183d0fe1c405239792faaab5f315871011"
integrity sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==
dependencies:
abstract-level "^1.0.2"
catering "^2.1.1"
module-error "^1.0.2"
run-parallel-limit "^1.1.0"
browser-stdout@1.3.1: browser-stdout@1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
...@@ -7528,11 +7483,6 @@ caseless@^0.12.0, caseless@~0.12.0: ...@@ -7528,11 +7483,6 @@ caseless@^0.12.0, caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
catering@^2.1.0, catering@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510"
integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==
cbor@^5.0.2: cbor@^5.0.2:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/cbor/-/cbor-5.2.0.tgz#4cca67783ccd6de7b50ab4ed62636712f287a67c" resolved "https://registry.yarnpkg.com/cbor/-/cbor-5.2.0.tgz#4cca67783ccd6de7b50ab4ed62636712f287a67c"
...@@ -7803,17 +7753,6 @@ class-utils@^0.3.5: ...@@ -7803,17 +7753,6 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classic-level@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/classic-level/-/classic-level-1.2.0.tgz#2d52bdec8e7a27f534e67fdeb890abef3e643c27"
integrity sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg==
dependencies:
abstract-level "^1.0.2"
catering "^2.1.0"
module-error "^1.0.1"
napi-macros "~2.0.0"
node-gyp-build "^4.3.0"
clean-regexp@^1.0.0: clean-regexp@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7" resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7"
...@@ -13089,7 +13028,7 @@ is-buffer@^1.1.5: ...@@ -13089,7 +13028,7 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.0, is-buffer@^2.0.5, is-buffer@~2.0.3: is-buffer@^2.0.0, is-buffer@~2.0.3:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
...@@ -14256,11 +14195,6 @@ level-sublevel@6.6.4: ...@@ -14256,11 +14195,6 @@ level-sublevel@6.6.4:
typewiselite "~1.0.0" typewiselite "~1.0.0"
xtend "~4.0.0" xtend "~4.0.0"
level-supports@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-4.0.1.tgz#431546f9d81f10ff0fea0e74533a0e875c08c66a"
integrity sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==
level-supports@~1.0.0: level-supports@~1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d"
...@@ -14268,14 +14202,6 @@ level-supports@~1.0.0: ...@@ -14268,14 +14202,6 @@ level-supports@~1.0.0:
dependencies: dependencies:
xtend "^4.0.2" xtend "^4.0.2"
level-transcoder@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c"
integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==
dependencies:
buffer "^6.0.3"
module-error "^1.0.1"
level-ws@0.0.0: level-ws@0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-0.0.0.tgz#372e512177924a00424b0b43aef2bb42496d228b" resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-0.0.0.tgz#372e512177924a00424b0b43aef2bb42496d228b"
...@@ -14311,14 +14237,6 @@ level-ws@^2.0.0: ...@@ -14311,14 +14237,6 @@ level-ws@^2.0.0:
level-packager "^5.1.0" level-packager "^5.1.0"
leveldown "^5.4.0" leveldown "^5.4.0"
level@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/level/-/level-8.0.0.tgz#41b4c515dabe28212a3e881b61c161ffead14394"
integrity sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ==
dependencies:
browser-level "^1.0.1"
classic-level "^1.2.0"
leveldown@^5.4.0: leveldown@^5.4.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98"
...@@ -15176,15 +15094,6 @@ memdown@~3.0.0: ...@@ -15176,15 +15094,6 @@ memdown@~3.0.0:
ltgt "~2.2.0" ltgt "~2.2.0"
safe-buffer "~5.1.1" safe-buffer "~5.1.1"
memory-level@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/memory-level/-/memory-level-1.0.0.tgz#7323c3fd368f9af2f71c3cd76ba403a17ac41692"
integrity sha512-UXzwewuWeHBz5krr7EvehKcmLFNoXxGcvuYhC41tRnkrTbJohtS7kVn9akmgirtRygg+f7Yjsfi8Uu5SGSQ4Og==
dependencies:
abstract-level "^1.0.0"
functional-red-black-tree "^1.0.1"
module-error "^1.0.1"
memorystream@^0.3.1: memorystream@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
...@@ -15849,11 +15758,6 @@ modify-values@^1.0.0: ...@@ -15849,11 +15758,6 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
module-error@^1.0.1, module-error@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86"
integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==
morgan@^1.10.0: morgan@^1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7"
...@@ -16164,11 +16068,6 @@ node-gyp-build@^4.2.0: ...@@ -16164,11 +16068,6 @@ node-gyp-build@^4.2.0:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"
integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==
node-gyp-build@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
node-gyp-build@~4.1.0: node-gyp-build@~4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
...@@ -18046,7 +17945,7 @@ querystringify@^2.1.1: ...@@ -18046,7 +17945,7 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
queue-microtask@^1.2.2, queue-microtask@^1.2.3: queue-microtask@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
...@@ -18818,11 +18717,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: ...@@ -18818,11 +18717,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0" hash-base "^3.0.0"
inherits "^2.0.1" inherits "^2.0.1"
rlp@4.0.0-beta.1:
version "4.0.0-beta.1"
resolved "https://registry.yarnpkg.com/rlp/-/rlp-4.0.0-beta.1.tgz#46983ee758344e5eee48f135129407434cfea2b6"
integrity sha512-UVIENF7Rw+nX5cpfzw6X3/oXNQKsSZ8HbDJUeU9RoIs1LLyMjcPZR1o26i1vFbpuVN8GRmcdopEYOMjVsLRsQQ==
rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6: rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.6.tgz#c80ba6266ac7a483ef1e69e8e2f056656de2fb2c" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.6.tgz#c80ba6266ac7a483ef1e69e8e2f056656de2fb2c"
...@@ -18862,13 +18756,6 @@ run-async@^2.2.0, run-async@^2.4.0: ...@@ -18862,13 +18756,6 @@ run-async@^2.2.0, run-async@^2.4.0:
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
run-parallel-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba"
integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==
dependencies:
queue-microtask "^1.2.2"
run-parallel@^1.1.9: run-parallel@^1.1.9:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
......
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