diff --git a/hildr-batcher/build.gradle b/hildr-batcher/build.gradle index a4249521..e7ae0b35 100644 --- a/hildr-batcher/build.gradle +++ b/hildr-batcher/build.gradle @@ -8,7 +8,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' } -group = 'me.grapebaba' +group = 'io.optimism' version = '0.4.0' repositories { @@ -33,7 +33,7 @@ repositories { application { // Define the main class for the application. - mainClass = 'io.optimism.HildrBatcher' + mainClass = 'io.optimism.batcher.HildrBatcher' } tasks.withType(JavaCompile).configureEach { @@ -160,18 +160,6 @@ jacocoTestCoverageVerification { } } -//checkstyle { -// toolVersion = '10.10.0' -// // default checkstyle config -- specific to your team agreement -// configFile = project(":").file("config/checkstyle/google_checks.xml") -// // Google style (idiosyncratic to Google): -// // configFile = project(":").file("config/checkstyle/google_checks.xml") -// // SUN style (closest to modern Java styles) -- the basis for this project: -// // configFile = project(":").file("config/checkstyle/sun_checks.xml") -//// ignoreFailures = false -//// maxWarnings = 0 -//} - spotless { // optional: limit format enforcement to just the files changed by this feature branch // ratchetFrom 'origin/main' diff --git a/hildr-batcher/src/main/java/io/optimism/batcher/HildrBatcher.java b/hildr-batcher/src/main/java/io/optimism/batcher/HildrBatcher.java index 6aa68876..4b32af6f 100644 --- a/hildr-batcher/src/main/java/io/optimism/batcher/HildrBatcher.java +++ b/hildr-batcher/src/main/java/io/optimism/batcher/HildrBatcher.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023 q315xia@163.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - package io.optimism.batcher; import io.optimism.batcher.cli.Cli; diff --git a/hildr-batcher/src/main/java/io/optimism/batcher/cli/Cli.java b/hildr-batcher/src/main/java/io/optimism/batcher/cli/Cli.java index ad3e93d9..eaa5e5d1 100644 --- a/hildr-batcher/src/main/java/io/optimism/batcher/cli/Cli.java +++ b/hildr-batcher/src/main/java/io/optimism/batcher/cli/Cli.java @@ -25,7 +25,7 @@ public class Cli implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(Cli.class); - @Option(names = "--l1-rpc-url", required = true, description = "The base chain RPC URL") + @Option(names = "--l1-rpc-url", required = true, description = "The L1 chain RPC URL") String l1RpcUrl; @Option(names = "--l2-rpc-url", required = true, description = "The L2 engine RPC URL") diff --git a/hildr-batcher/src/main/java/io/optimism/batcher/exception/BatcherExecutionException.java b/hildr-batcher/src/main/java/io/optimism/batcher/exception/BatcherExecutionException.java index ae3a6048..bb34f9b2 100644 --- a/hildr-batcher/src/main/java/io/optimism/batcher/exception/BatcherExecutionException.java +++ b/hildr-batcher/src/main/java/io/optimism/batcher/exception/BatcherExecutionException.java @@ -1,7 +1,7 @@ package io.optimism.batcher.exception; /** - * The execution exception of Bathcer. + * The bathcer execution exception. * * @author thinkAfCod * @since 0.1.1 diff --git a/hildr-batcher/src/main/java/io/optimism/type/RollupConfigRes.java b/hildr-batcher/src/main/java/io/optimism/type/RollupConfigRes.java index fcdfc821..a4286445 100644 --- a/hildr-batcher/src/main/java/io/optimism/type/RollupConfigRes.java +++ b/hildr-batcher/src/main/java/io/optimism/type/RollupConfigRes.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023 q315xia@163.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - package io.optimism.type; import org.web3j.protocol.core.Response; diff --git a/hildr-node/build.gradle b/hildr-node/build.gradle index e5a9b6f1..0b6a6a5f 100644 --- a/hildr-node/build.gradle +++ b/hildr-node/build.gradle @@ -22,7 +22,7 @@ plugins { // id 'signing' } -group 'me.grapebaba' +group 'io.optimism' version '0.4.0' repositories { @@ -74,12 +74,6 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14") implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14") - implementation('org.web3j:core:4.11.2') { - exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' - exclude group: 'com.squareup.okhttp3', module: 'okhttp' - exclude group: 'com.squareup.okhttp3', module: 'logging-interceptor' - } - implementation('net.osslabz.evm:evm-abi-decoder:0.0.6') implementation 'com.github.gestalt-config:gestalt-core:0.20.4' implementation 'com.github.gestalt-config:gestalt-toml:0.20.4' diff --git a/hildr-proposer/build.gradle b/hildr-proposer/build.gradle new file mode 100644 index 00000000..efb6da71 --- /dev/null +++ b/hildr-proposer/build.gradle @@ -0,0 +1,246 @@ +plugins { + id 'java' + id 'application' + id "jacoco" + id "com.diffplug.spotless" version "6.22.0" + id "net.ltgt.errorprone" version "3.1.0" + id 'org.graalvm.buildtools.native' version '0.9.28' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +group = 'io.optimism' +version = '0.4.0' + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() + maven { + url "https://artifacts.consensys.net/public/teku/maven/" + } + maven { + url "https://dl.cloudsmith.io/public/libp2p/jvm-libp2p/maven/" + } + maven { + url "https://hyperledger.jfrog.io/artifactory/besu-maven/" + } + maven { + url "https://artifacts.consensys.net/public/maven/maven/" + } + maven { url "https://jitpack.io" } + google() +} + +application { + // Define the main class for the application. + mainClass = 'io.optimism.proposer.HildrProposer' +} + +tasks.withType(JavaCompile).configureEach { + options.annotationProcessorPath = configurations.annotationProcessor + options.compilerArgs += "--enable-preview" + options.compilerArgs += "-Xlint:preview" + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + +tasks.withType(JavaCompile).configureEach { + options.annotationProcessorPath = configurations.annotationProcessor + options.compilerArgs += "--enable-preview" + options.compilerArgs += "-Xlint:preview" + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + +tasks.withType(Test).configureEach { + jvmArgs += "--enable-preview" +} + +tasks.withType(JavaExec).configureEach { + jvmArgs += "--enable-preview" +} + +dependencies { + implementation project(':hildr-utilities') + implementation 'com.github.gestalt-config:gestalt-core:0.20.4' + implementation 'com.github.gestalt-config:gestalt-toml:0.20.4' + + implementation 'com.fasterxml.jackson:jackson-bom:2.15.2' + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-toml' + implementation 'org.jctools:jctools-core:4.0.1' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //jsonrpc + implementation('io.vertx:vertx-auth-jwt:4.4.2') + implementation('io.vertx:vertx-core:4.4.2') + implementation('io.vertx:vertx-web:4.4.2') + + implementation 'io.micrometer:micrometer-registry-prometheus:1.11.0' + implementation platform('io.micrometer:micrometer-tracing-bom:1.1.1') + implementation 'io.micrometer:micrometer-tracing' + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + + // Logback + implementation 'ch.qos.logback:logback-core:1.4.12' + implementation 'ch.qos.logback:logback-classic:1.4.14' + implementation 'org.slf4j:slf4j-api:2.0.7' + + implementation platform("io.opentelemetry:opentelemetry-bom-alpha:1.26.0-alpha") + // OpenTelemetry core + implementation(platform("io.opentelemetry:opentelemetry-bom:1.26.0")) + implementation 'io.opentelemetry:opentelemetry-api' + implementation 'io.opentelemetry:opentelemetry-sdk' + implementation 'io.opentelemetry:opentelemetry-sdk-logs' + + // OpenTelemetry log4j appenders + implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:1.26.0-alpha") + runtimeOnly 'io.opentelemetry.instrumentation:opentelemetry-logback-mdc-1.0' + + implementation('io.tmio:tuweni-crypto:2.4.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + + implementation 'info.picocli:picocli:4.7.3' + annotationProcessor 'info.picocli:picocli-codegen:4.7.3' + + implementation 'io.tmio:tuweni-crypto:2.4.2' + + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + // https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite-api + testImplementation 'org.junit.platform:junit-platform-suite-api:1.9.1' + // https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite-engine + testRuntimeOnly 'org.junit.platform:junit-platform-suite-engine:1.9.1' + + testRuntimeOnly 'org.junit.platform:junit-platform-reporting:1.9.1' + + testImplementation 'org.mockito:mockito-junit-jupiter:2.19.0' + testImplementation("com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14") + + errorprone("com.google.errorprone:error_prone_core:2.18.0") +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } + finalizedBy jacocoTestReport +} + +jacoco { + toolVersion = "0.8.12" +} + +jacocoTestReport { + dependsOn test + + reports { + csv.required = true + } +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0 + } + } + } +} + +spotless { + // optional: limit format enforcement to just the files changed by this feature branch +// ratchetFrom 'origin/main' + + format 'misc', { + // define the files to apply `misc` to + target '*.gradle', '*.md', '.gitignore' + + // define the steps to apply to those files + trimTrailingWhitespace() + indentWithTabs() // or spaces. Takes an integer argument if you don't like 4 + endWithNewline() + } + java { + // Use the default importOrder configuration + + // don't need to set target, it is inferred from java + + // apply a specific flavor of google-java-format + palantirJavaFormat('2.38.0') + // fix formatting of type annotations + formatAnnotations() + + importOrder() + + removeUnusedImports() + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} + +check { + dependsOn += jacocoTestCoverageVerification +// dependsOn += integrationTest +} + +tasks.withType(Test).configureEach { + def outputDir = reports.junitXml.outputLocation + jvmArgumentProviders << ({ + [ + "-Djunit.platform.reporting.open.xml.enabled=true", + "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}", + "--enable-preview" + ] + } as CommandLineArgumentProvider) +} + +java { + withJavadocJar() + withSourcesJar() +} + + +javadoc { + if (JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } + options.addBooleanOption('-enable-preview', true) + options.addStringOption('-release', '21') +} + +jar { + enabled = false + manifest { + attributes "Main-Class": "io.optimism.proposer.HildrProposer" + attributes "Multi-Release": "true" + } + dependsOn(shadowJar) +} + +shadowJar { + archiveFileName = "${project.name}-${project.version}.jar" + transform(com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer) +} + +nativeCompile { + enabled = false +} diff --git a/hildr-proposer/src/main/java/io/optimism/proposer/HildrProposer.java b/hildr-proposer/src/main/java/io/optimism/proposer/HildrProposer.java new file mode 100644 index 00000000..4e4052e1 --- /dev/null +++ b/hildr-proposer/src/main/java/io/optimism/proposer/HildrProposer.java @@ -0,0 +1,26 @@ +package io.optimism.proposer; + +import io.optimism.proposer.cli.Cli; +import picocli.CommandLine; + +/** + * Batcher main method. + * + * @author thinkAfCod + * @since 0.1.1 + */ +public class HildrProposer { + + /** Constructor of HildrBatcher. */ + public HildrProposer() {} + + /** + * Main method of HildrBatcher. + * + * @param args Starts arguments + */ + public static void main(String[] args) { + int exitCode = new CommandLine(new Cli()).execute(args); + System.exit(exitCode); + } +} diff --git a/hildr-proposer/src/main/java/io/optimism/proposer/L2OutputSubmitter.java b/hildr-proposer/src/main/java/io/optimism/proposer/L2OutputSubmitter.java new file mode 100644 index 00000000..7eac02d0 --- /dev/null +++ b/hildr-proposer/src/main/java/io/optimism/proposer/L2OutputSubmitter.java @@ -0,0 +1,232 @@ +package io.optimism.proposer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.AbstractExecutionThreadService; +import io.optimism.proposer.config.Config; +import io.optimism.proposer.exception.OutputSubmitterExecution; +import io.optimism.utilities.rpc.Web3jProvider; +import io.optimism.utilities.rpc.response.OutputRootResult; +import io.optimism.utilities.rpc.response.SyncStatusResult; +import io.optimism.utilities.telemetry.TracerTaskWrapper; +import io.optimism.utilities.web3j.Web3jUtil; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.StructuredTaskScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.abi.FunctionEncoder; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.Web3jService; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.protocol.core.methods.response.Web3Sha3; +import org.web3j.utils.Numeric; + +/** + * L2OutputSubmitter submits L2 output. + * + * @author thinkAfCod + * @since 0.1.1 + */ +public class L2OutputSubmitter extends AbstractExecutionThreadService { + + private static final Logger LOGGER = LoggerFactory.getLogger(L2OutputSubmitter.class); + + private final Config config; + private final Web3j l1Client; + private final Web3j l2Client; + private final Credentials l2From; + private final Web3jService rollUpClient; + private final ObjectMapper mapper; + + private BigInteger nonce; + private volatile boolean isShutdownTriggered; + + private void tryOutputSubmit() { + var syncStatus = this.syncStatus(); + var currentBlockNum = Boolean.TRUE.equals(config.allowNonFinalized()) + ? syncStatus.safeL2().number() + : syncStatus.finalizedL2().number(); + var output = this.fetchOutputInfo(currentBlockNum); + if (output != null) { + this.sendOutputTx(currentBlockNum, output, syncStatus); + } + } + + private OutputRootResult fetchOutputInfo(BigInteger currentBlockNum) { + BigInteger nextCheckPointBlockNum = this.nextCheckPointBlock(); + if (currentBlockNum.compareTo(nextCheckPointBlockNum) < 0) { + return null; + } + return this.outputAtBlock(nextCheckPointBlockNum); + } + + private Object sendOutputTx( + final BigInteger curBlock, final OutputRootResult output, final SyncStatusResult status) { + this.waitL1Head(status.headL1().number().add(BigInteger.ONE)); + Function proposeL2OutputFn = null; + try { + proposeL2OutputFn = FunctionEncoder.makeFunction( + "proposeL2Output", + List.of("bytes32", "uint256", "bytes32", "uint256"), + List.of( + Numeric.hexStringToByteArray(output.outputRoot()), + curBlock, + status.currentL1().hash(), + status.currentL1().number()), + List.of()); + } catch (ReflectiveOperationException e) { + throw new OutputSubmitterExecution(e); + } + + String fnData = FunctionEncoder.encode(proposeL2OutputFn); + + // todo parameter + RawTransaction tx = RawTransaction.createTransaction( + this.config.l2ChainId(), + this.getNonce(), + BigInteger.ZERO, + this.config.l2OutputOracleAddr(), + BigInteger.ZERO, + fnData, + BigInteger.ZERO, + BigInteger.ZERO); + var receipt = Web3jUtil.executeContractReturnReceipt(l2Client, tx, this.config.l2ChainId(), this.l2From); + Optional txOption = receipt.getTransactionReceipt(); + if (txOption.isPresent() && txOption.get().isStatusOK()) { + LOGGER.info( + "proposer tx successfully published: tx_hash = {}, l1BlockNum = {}, l1BlockHash = {}", + txOption.get().getTransactionHash(), + status.currentL1().number(), + status.currentL1().hash()); + } else { + LOGGER.error( + "proposer tx successfully published but reverted: tx_hash = {}", + txOption.map(TransactionReceipt::getTransactionHash).orElse(null)); + } + return null; + } + + private BigInteger getNonce() { + if (nonce == null) { + nonce = Web3jUtil.getTxCount(this.l2Client, this.l2From.getAddress()); + } else { + nonce = nonce.add(BigInteger.ONE); + } + return nonce; + } + + private void waitL1Head(final BigInteger headNum) { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + scope.fork(() -> { + BigInteger l1Head = BigInteger.ZERO; + do { + if (l1Head.compareTo(BigInteger.ZERO) == 0) { + Thread.sleep(this.config.pollInterval()); + } + var blockNumResp = l1Client.ethBlockNumber().sendAsync().get(); + if (blockNumResp != null) { + l1Head = blockNumResp.getBlockNumber(); + } + } while (l1Head.compareTo(headNum) <= 0); + return null; + }); + scope.join(); + } catch (InterruptedException e) { + throw new OutputSubmitterExecution(e); + } + } + + private BigInteger nextCheckPointBlock() { + // l2 oracle contract fetches next block number + Function nextBlockNumberFn = null; + try { + nextBlockNumberFn = + FunctionEncoder.makeFunction("nextBlockNumber", List.of(), List.of(), List.of("uint256")); + } catch (ReflectiveOperationException e) { + throw new OutputSubmitterExecution(e); + } + List resp = Web3jUtil.executeContract( + this.l2Client, this.l2From.getAddress(), this.config.l2OutputOracleAddr(), nextBlockNumberFn); + return resp.isEmpty() ? null : ((Uint256) resp.get(0)).getValue(); + } + + private OutputRootResult outputAtBlock(BigInteger blockNumber) { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var outputInfo = scope.fork(TracerTaskWrapper.wrap(() -> { + return new Request<>( + "optimism_outputAtBlock", + Collections.singletonList(blockNumber), + this.rollUpClient, + Web3Sha3.class) + .send() + .getResult(); + })); + scope.join(); + scope.throwIfFailed(); + return mapper.readValue(outputInfo.get(), OutputRootResult.class); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new OutputSubmitterExecution(e); + } + } + + private SyncStatusResult syncStatus() { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var outputInfo = scope.fork(TracerTaskWrapper.wrap(() -> { + return new Request<>("optimism_syncStatus", List.of(), this.rollUpClient, Web3Sha3.class) + .send() + .getResult(); + })); + scope.join(); + scope.throwIfFailed(); + return mapper.readValue(outputInfo.get(), SyncStatusResult.class); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new OutputSubmitterExecution(e); + } + } + + /** + * The L2OutputSubmitter constructor. + * @param config The proposer config. + */ + public L2OutputSubmitter(Config config) { + this.config = config; + this.l1Client = Web3jProvider.createClient(config.l1RpcUrl()); + this.l2Client = Web3jProvider.createClient(config.l2RpcUrl()); + this.l2From = Credentials.create(config.l2Signer()); + var tuple = Web3jProvider.create(config.rollupRpc()); + this.rollUpClient = tuple.component2(); + this.mapper = new ObjectMapper(); + } + + @Override + protected void run() throws Exception { + try { + while (isRunning() && !this.isShutdownTriggered) { + this.tryOutputSubmit(); + Thread.sleep(config.pollInterval()); + } + } catch (InterruptedException e) { + throw new OutputSubmitterExecution(""); + } + } + + @Override + protected void shutDown() throws Exception { + super.shutDown(); + } + + @Override + protected void triggerShutdown() { + this.isShutdownTriggered = true; + } +} diff --git a/hildr-proposer/src/main/java/io/optimism/proposer/cli/Cli.java b/hildr-proposer/src/main/java/io/optimism/proposer/cli/Cli.java new file mode 100644 index 00000000..7b5b35f3 --- /dev/null +++ b/hildr-proposer/src/main/java/io/optimism/proposer/cli/Cli.java @@ -0,0 +1,106 @@ +package io.optimism.proposer.cli; + +import io.optimism.proposer.L2OutputSubmitter; +import io.optimism.proposer.config.Config; +import io.optimism.utilities.telemetry.Logging; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; +import sun.misc.Signal; + +/** + * Proposer CLI handler. + * + * @author thinkAfCod + * @since 0.1.1 + */ +public class Cli implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(Cli.class); + + @CommandLine.Option(names = "--enable-metrics", required = false, description = "Enable metrics server") + boolean enableMetrics; + + @CommandLine.Option(names = "--l1-rpc-url", required = true, description = "The L1 chain RPC URL") + String l1RpcUrl; + + @CommandLine.Option(names = "--l2-rpc-url", required = true, description = "The L2 engine RPC URL") + String l2RpcUrl; + + @CommandLine.Option(names = "--rollup-rpc-url", required = true, description = "The rollup node RPC URL") + String rollupRpcUrl; + + @CommandLine.Option(names = "--l2-chainId", required = true, description = "The L2 chain ID") + Long l2ChainId; + + @CommandLine.Option(names = "--l2-signer", required = true, description = "The L2 chain private key") + String l2Signer; + + @CommandLine.Option(names = "--l2oo-address", description = "The L2 output oracle contract address") + String l2OutputOracleAddr; + + @CommandLine.Option(names = "--l2dgf-address", description = "The L2 dispute game factory contract address") + String dgfContractAddr; + + @CommandLine.Option(names = "--poll-interval", description = "How frequently to poll L2 for new blocks") + Long pollInterval; + + @CommandLine.Option( + names = "--network-timeout", + defaultValue = "300", + description = "How frequently to poll L2 for new blocks") + Long networkTimeout; + + @CommandLine.Option( + names = "--allow-non-finalized", + defaultValue = "false", + description = "Allow the proposer to submit proposals for L2 blocks derived from non-finalized L1 blocks") + boolean allowNonFinalized; + + /** + * The proposer CLI constructor. + */ + public Cli() {} + + @Override + public void run() { + + // listen close signal + Signal.handle(new Signal("INT"), sig -> System.exit(0)); + Signal.handle(new Signal("TERM"), sig -> System.exit(0)); + + var tracer = Logging.INSTANCE.getTracer("hildr-proposer-cli"); + var span = tracer.nextSpan().name("proposer-submitter").start(); + try (var unused = tracer.withSpan(span)) { + // if (this.enableMetrics) { + // // todo start metrics server + // } + // start l2 output submitter + var submitter = new L2OutputSubmitter(optionToConfig()); + submitter.startAsync().awaitTerminated(); + } catch (Exception e) { + LOGGER.error("hildr proposer: ", e); + throw new RuntimeException(e); + } finally { + if (this.enableMetrics) { + LOGGER.info("stop metrics"); + // todo stop metrics server + } + span.end(); + } + } + + private Config optionToConfig() { + return new Config( + this.l2ChainId, + this.l1RpcUrl, + this.l2RpcUrl, + this.rollupRpcUrl, + this.l2Signer, + this.l2OutputOracleAddr, + this.dgfContractAddr, + this.pollInterval, + this.networkTimeout, + this.allowNonFinalized); + } +} diff --git a/hildr-proposer/src/main/java/io/optimism/proposer/config/Config.java b/hildr-proposer/src/main/java/io/optimism/proposer/config/Config.java new file mode 100644 index 00000000..c0422ec5 --- /dev/null +++ b/hildr-proposer/src/main/java/io/optimism/proposer/config/Config.java @@ -0,0 +1,30 @@ +package io.optimism.proposer.config; + +/** + * The proposer config. + * + * @param l2ChainId The chain ID for L2. + * @param l1RpcUrl The HTTP URL for L1. + * @param l2RpcUrl The HTTP URL for L2. + * @param rollupRpc The HTTP URL for the rollup node. + * @param l2Signer The signer for L2. + * @param l2OutputOracleAddr The L2OutputOracle contract address. + * @param dgfContractAddr The DisputeGameFactory contract address. + * @param pollInterval The delay between querying L2 for more transaction and creating a new batch. + * @param networkTimeout network timeout. + * @param allowNonFinalized set to true to propose outputs for L2 blocks derived from non-finalized + * L1 data + * @author thinkAfCod + * @since 0.1.1 + */ +public record Config( + Long l2ChainId, + String l1RpcUrl, + String l2RpcUrl, + String rollupRpc, + String l2Signer, + String l2OutputOracleAddr, + String dgfContractAddr, + Long pollInterval, + Long networkTimeout, + Boolean allowNonFinalized) {} diff --git a/hildr-proposer/src/main/java/io/optimism/proposer/exception/OutputSubmitterExecution.java b/hildr-proposer/src/main/java/io/optimism/proposer/exception/OutputSubmitterExecution.java new file mode 100644 index 00000000..dabdf41f --- /dev/null +++ b/hildr-proposer/src/main/java/io/optimism/proposer/exception/OutputSubmitterExecution.java @@ -0,0 +1,38 @@ +package io.optimism.proposer.exception; + +/** + * output submitter execution exception. + * + * @author thinkAfCod + * @since 0.1.1 + */ +public class OutputSubmitterExecution extends RuntimeException { + + /** + * Instantiates a new output execution exception. + * + * @param message the message + */ + public OutputSubmitterExecution(String message) { + super(message); + } + + /** + * Instantiates a new output execution exception. + * + * @param message the message + * @param cause the cause + */ + public OutputSubmitterExecution(String message, Throwable cause) { + super(message, cause); + } + + /** + * Instantiates a new output execution exception. + * + * @param cause the cause + */ + public OutputSubmitterExecution(Throwable cause) { + super(cause); + } +} diff --git a/hildr-proposer/src/main/resources/logback.xml b/hildr-proposer/src/main/resources/logback.xml new file mode 100644 index 00000000..0e2c6a65 --- /dev/null +++ b/hildr-proposer/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %msg%n + + + + + logs/app.log + + app-%d{yyyy-MM-dd HH}.gz + 30 + 3GB + + + %d{HH:mm:ss.SSS} %-5level %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %msg%n + + + + + + + + + + + diff --git a/hildr-utilities/src/main/java/io/optimism/utilities/encoding/TxDecoder.java b/hildr-utilities/src/main/java/io/optimism/utilities/encoding/TxDecoder.java index 665943a9..6987d38c 100644 --- a/hildr-utilities/src/main/java/io/optimism/utilities/encoding/TxDecoder.java +++ b/hildr-utilities/src/main/java/io/optimism/utilities/encoding/TxDecoder.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023 q315xia@163.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - package io.optimism.utilities.encoding; import io.optimism.type.DepositTransaction; diff --git a/hildr-utilities/src/main/java/io/optimism/utilities/rpc/response/OutputRootResult.java b/hildr-utilities/src/main/java/io/optimism/utilities/rpc/response/OutputRootResult.java new file mode 100644 index 00000000..b4b5d209 --- /dev/null +++ b/hildr-utilities/src/main/java/io/optimism/utilities/rpc/response/OutputRootResult.java @@ -0,0 +1,20 @@ +package io.optimism.utilities.rpc.response; + +import io.optimism.type.L2BlockRef; + +/** + * + * @param version + * @param outputRoot + * @param blockRef + * @param withdrawalStorageRoot + * @param stateRoot + * @param syncStatus + */ +public record OutputRootResult( + String version, + String outputRoot, + L2BlockRef blockRef, + String withdrawalStorageRoot, + String stateRoot, + SyncStatusResult syncStatus) {} diff --git a/hildr-utilities/src/main/java/io/optimism/utilities/rpc/response/SyncStatusResult.java b/hildr-utilities/src/main/java/io/optimism/utilities/rpc/response/SyncStatusResult.java new file mode 100644 index 00000000..d9970141 --- /dev/null +++ b/hildr-utilities/src/main/java/io/optimism/utilities/rpc/response/SyncStatusResult.java @@ -0,0 +1,38 @@ +package io.optimism.utilities.rpc.response; + +import io.optimism.type.L1BlockRef; +import io.optimism.type.L2BlockRef; + +/** + * The SyncStatusResult type. A snapshot of the driver. Values may be zeroed if not yet initialized. + * + * @param currentL1 The L1 block that the driver process is currently at in the inner-most stage. + * @param currentL1Finalized The L1 block that the driver process is currently accepting as + * finalized in the inner-most stage. + * @param headL1 The perceived head of the L1 chain, no confirmation distance. + * @param safeL1 The stored L1 safe block or an empty block reference if the L1 safe block has not + * been initialized yet. + * @param finalizedL1 The stored L1 finalized block or an empty block reference if the L1 finalized + * block has not been initialized yet. + * @param unsafeL2 The absolute tip of the L2 chain, + * @param safeL2 Points to the L2 block that was derived from the L1 chain. + * @param finalizedL2 Points to the L2 block that was derived fully from finalized L1 information, + * thus irreversible. + * @param unsafeL2SyncTarget Points to the first unprocessed unsafe L2 block. It may be zeroed if + * there is no targeted block. + * @param pendingSafeL2 Points to the L2 block processed from the batch, but not consolidated to + * the safe block yet. + * @author thinkAfCod + * @since 0.1.1 + */ +public record SyncStatusResult( + L1BlockRef currentL1, + L1BlockRef currentL1Finalized, + L1BlockRef headL1, + L1BlockRef safeL1, + L1BlockRef finalizedL1, + L1BlockRef unsafeL2, + L1BlockRef safeL2, + L1BlockRef finalizedL2, + L2BlockRef unsafeL2SyncTarget, + L1BlockRef pendingSafeL2) {} diff --git a/hildr-utilities/src/main/java/io/optimism/utilities/telemetry/MetricsSupplier.java b/hildr-utilities/src/main/java/io/optimism/utilities/telemetry/MetricsSupplier.java index db52a432..b84d0819 100644 --- a/hildr-utilities/src/main/java/io/optimism/utilities/telemetry/MetricsSupplier.java +++ b/hildr-utilities/src/main/java/io/optimism/utilities/telemetry/MetricsSupplier.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023 q315xia@163.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - package io.optimism.utilities.telemetry; import io.micrometer.core.instrument.Counter; diff --git a/hildr-utilities/src/main/java/io/optimism/utilities/web3j/Web3jCallException.java b/hildr-utilities/src/main/java/io/optimism/utilities/web3j/Web3jCallException.java new file mode 100644 index 00000000..74d247a1 --- /dev/null +++ b/hildr-utilities/src/main/java/io/optimism/utilities/web3j/Web3jCallException.java @@ -0,0 +1,38 @@ +package io.optimism.utilities.web3j; + +/** + * Web3jCallException class. Throws it when the call of web3j request task failed. + * + * @author thinkAfCod + * @since 0.1.1 + */ +public class Web3jCallException extends RuntimeException { + + /** + * Instantiates a new Web3jCallException. + * + * @param message the message + */ + public Web3jCallException(String message) { + super(message); + } + + /** + * Instantiates a new Web3jCallException. + * + * @param message the message + * @param cause the cause + */ + public Web3jCallException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Instantiates a new Web3jCallException. + * + * @param cause the cause + */ + public Web3jCallException(Throwable cause) { + super(cause); + } +} diff --git a/hildr-utilities/src/main/java/io/optimism/utilities/web3j/Web3jUtil.java b/hildr-utilities/src/main/java/io/optimism/utilities/web3j/Web3jUtil.java new file mode 100644 index 00000000..c169dbe7 --- /dev/null +++ b/hildr-utilities/src/main/java/io/optimism/utilities/web3j/Web3jUtil.java @@ -0,0 +1,168 @@ +package io.optimism.utilities.web3j; + +import io.optimism.utilities.telemetry.TracerTaskWrapper; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.StructuredTaskScope; +import org.web3j.abi.FunctionEncoder; +import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Hash; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.request.Transaction; +import org.web3j.protocol.core.methods.response.EthBlock; +import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.utils.Numeric; + +/** + * Web3jUtil provides some utility methods for web3j. + * + * @author thinkAfCod + * @since 0.1.1 + */ +public class Web3jUtil { + + private Web3jUtil() {} + + /** + * Get the transaction count of the given address. + * + * @param client the web3j client + * @param fromAddr the address + * @return the transaction count + */ + public static BigInteger getTxCount(final Web3j client, final String fromAddr) { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var countFuture = scope.fork(TracerTaskWrapper.wrap(() -> { + var countReq = client.ethGetTransactionCount(fromAddr, DefaultBlockParameterName.LATEST); + return countReq.send().getTransactionCount(); + })); + scope.join(); + scope.throwIfFailed(); + return countFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new Web3jCallException("get tx count failed", e); + } + } + + /** + * Get the transaction receipt of the given transaction hash. + * @param client the web3j client + * @param txHash the transaction hash + * @return the transaction receipt + */ + public static EthGetTransactionReceipt getTxReceipt(final Web3j client, final String txHash) { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var receiptFuture = + scope.fork(() -> client.ethGetTransactionReceipt(txHash).send()); + scope.join(); + scope.throwIfFailed(); + return receiptFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new Web3jCallException("failed to get TxReceipt", e); + } + } + + /** + * Poll the block by the given parameter. + * @param client the web3j client + * @param parameter the block parameter + * @param returnFullTransactionObjects whether to return full transaction objects + * @return the block + */ + public static EthBlock pollBlock( + final Web3j client, final DefaultBlockParameter parameter, final boolean returnFullTransactionObjects) { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var receiptFuture = scope.fork(() -> client.ethGetBlockByNumber(parameter, returnFullTransactionObjects) + .send()); + scope.join(); + scope.throwIfFailed(); + return receiptFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new Web3jCallException("failed to get block by number", e); + } + } + + /** + * Execute the contract. + * @param client the web3j client + * @param fromAddr the from address + * @param contractAddr the contract address + * @param function the function + * @return the list of type + */ + public static List executeContract( + final Web3j client, String fromAddr, String contractAddr, final Function function) { + String fnData = FunctionEncoder.encode(function); + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var fut = scope.fork(() -> { + return client.ethCall( + Transaction.createEthCallTransaction(fromAddr, contractAddr, fnData), + DefaultBlockParameterName.LATEST) + .send() + .getValue(); + }); + scope.join(); + scope.throwIfFailed(); + return FunctionReturnDecoder.decode(fut.get(), function.getOutputParameters()); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new Web3jCallException(e); + } + } + + /** + * Execute the contract and return the transaction receipt. + * @param client the web3j client + * @param tx the raw transaction + * @param chainId the chain ID + * @param credentials the credentials + * @return the transaction receipt + */ + public static EthGetTransactionReceipt executeContractReturnReceipt( + final Web3j client, final RawTransaction tx, final long chainId, final Credentials credentials) { + + byte[] sign = TransactionEncoder.signMessage(tx, chainId, credentials); + var signTxHexValue = Numeric.toHexString(sign); + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var fork = scope.fork(() -> { + EthSendTransaction txResp = + client.ethSendRawTransaction(signTxHexValue).send(); + if (txResp == null) { + throw new Web3jCallException("call contract tx response is null"); + } + if (txResp.hasError()) { + throw new Web3jCallException(String.format( + "call contract tx has error: code = %d, msg = %s, data = %s", + txResp.getError().getCode(), + txResp.getError().getMessage(), + txResp.getError().getData())); + } + String txHashLocal = Hash.sha3(signTxHexValue); + String txHashRemote = txResp.getTransactionHash(); + if (txHashLocal.equals(txHashRemote)) { + throw new Web3jCallException(String.format( + "tx has mismatch: txHashLocal = %s, txHashRemote = %s", txHashLocal, txHashRemote)); + } + return Web3jUtil.getTxReceipt(client, txHashLocal); + }); + scope.join(); + scope.throwIfFailed(); + return fork.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new Web3jCallException(e); + } + } +} diff --git a/settings.gradle b/settings.gradle index e9409086..8acae4f0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,4 +17,6 @@ pluginManagement { rootProject.name = 'hildr' include('hildr-node') include('hildr-batcher') -include('hildr-utilities') \ No newline at end of file +include('hildr-utilities') +include ('hildr-proposer') +