diff --git a/.github/workflows/infrastructure.yml b/.github/workflows/infrastructure.yml index 1eab7f6f3..085ca1d83 100644 --- a/.github/workflows/infrastructure.yml +++ b/.github/workflows/infrastructure.yml @@ -14,6 +14,7 @@ on: - stressnet-wl1 - manual-test - instant-bridge-test + - preconf-rpc-test default: 'devnet' all_targets: description: 'All Arch & Os Targets' diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..3988e40b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +TAG ?= $(shell git describe --tags || git rev-parse --short HEAD) + +.PHONY: docker +docker: + cd infrastructure/docker && TAG=$(TAG) docker buildx bake diff --git a/bridge/standard/pkg/transfer/transfer.go b/bridge/standard/pkg/transfer/transfer.go index e60e64493..a138d7644 100644 --- a/bridge/standard/pkg/transfer/transfer.go +++ b/bridge/standard/pkg/transfer/transfer.go @@ -34,7 +34,11 @@ type TransferStatus struct { Error error } -type Transfer struct { +type Transfer interface { + Do(ctx context.Context) <-chan TransferStatus +} + +type transfer struct { signer keysigner.KeySigner amount *big.Int destAddress common.Address @@ -54,7 +58,7 @@ func NewTransferToSettlement( l1RPCUrl string, l1ContractAddr common.Address, settlementContractAddr common.Address, -) (*Transfer, error) { +) (Transfer, error) { l1Client, err := ethclient.Dial(l1RPCUrl) if err != nil { return nil, fmt.Errorf("failed to dial l1 rpc: %s", err) @@ -88,7 +92,7 @@ func NewTransferToSettlement( return nil, fmt.Errorf("failed to create settlement filterer: %s", err) } - return &Transfer{ + return &transfer{ signer: signer, amount: amount, destAddress: destAddress, @@ -109,7 +113,7 @@ func NewTransferToL1( l1RPCUrl string, l1ContractAddr common.Address, settlementContractAddr common.Address, -) (*Transfer, error) { +) (Transfer, error) { l1Client, err := ethclient.Dial(l1RPCUrl) if err != nil { return nil, fmt.Errorf("failed to dial l1 rpc: %s", err) @@ -146,7 +150,7 @@ func NewTransferToL1( return nil, fmt.Errorf("failed to create settlement filterer: %s", err) } - return &Transfer{ + return &transfer{ amount: amount, destAddress: destAddress, signer: signer, @@ -159,7 +163,7 @@ func NewTransferToL1( }, nil } -func (t *Transfer) Do(ctx context.Context) <-chan TransferStatus { +func (t *transfer) Do(ctx context.Context) <-chan TransferStatus { statusChan := make(chan TransferStatus) go func() { defer close(statusChan) diff --git a/contracts/scripts/validator-registry/DeployForMockL1.s.sol b/contracts/scripts/validator-registry/DeployForMockL1.s.sol index d997031ec..5e0f7b257 100644 --- a/contracts/scripts/validator-registry/DeployForMockL1.s.sol +++ b/contracts/scripts/validator-registry/DeployForMockL1.s.sol @@ -34,7 +34,6 @@ contract DeployForMockL1 is Script { uint256 payoutPeriodBlocks = 200; address owner = msg.sender; - console.log("Deploying VanillaRegistry..."); address vanillaRegistryProxy = Upgrades.deployUUPSProxy( "VanillaRegistry.sol", abi.encodeCall( @@ -42,20 +41,18 @@ contract DeployForMockL1 is Script { (minStake, slashOracle, slashReceiver, unstakePeriodBlocks, payoutPeriodBlocks, owner) ) ); - console.log("VanillaRegistry deployed at:", vanillaRegistryProxy); + console.log("_VanillaRegistry:", vanillaRegistryProxy); VanillaRegistry vanillaRegistry = VanillaRegistry(payable(vanillaRegistryProxy)); address[] memory stakers = new address[](1); stakers[0] = owner; vanillaRegistry.whitelistStakers(stakers); - console.log("Deploying mock AVS and Middleware contracts..."); AlwaysFalseAVS mockAVS = new AlwaysFalseAVS(); AlwaysFalseMiddleware mockMiddleware = new AlwaysFalseMiddleware(); - console.log("Mock AVS deployed at:", address(mockAVS)); - console.log("Mock Middleware deployed at:", address(mockMiddleware)); + console.log("_MockAVS:", address(mockAVS)); + console.log("_MockMiddleware:", address(mockMiddleware)); - console.log("Deploying ValidatorOptInRouter..."); address routerProxy = Upgrades.deployUUPSProxy( "ValidatorOptInRouter.sol", abi.encodeCall( @@ -63,7 +60,7 @@ contract DeployForMockL1 is Script { (vanillaRegistryProxy, address(mockAVS), address(mockMiddleware), msg.sender) ) ); - console.log("ValidatorOptInRouter deployed at:", routerProxy); + console.log("ValidatorOptInRouter:", routerProxy); uint256 batchSize = 5; uint256 numKeys = 32; @@ -104,7 +101,6 @@ contract DeployForMockL1 is Script { pubkeysToRegister[31] = hex"ababbfe729893e69384ef1f32c7fa15902be6ace12aeaa21c56be726bc8c71e4e9b884735b82dbc619315752cffdb73e"; uint256 totalKeys = pubkeysToRegister.length; - console.log("Registering", totalKeys, "validators in batches of", batchSize); for (uint256 i = 0; i < totalKeys; i += batchSize) { uint256 currentBatchSize = batchSize; @@ -118,7 +114,6 @@ contract DeployForMockL1 is Script { uint256 batchStake = minStake * currentBatchSize; vanillaRegistry.stake{value: batchStake}(batchKeys); } - console.log("Successfully registered all validators"); vm.stopBroadcast(); } } diff --git a/go.work.sum b/go.work.sum index afe9f6f34..e7b42451b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1796,6 +1796,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= diff --git a/infrastructure/docker/Dockerfile.bidderemulator b/infrastructure/docker/Dockerfile.bidderemulator index e01396237..1a8d698f9 100644 --- a/infrastructure/docker/Dockerfile.bidderemulator +++ b/infrastructure/docker/Dockerfile.bidderemulator @@ -1,22 +1,9 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY p2p/go.mod p2p/go.sum /app/p2p/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ - -RUN cd /app/p2p && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download - -COPY . . - -RUN go build -o /app/bidder-emulator ./p2p/integrationtest/real-bidder - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/bidder-emulator /usr/local/bin/bidder-emulator -COPY --from=builder /app/p2p/integrationtest/real-bidder/entrypoint.sh entrypoint.sh +COPY --from=builder_ctx /go/bin/real-bidder /usr/local/bin/bidder-emulator +COPY --from=builder_ctx /scripts/bidder-emulator-entrypoint.sh entrypoint.sh + +EXPOSE 8080 ENTRYPOINT ["./entrypoint.sh"] diff --git a/infrastructure/docker/Dockerfile.bridge b/infrastructure/docker/Dockerfile.bridge index 1b35a52ac..77dfabfc4 100644 --- a/infrastructure/docker/Dockerfile.bridge +++ b/infrastructure/docker/Dockerfile.bridge @@ -1,21 +1,8 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY bridge/standard/go.mod bridge/standard/go.sum /app/bridge/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ - -RUN cd /app/bridge && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download - -COPY . . - -RUN go build -o /app/mev-commit-bridge ./bridge/cmd - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/mev-commit-bridge /usr/local/bin/mev-commit-bridge +COPY --from=builder_ctx /go/bin/relayer /usr/local/bin/mev-commit-bridge + +EXPOSE 8080 ENTRYPOINT ["mev-commit-bridge"] diff --git a/infrastructure/docker/Dockerfile.builder b/infrastructure/docker/Dockerfile.builder new file mode 100644 index 000000000..d76f6c8a8 --- /dev/null +++ b/infrastructure/docker/Dockerfile.builder @@ -0,0 +1,58 @@ +FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS build + +WORKDIR /ws + +COPY go.work go.work.sum ./ + +COPY contracts-abi/go.mod contracts-abi/go.sum ./contracts-abi/ +COPY p2p/go.mod p2p/go.sum ./p2p/ +COPY oracle/go.mod oracle/go.sum ./oracle/ +COPY testing/go.mod testing/go.sum ./testing/ +COPY tools/go.mod tools/go.sum ./tools/ +COPY x/go.mod x/go.sum ./x/ +COPY bridge/standard/go.mod bridge/standard/go.sum ./bridge/standard/ +COPY cl/go.mod cl/go.sum ./cl/ +COPY infrastructure/tools/keystore-generator/go.mod infrastructure/tools/keystore-generator/go.sum ./infrastructure/tools/keystore-generator/ + +COPY p2p/integrationtest/real-bidder/entrypoint.sh /scripts/bidder-emulator-entrypoint.sh +COPY p2p/integrationtest/provider/entrypoint.sh /scripts/provider-emulator-entrypoint.sh + +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + go work sync && \ + go mod download all + +COPY . . + +ARG TARGETS="./oracle/cmd \ + ./p2p/cmd \ + ./bridge/standard/cmd/relayer \ + ./bridge/standard/cmd/emulator \ + ./infrastructure/tools/keystore-generator \ + ./testing/cmd \ + ./tools/preconf-rpc \ + ./tools/beacon-emulator \ + ./tools/dashboard \ + ./tools/bidder-cli \ + ./tools/bls-signer \ + ./tools/l1-transaction-emulator \ + ./tools/relay-emulator \ + ./tools/validators-monitor \ + ./tools/points-service \ + ./p2p/integrationtest/real-bidder \ + ./p2p/integrationtest/provider" + +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + set -e; \ + for path in ${TARGETS}; do \ + # If the last element is literally "cmd", use the directory above it + bn=$(basename "$path"); \ + if [ "$bn" = "cmd" ]; then \ + name=$(basename "$(dirname "$path")"); \ + else \ + name=$bn; \ + fi; \ + echo "→ building $path as /go/bin/$name"; \ + CGO_ENABLED=0 go build -o "/go/bin/$name" "$path"; \ + done diff --git a/infrastructure/docker/Dockerfile.dashboard b/infrastructure/docker/Dockerfile.dashboard index 7000cc4ce..57d3948ab 100644 --- a/infrastructure/docker/Dockerfile.dashboard +++ b/infrastructure/docker/Dockerfile.dashboard @@ -1,21 +1,8 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY tools/go.mod tools/go.sum /app/dashboard/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ - -RUN cd /app/tools && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download - -COPY . . - -RUN go build -o /app/mev-commit-dashboard ./tools/dashboard - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/mev-commit-dashboard /usr/local/bin/mev-commit-dashboard +COPY --from=builder_ctx /go/bin/dashboard /usr/local/bin/mev-commit-dashboard + +EXPOSE 8080 ENTRYPOINT ["mev-commit-dashboard"] diff --git a/infrastructure/docker/Dockerfile.l1transactor b/infrastructure/docker/Dockerfile.l1transactor index 6097474af..664e6c930 100644 --- a/infrastructure/docker/Dockerfile.l1transactor +++ b/infrastructure/docker/Dockerfile.l1transactor @@ -1,25 +1,6 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY tools/go.mod tools/go.sum /app/tools/ -COPY p2p/go.mod p2p/go.sum /app/p2p/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ -COPY bridge/standard/go.mod bridge/standard/go.sum /app/bridge/standard/ - -RUN cd /app/tools && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download -RUN cd /app/p2p && go mod download -RUN cd /app/bridge/standard && go mod download - -COPY . . - -RUN go build -o /app/l1-transactor ./tools/l1-transaction-emulator - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/l1-transactor /usr/local/bin/l1-transactor +COPY --from=builder_ctx /go/bin/l1-transaction-emulator /usr/local/bin/l1-transactor ENTRYPOINT ["l1-transactor"] diff --git a/infrastructure/docker/Dockerfile.oracle b/infrastructure/docker/Dockerfile.oracle index 981818269..599049b64 100644 --- a/infrastructure/docker/Dockerfile.oracle +++ b/infrastructure/docker/Dockerfile.oracle @@ -1,21 +1,8 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY oracle/go.mod oracle/go.sum /app/oracle/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ - -RUN cd /app/oracle && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download - -COPY . . - -RUN go build -o /app/mev-commit-oracle ./oracle/cmd - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/mev-commit-oracle /usr/local/bin/mev-commit-oracle +COPY --from=builder_ctx /go/bin/oracle /usr/local/bin/mev-commit-oracle + +EXPOSE 8080 ENTRYPOINT ["mev-commit-oracle", "start"] diff --git a/infrastructure/docker/Dockerfile.p2p b/infrastructure/docker/Dockerfile.p2p index c57d9eea0..dcabb97d8 100644 --- a/infrastructure/docker/Dockerfile.p2p +++ b/infrastructure/docker/Dockerfile.p2p @@ -1,21 +1,8 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY p2p/go.mod p2p/go.sum /app/p2p/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ - -RUN cd /app/p2p && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download - -COPY . . - -RUN go build -o /app/mev-commit ./p2p/cmd - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/mev-commit /usr/local/bin/mev-commit +COPY --from=builder_ctx /go/bin/p2p /usr/local/bin/mev-commit + +EXPOSE 13522 13523 13524 ENTRYPOINT ["mev-commit"] diff --git a/infrastructure/docker/Dockerfile.provideremulator b/infrastructure/docker/Dockerfile.provideremulator index 5ff3e86f7..ffc43fa90 100644 --- a/infrastructure/docker/Dockerfile.provideremulator +++ b/infrastructure/docker/Dockerfile.provideremulator @@ -1,22 +1,9 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY p2p/go.mod p2p/go.sum /app/p2p/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ - -RUN cd /app/p2p && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download - -COPY . . - -RUN go build -o /app/provider-emulator ./p2p/integrationtest/provider - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/provider-emulator /usr/local/bin/provider-emulator -COPY --from=builder /app/p2p/integrationtest/provider/entrypoint.sh entrypoint.sh +COPY --from=builder_ctx /go/bin/provider /usr/local/bin/provider-emulator +COPY --from=builder_ctx /scripts/provider-emulator-entrypoint.sh entrypoint.sh + +EXPOSE 8080 ENTRYPOINT ["./entrypoint.sh"] diff --git a/infrastructure/docker/Dockerfile.relayemulator b/infrastructure/docker/Dockerfile.relayemulator index 4f096e6bd..2d10f69fa 100644 --- a/infrastructure/docker/Dockerfile.relayemulator +++ b/infrastructure/docker/Dockerfile.relayemulator @@ -1,25 +1,8 @@ -FROM golang:1.23.0-alpine AS builder - -WORKDIR /app - -COPY tools/go.mod tools/go.sum /app/tools/ -COPY p2p/go.mod p2p/go.sum /app/p2p/ -COPY x/go.mod x/go.sum /app/x/ -COPY contracts-abi/go.mod contracts-abi/go.sum /app/contracts-abi/ -COPY bridge/standard/go.mod bridge/standard/go.sum /app/bridge/standard/ - -RUN cd /app/tools && go mod download -RUN cd /app/x && go mod download -RUN cd /app/contracts-abi && go mod download -RUN cd /app/bridge/standard && go mod download -RUN cd /app/p2p && go mod download - -COPY . . - -RUN go build -o /app/relay-emulator ./tools/relay-emulator - +# syntax=docker/dockerfile:1.4 FROM alpine:3.10 -COPY --from=builder /app/relay-emulator /usr/local/bin/relay-emulator +COPY --from=builder_ctx /go/bin/relay-emulator /usr/local/bin/relay-emulator + +EXPOSE 8080 ENTRYPOINT ["relay-emulator"] diff --git a/infrastructure/docker/Dockerfile.rpc b/infrastructure/docker/Dockerfile.rpc new file mode 100644 index 000000000..6e4dcb8a6 --- /dev/null +++ b/infrastructure/docker/Dockerfile.rpc @@ -0,0 +1,8 @@ +# syntax=docker/dockerfile:1.4 +FROM alpine:3.10 + +COPY --from=builder_ctx /go/bin/preconf-rpc /usr/local/bin/preconf-rpc + +EXPOSE 8080 + +ENTRYPOINT ["preconf-rpc"] diff --git a/infrastructure/docker/docker-bake.hcl b/infrastructure/docker/docker-bake.hcl new file mode 100644 index 000000000..e8e87d93a --- /dev/null +++ b/infrastructure/docker/docker-bake.hcl @@ -0,0 +1,92 @@ +variable "TAG" { default = "dev" } + +target "mev-commit-builder" { + context = "../../" + dockerfile = "infrastructure/docker/Dockerfile.builder" +} + +target "mev-commit-oracle" { + context = "./" + dockerfile = "Dockerfile.oracle" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/mev-commit-oracle:${TAG}"] +} + +target "mev-commit" { + context = "./" + dockerfile = "Dockerfile.p2p" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/mev-commit:${TAG}"] +} + +target "mev-commit-bridge" { + context = "./" + dockerfile = "Dockerfile.bridge" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/mev-commit-bridge:${TAG}"] +} + +target "mev-commit-dashboard" { + context = "./" + dockerfile = "Dockerfile.dashboard" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/mev-commit-dashboard:${TAG}"] +} + +target "preconf-rpc" { + context = "./" + dockerfile = "Dockerfile.rpc" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/preconf-rpc:${TAG}"] +} + +target "bidder-emulator" { + context = "./" + dockerfile = "Dockerfile.bidderemulator" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/bidder-emulator:${TAG}"] +} + +target "provider-emulator" { + context = "./" + dockerfile = "Dockerfile.provideremulator" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/provider-emulator:${TAG}"] +} + +target "relay-emulator" { + context = "./" + dockerfile = "Dockerfile.relayemulator" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/relay-emulator:${TAG}"] +} + +target "l1-transactor" { + context = "./" + dockerfile = "Dockerfile.l1transactor" + contexts = { + builder_ctx = "target:mev-commit-builder" + } + tags = ["ghcr.io/primev/l1-transactor:${TAG}"] +} + +group "default" { + targets = ["mev-commit-builder", "mev-commit-oracle", "mev-commit", "mev-commit-bridge", "mev-commit-dashboard", "preconf-rpc", "bidder-emulator", "provider-emulator", "relay-emulator", "l1-transactor"] +} + diff --git a/infrastructure/nomad/playbooks/templates/jobs/contracts-deployer.nomad.j2 b/infrastructure/nomad/playbooks/templates/jobs/contracts-deployer.nomad.j2 index 941f663d1..a84fc5259 100644 --- a/infrastructure/nomad/playbooks/templates/jobs/contracts-deployer.nomad.j2 +++ b/infrastructure/nomad/playbooks/templates/jobs/contracts-deployer.nomad.j2 @@ -5,19 +5,10 @@ job "{{ job.name }}" { group "{{ job.name }}-group" { count = {{ job.count }} - # This is a special case for CI because the runner machine is not very - # powerful and compiling and deploying contracts can take a long time. - {% if env == 'devenv' and profile == 'ci' %} update { - healthy_deadline = "20m" - progress_deadline = "30m" + healthy_deadline = "25m" + progress_deadline = "35m" } - {% else %} - update { - healthy_deadline = "10m" - progress_deadline = "15m" - } - {% endif %} {% if env == 'devenv' %} restart { @@ -231,8 +222,6 @@ job "{{ job.name }}" { | jq -c 'with_entries(select(.key | startswith("_") | not))' \ > local/${DEPLOY_TYPE}_addresses.json - forge clean --root "${CONTRACT_REPO_ROOT_PATH}" - {% if profile == 'testnet' %} export RPC_URL="{{ job.env['l1_rpc_url'] }}" export CHAIN_ID="17000" @@ -251,6 +240,43 @@ job "{{ job.name }}" { {{- end }} {% endraw %} + # Only deploy validator-registry when beacon-emulator is running + {%- raw %} + BEACON_EMULATOR_RUNNING=false + {{- range nomadService "beacon-emulator" }} + BEACON_EMULATOR_RUNNING=true + {{- end }} + {% endraw %} + + if [ "$BEACON_EMULATOR_RUNNING" = true ]; then + + forge clean --root "${CONTRACT_REPO_ROOT_PATH}" + + start_time=$(date +%s) + export DEPLOY_TYPE="validator-registry" + echo "Deploying ${DEPLOY_TYPE} contracts..." + LOGS="$(${CONTRACT_REPO_ROOT_PATH}/entrypoint.sh)" + + if [ $? -ne 0 ]; then + echo "Failed to deploy ${DEPLOY_TYPE} contracts!" + echo "${LOGS}" + exit 1 + fi + end_time=$(date +%s) + echo "${DEPLOY_TYPE} contracts deployed successfully in: $(date -ud @$((end_time - start_time)) +'%H:%M:%S')." + echo "${LOGS}" \ + | sed -n '/{.*}/p' \ + | jq -c 'reduce .logs[] as $item ({}; . + {($item | split(": ")[0]): ($item | split(": ")[1])})' \ + | jq -c 'with_entries(select(.key | startswith("_") | not))' \ + > local/${DEPLOY_TYPE}_addresses.json + else + echo "Skipping validator-registry deployment as beacon-emulator is not running" + fi + + {%- endif %} + + forge clean --root "${CONTRACT_REPO_ROOT_PATH}" + start_time=$(date +%s) export DEPLOY_TYPE="l1-gateway" echo "Deploying ${DEPLOY_TYPE} contracts..." @@ -286,51 +312,10 @@ job "{{ job.name }}" { | jq -c 'with_entries(select(.key | startswith("_") | not))' \ > local/${DEPLOY_TYPE}_addresses.json - # Only deploy validator-registry when beacon-emulator is running - {%- raw %} - BEACON_EMULATOR_RUNNING=false - {{- range nomadService "beacon-emulator" }} - BEACON_EMULATOR_RUNNING=true - {{- end }} - {% endraw %} - - if [ "$BEACON_EMULATOR_RUNNING" = true ]; then - - cd "${TMP_CONTRACTS_DIR}" - "${FORGE_BIN_PATH}" clean --root "${CONTRACT_REPO_ROOT_PATH}" - - start_time=$(date +%s) - export DEPLOY_TYPE="validator-registry" - echo "Deploying ${DEPLOY_TYPE} contracts..." - LOGS="$(./entrypoint.sh)" - - # Now cd back to previous directory again. - cd ../../ - - if [ $? -ne 0 ]; then - echo "Failed to deploy ${DEPLOY_TYPE} contracts!" - echo "${LOGS}" - exit 1 - fi - end_time=$(date +%s) - echo "${DEPLOY_TYPE} contracts deployed successfully in: $(date -ud @$((end_time - start_time)) +'%H:%M:%S')." - echo "${LOGS}" \ - | sed -n '/{.*}/p' \ - | jq -c 'reduce .logs[] as $item ({}; . + {($item | split(": ")[0]): ($item | split(": ")[1])})' \ - | jq -c 'with_entries(select(.key | startswith("_") | not))' \ - > local/${DEPLOY_TYPE}_addresses.json - - else - echo "Skipping validator-registry deployment as beacon-emulator is not running" - fi - - {%- endif %} - mkdir -p /local/data/www > /dev/null 2>&1 jq -s 'add' local/*_addresses.json > local/data/www/contracts.json exec python3 -m http.server {{ job.ports[0]['http']['static'] }} --directory /local/data/www - # endtodo EOH destination = "local/run.sh" change_mode = "noop" diff --git a/infrastructure/nomad/playbooks/templates/jobs/mev-commit-emulator.nomad.j2 b/infrastructure/nomad/playbooks/templates/jobs/mev-commit-emulator.nomad.j2 index 0f2379d82..2f46511d6 100644 --- a/infrastructure/nomad/playbooks/templates/jobs/mev-commit-emulator.nomad.j2 +++ b/infrastructure/nomad/playbooks/templates/jobs/mev-commit-emulator.nomad.j2 @@ -117,7 +117,7 @@ job "{{ job.name }}" { {% if job.target_type == 'bidder' %} -rpc-addr "${EMULATOR_L1_RPC_URL}" \ {% endif %} - {% if job.target_type == 'provider' and profile == 'instant-bridge' %} + {% if job.target_type == 'provider' and profile == 'preconf-rpc-test' %} -error-probability 0 \ {% endif %} -log-tags "${EMULATOR_LOG_TAGS}" \ diff --git a/infrastructure/nomad/playbooks/templates/jobs/mev-commit.nomad.j2 b/infrastructure/nomad/playbooks/templates/jobs/mev-commit.nomad.j2 index 3f2014235..7f6edf9b2 100644 --- a/infrastructure/nomad/playbooks/templates/jobs/mev-commit.nomad.j2 +++ b/infrastructure/nomad/playbooks/templates/jobs/mev-commit.nomad.j2 @@ -99,6 +99,11 @@ job "{{ job.name }}" { MEV_COMMIT_SETTLEMENT_WS_RPC_ENDPOINT="ws://{{ .Address}}:{{ .Port }}" {{- end }} {{- end }} + {{- range nomadService "mock-l1" }} + {{- if contains "http" .Tags }} + MEV_COMMIT_L1_RPC_URL="http://{{ .Address }}:{{ .Port }}" + {{- end }} + {{- end }} {{- range nomadService "beacon-emulator" }} {{- if contains "http" .Tags }} MEV_COMMIT_BEACON_API_URL="http://{{ .Address }}:{{ .Port }}" @@ -174,7 +179,7 @@ job "{{ job.name }}" { export MEV_COMMIT_ORACLE_ADDR="$(jq -r '.Oracle' ${CONTRACTS_FILE})" {{- range nomadService "beacon-emulator" }} {{ if contains "http" .Tags }} - export MEV_COMMIT_VALIDATOR_ROUTER_ADDR="0x251Fbc993f58cBfDA8Ad7b0278084F915aCE7fc3" + export MEV_COMMIT_VALIDATOR_ROUTER_ADDR="$(jq -r '.ValidatorOptInRouter' ${CONTRACTS_FILE})" {{ end }} {{- end }} {% endraw %} diff --git a/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 b/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 new file mode 100644 index 000000000..c184c8e28 --- /dev/null +++ b/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 @@ -0,0 +1,196 @@ +#jinja2: trim_blocks:True, lstrip_blocks:True +job "{{ job.name }}" { + datacenters = ["{{ datacenter }}"] + + group "{{ job.name }}-group" { + count = {{ job.count }} + + {% if env == 'devenv' %} + restart { + attempts = 0 + mode = "fail" + } + + reschedule { + attempts = 0 + unlimited = false + } + {% endif %} + + network { + mode = "bridge" + + dns { + servers = {{ (ansible_facts['dns']['nameservers'] + ['1.1.1.1']) | tojson }} + } + + {% for port_name, port_details in job.ports[0].items() %} + port "{{ port_name }}" { + {% if port_details.get('static') %} + static = {{ port_details['static'] }} + {% endif %} + {% if port_details.get('to') %} + to = {{ port_details['to'] }} + {% endif %} + } + {% endfor %} + } + + {% for port_name in job.ports[0] %} + service { + name = "{{ job.name }}" + port = "{{ port_name }}" + tags = ["{{ port_name }}"] + provider = "nomad" + } + {% endfor %} + + task "preconfrpc" { + driver = "exec" + + resources { + cpu = 4000 + memory = 4096 + } + + artifact { + source = "https://foundry.paradigm.xyz" + destination = "local/foundry.sh" + } + + {% if env != 'devenv' %} + artifact { + source = "https://primev-infrastructure-artifacts.s3.us-west-2.amazonaws.com/preconf-rpc_{{ version }}_Linux_{{ target_system_architecture }}.tar.gz" + } + {% else %} + artifact { + source = "http://{{ ansible_facts['default_ipv4']['address'] }}:1111/preconf-rpc_{{ version }}_Linux_{{ target_system_architecture }}.tar.gz" + } + {% endif %} + + template { + data = <<-EOH + XDG_CONFIG_HOME="local/.config" + PRECONF_RPC_LOG_LEVEL="{{ job.env.get('log-level', 'info') }}" + PRECONF_RPC_LOG_FMT="{{ job.env.get('log-format', 'json') }}" + PRECONF_RPC_LOG_TAGS="{{ 'service.name:' + job.name + '-{{ env "NOMAD_ALLOC_INDEX" }}' + ',service.version:' + version }}" + CONTRACTS_JSON_URL="{{ job.env.get('contracts_json_url', '') }}" + PRECONF_RPC_SETTLEMENT_RPC_URL="{{ job.env.get('settlement_rpc_url', '') }}" + {%- raw %} + PRECONF_RPC_KEYSTORE_DIR="/local/data-{{ env "NOMAD_ALLOC_INDEX" }}/keystore" + PRECONF_RPC_KEYSTORE_FILENAME="{{ with secret "secret/data/mev-commit" }}{{ .Data.data.preconf_rpc_keystore_filename }}{{ end }}" + PRECONF_RPC_KEYSTORE_PASSWORD="{{ with secret "secret/data/mev-commit" }}{{ .Data.data.preconf_rpc_keystore_password }}{{ end }}" + {{- range nomadService "mev-commit-geth-bootnode1" }} + {{- if contains "http" .Tags }} + PRECONF_RPC_SETTLEMENT_RPC_URL="http://{{ .Address }}:{{ .Port }}" + {{- end }} + {{- end }} + {{- range nomadService "{% endraw %}{{ job.target.name }}{% raw %}" }} + {{- if contains "rpc" .Tags }} + PRECONF_RPC_BIDDER_RPC_URL="{{ .Address }}:{{ .Port }}" + {{- end }} + {{- end }} + {% endraw %} + XDG_CONFIG_HOME="local/.config" + {% if profile == 'preconf-rpc-test' %} + {%- raw %} + {{- $secret := secret "secret/data/mev-commit" }} + CONTRACT_DEPLOYER_KEYSTORE_PATH="/local/data-{{ env "NOMAD_ALLOC_INDEX" }}/deployer_keystore" + CONTRACT_DEPLOYER_KEYSTORE_FILENAME="{{ $secret.Data.data.contract_deployer_keystore_filename }}" + CONTRACT_DEPLOYER_KEYSTORE_PASSWORD="{{ $secret.Data.data.contract_deployer_keystore_password }}" + {% endraw %} + {% endif %} + PRECONF_RPC_L1_RPC_URLS="{{ job.env['l1_rpc_urls'] }}" + CONTRACTS_PATH="local/contracts" + ARTIFACT_OUT_PATH="local" + EOH + destination = "secrets/.env" + env = true + } + + template { + data = <<-EOH + #!/usr/bin/env bash + + {% raw %} + {{- range nomadService "datadog-agent-logs-collector" }} + {{ if contains "tcp" .Tags }} + exec > >(nc {{ .Address }} {{ .Port }}) 2>&1 + {{ end }} + {{- end }} + mkdir -p "${PRECONF_RPC_KEYSTORE_DIR}" > /dev/null 2>&1 + {{- with secret "secret/data/mev-commit" }} + PRECONF_RPC_KEYSTORE_FILE="${PRECONF_RPC_KEYSTORE_DIR}/${PRECONF_RPC_KEYSTORE_FILENAME}" + echo '{{ .Data.data.preconf_rpc_keystore }}' > "${PRECONF_RPC_KEYSTORE_FILE}" + {{ end }} + {% endraw %} + + {% if profile == 'preconf-rpc-test' %} + mkdir -p "${CONTRACT_DEPLOYER_KEYSTORE_PATH}" > /dev/null 2>&1 + CONTRACT_DEPLOYER_KEYSTORE_FILE="${CONTRACT_DEPLOYER_KEYSTORE_PATH}/${CONTRACT_DEPLOYER_KEYSTORE_FILENAME}" + {%- raw %} + {{- $secret := secret "secret/data/mev-commit" }} + echo '{{ $secret.Data.data.contract_deployer_keystore }}' > "${CONTRACT_DEPLOYER_KEYSTORE_FILE}" + {%- endraw %} + {% endif %} + + {% raw %} + {{- range nomadService "contracts-deployer" }} + {{ if contains "http" .Tags }} + CONTRACTS_JSON_URL="http://{{ .Address }}:{{ .Port }}/contracts.json" + {{ end }} + {{- end }} + {% endraw %} + CONTRACTS_FILE="/local/contracts.json" + curl -s -o "${CONTRACTS_FILE}" "${CONTRACTS_JSON_URL}" + export PRECONF_RPC_SETTLEMENT_CONTRACT_ADDR="$(jq -r '.SettlementGateway' ${CONTRACTS_FILE})" + export PRECONF_RPC_L1_CONTRACT_ADDR="$(jq -r '.L1Gateway' ${CONTRACTS_FILE})" + + chmod +x local/foundry.sh && local/foundry.sh + chmod +x ${XDG_CONFIG_HOME}/.foundry/bin/foundryup + ${XDG_CONFIG_HOME}/.foundry/bin/foundryup 2>&1 + if [ $? -ne 0 ]; then + echo "Failed to install foundry tools" + exit 1 + fi + export PATH="${XDG_CONFIG_HOME}/.foundry/bin:$PATH" + {%- raw %} + {{- range nomadService "mock-l1" }} + {{- if contains "ws" .Tags }} + L1_RPC_URL="ws://{{ .Address}}:{{ .Port }}" + {{- end }} + {{- with secret "secret/data/mev-commit" }} + ADDRESS="$(cat "${PRECONF_RPC_KEYSTORE_FILE}" | jq -r '.address')" + {{ end }} + cast send \ + --keystore "${CONTRACT_DEPLOYER_KEYSTORE_FILE}" \ + --password "${CONTRACT_DEPLOYER_KEYSTORE_PASSWORD}" \ + --priority-gas-price 2000000000 \ + --gas-price 5000000000 \ + --value 100ether \ + --rpc-url "${L1_RPC_URL}" \ + "${ADDRESS}" + + if [ $? -eq 0 ]; then + echo "Funds successfully sent to: ${ADDRESS}" + else + echo "Failed to send funds to: ${ADDRESS}" + fi + {{- end }} + {% endraw %} + + chmod +x local/preconf-rpc + exec ./local/preconf-rpc + EOH + destination = "local/run.sh" + change_mode = "noop" + perms = "0755" + } + + config { + command = "bash" + args = ["-c", "exec local/run.sh"] + } + } + } +} diff --git a/infrastructure/nomad/playbooks/variables/profiles.yml b/infrastructure/nomad/playbooks/variables/profiles.yml index 295c3380c..fc2f0256c 100644 --- a/infrastructure/nomad/playbooks/variables/profiles.yml +++ b/infrastructure/nomad/playbooks/variables/profiles.yml @@ -42,6 +42,9 @@ artifacts: instant-bridge: &instant_bridge_artifact type: binary path: tools/instant-bridge + preconf-rpc: &preconf_rpc_artifact + type: binary + path: tools/preconf-rpc jobs: artifacts: &artifacts_job @@ -738,6 +741,24 @@ jobs: settlement_rpc_url: "{{ settlement_rpc_url if settlement_rpc_url is defined else '' }}" contracts_json_url: "{{ contracts_json_url if contracts_json_url is defined else '' }}" + preconf_rpc: &preconf_rpc_job + name: preconf-rpc + template: preconf-rpc.nomad.j2 + artifacts: + - *preconf_rpc_artifact + - keystores: + preconf_rpc_keystore: + count: 1 + target: *mev_commit_bidder_node1_job + ports: + - http: + to: 8080 + env: + l1_chain_id: "{{ environments[env].chain_id }}" + l1_rpc_urls: "{{ resolved_l1_rpc_urls }}" + settlement_rpc_url: "{{ settlement_rpc_url if settlement_rpc_url is defined else '' }}" + contracts_json_url: "{{ contracts_json_url if contracts_json_url is defined else '' }}" + profiles: ci: jobs: @@ -943,6 +964,34 @@ profiles: - *instant_bridge_job - *datadog_agent_metrics_collector_job + preconf-rpc-test: + jobs: + - *artifacts_job + - *datadog_agent_logs_collector_job + - *otel_collector_job + - *beacon_emulator_job + - *mock_l1_job + - *mev_commit_geth_bootnode1_job + - *mev_commit_geth_signer_node1_job + - *mev_commit_geth_member_node_job + - *relay_emulator_job + - *contracts_deployer_job + - *mev_commit_bridge_job + - *mev_commit_dashboard_job + - *mev_commit_bootnode1_job + - *mev_commit_provider_node1_job + - *mev_commit_provider_node1_funder_job + - *mev_commit_provider_node2_job + - *mev_commit_provider_node2_funder_job + - *mev_commit_provider_node3_job + - *mev_commit_provider_node3_funder_job + - *mev_commit_provider_emulator_nodes_job + - *mev_commit_oracle_job + - *mev_commit_bidder_node1_job + - *mev_commit_bidder_node1_funder_job + - *preconf_rpc_job + - *datadog_agent_metrics_collector_job + instant-bridge: jobs: - *artifacts_job diff --git a/p2p/pkg/node/node.go b/p2p/pkg/node/node.go index 7189c2ae3..18b2475e5 100644 --- a/p2p/pkg/node/node.go +++ b/p2p/pkg/node/node.go @@ -191,6 +191,13 @@ func NewNode(opts *Options) (*Node, error) { setDefault(&opts.BeaconAPIURL, defaults.BeaconAPIURL) } + opts.Logger.Info( + "using L1 contracts", + "ValidatorRouter", opts.ValidatorRouterContract, + "BeaconAPIURL", opts.BeaconAPIURL, + "L1RPCURL", opts.L1RPCURL, + ) + notificationsSvc := notifications.New(opts.NotificationsBufferCap) nd.closers = append( nd.closers, diff --git a/tools/go.mod b/tools/go.mod index f0dc09db0..ebe3e9044 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/mattn/go-sqlite3 v1.14.24 - github.com/primev/mev-commit/bridge/standard v0.0.1 + github.com/primev/mev-commit/bridge/standard v0.0.1 // indirect github.com/primev/mev-commit/contracts-abi v0.0.1 github.com/primev/mev-commit/p2p v0.0.1 github.com/primev/mev-commit/x v0.0.1 @@ -21,12 +21,32 @@ require ( google.golang.org/protobuf v1.34.2 ) -require github.com/DATA-DOG/go-sqlmock v1.5.2 +require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/cockroachdb/pebble v1.1.2 + github.com/google/go-cmp v0.6.0 + resenje.org/multex v0.2.0 +) require ( + github.com/DataDog/zstd v1.5.5 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect + github.com/getsentry/sentry-go v0.28.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect ) replace github.com/primev/mev-commit/p2p => ../p2p @@ -73,7 +93,7 @@ require ( github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/crypto v0.35.0 // indirect + golang.org/x/crypto v0.35.0 golang.org/x/net v0.36.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 5b8846f59..a689056f3 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -20,6 +20,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a h1:f52TdbU4D5nozMAhO9TvTJ2ZMCXtN4VIAmfrrZ0JXQ4= @@ -44,6 +46,7 @@ github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOV github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= @@ -70,6 +73,8 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqG github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -124,6 +129,8 @@ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= @@ -164,6 +171,8 @@ github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsq github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks= github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -175,6 +184,7 @@ github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3 github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -189,6 +199,7 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= @@ -213,23 +224,49 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= @@ -249,5 +286,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +resenje.org/multex v0.2.0 h1:y1S8+bItGZo0lberxtQi9IhbWTpvRezhCWIFvt12VmU= +resenje.org/multex v0.2.0/go.mod h1:z+E+cUHGTgpqYn+P3yFOnC92i3X7rStzSur4rjOZM9s= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/tools/instant-bridge/api/api.go b/tools/instant-bridge/api/api.go index c14d76a92..eb079b5e8 100644 --- a/tools/instant-bridge/api/api.go +++ b/tools/instant-bridge/api/api.go @@ -16,24 +16,26 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/primev/mev-commit/p2p/pkg/apiserver" - "github.com/primev/mev-commit/tools/instant-bridge/bidder" - "github.com/primev/mev-commit/tools/instant-bridge/transfer" "github.com/primev/mev-commit/x/health" + bidder "github.com/primev/mev-commit/x/opt-in-bidder" + "github.com/primev/mev-commit/x/transfer" ) type API struct { - logger *slog.Logger - mux *http.ServeMux - port int - srv *http.Server - health health.Health - bidder *bidder.BidderClient - transferer *transfer.Transferer - minServiceFee *big.Int - status *status - owner common.Address - l1Client *ethclient.Client - settlementClient *ethclient.Client + logger *slog.Logger + mux *http.ServeMux + port int + srv *http.Server + health health.Health + bidder *bidder.BidderClient + transferer *transfer.Transferer + minServiceFee *big.Int + status *status + owner common.Address + l1Client *ethclient.Client + settlementClient *ethclient.Client + l1ChainID *big.Int + settlementChainID *big.Int } type bid struct { @@ -62,19 +64,23 @@ func NewAPI( owner common.Address, l1Client *ethclient.Client, settlementClient *ethclient.Client, + l1ChainID *big.Int, + settlementChainID *big.Int, ) *API { a := &API{ - logger: logger, - mux: http.NewServeMux(), - port: port, - status: &status{}, - health: health, - bidder: bdr, - transferer: transferer, - minServiceFee: minServiceFee, - owner: owner, - l1Client: l1Client, - settlementClient: settlementClient, + logger: logger, + mux: http.NewServeMux(), + port: port, + status: &status{}, + health: health, + bidder: bdr, + transferer: transferer, + minServiceFee: minServiceFee, + owner: owner, + l1Client: l1Client, + settlementClient: settlementClient, + l1ChainID: l1ChainID, + settlementChainID: settlementChainID, } a.status.bridgedAmount.Store(big.NewInt(0)) @@ -170,7 +176,7 @@ func NewAPI( return } - tx, err := a.transferer.ValidateL1Tx(b.RawTx) + tx, err := a.transferer.ValidateTx(b.RawTx, a.l1ChainID) if err != nil { apiserver.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid raw tx: %w", err)) return @@ -216,7 +222,9 @@ func NewAPI( req.Context(), halfFee, bridgeAmt, + // bridgeAmt, b.RawTx, + nil, ) if err != nil { apiserver.WriteError(w, http.StatusInternalServerError, err) @@ -226,29 +234,28 @@ func NewAPI( for status := range statusC { switch status.Type { case bidder.BidStatusNoOfProviders: - a.logger.Info("no of providers", "count", status.Arg1) + a.logger.Info("no of providers", "count", status.Arg.(int)) case bidder.BidStatusWaitSecs: - a.logger.Info("waiting for next slot", "seconds", status.Arg1) + a.logger.Info("waiting for next slot", "seconds", status.Arg.(int)) case bidder.BidStatusAttempted: - a.logger.Info("bid attempted", "block", status.Arg1) + a.logger.Info("bid attempted", "block", status.Arg) case bidder.BidStatusFailed: apiserver.WriteError( w, http.StatusInternalServerError, - fmt.Errorf("bid failed: %s", status.Arg2), + fmt.Errorf("bid failed: %s", status.Arg.(string)), ) return - case bidder.BidStatusSucceeded: - a.logger.Info("bid succeeded", "block", status.Arg1) } } a.status.bidsSucceeded.Add(1) a.status.transfersAttempted.Add(1) - err = a.transferer.TransferOnSettlement( + err = a.transferer.Transfer( req.Context(), destAddr, + a.settlementChainID, bridgeAmt, ) if err != nil { diff --git a/tools/instant-bridge/service/service.go b/tools/instant-bridge/service/service.go index 324ef7bda..6b96a952f 100644 --- a/tools/instant-bridge/service/service.go +++ b/tools/instant-bridge/service/service.go @@ -14,13 +14,13 @@ import ( bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" - "github.com/primev/mev-commit/tools/instant-bridge/accountsync" "github.com/primev/mev-commit/tools/instant-bridge/api" - "github.com/primev/mev-commit/tools/instant-bridge/bidder" - "github.com/primev/mev-commit/tools/instant-bridge/transfer" + "github.com/primev/mev-commit/x/accountsync" "github.com/primev/mev-commit/x/contracts/ethwrapper" "github.com/primev/mev-commit/x/health" "github.com/primev/mev-commit/x/keysigner" + bidder "github.com/primev/mev-commit/x/opt-in-bidder" + "github.com/primev/mev-commit/x/transfer" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -146,8 +146,6 @@ func New(config *Config) (*Service, error) { transferer := transfer.NewTransferer( config.Logger.With("module", "transferer"), settlementClient, - l1ChainID, - settlementChainID, config.Signer, config.GasTipCap, config.GasFeeCap, @@ -163,6 +161,8 @@ func New(config *Config) (*Service, error) { config.Signer.GetAddress(), l1RPCClient.RawClient(), settlementClient, + l1ChainID, + settlementChainID, ) apiService.Start() diff --git a/tools/preconf-rpc/.goreleaser.yml b/tools/preconf-rpc/.goreleaser.yml new file mode 100644 index 000000000..e9655f183 --- /dev/null +++ b/tools/preconf-rpc/.goreleaser.yml @@ -0,0 +1,64 @@ +version: 1 + +project_name: preconf-rpc +dist: /tmp/dist/preconf-rpc + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + dir: ./tools/preconf-rpc + binary: "{{ .ProjectName }}" + flags: + - -v + - -trimpath + +archives: + - format: tar.gz + name_template: >- + {{- .Binary }}_ + {{- with index .Env "RELEASE_VERSION" -}} + {{ . }} + {{- else -}} + {{- if .IsSnapshot }}{{ .ShortCommit }} + {{- else }}{{ .Version }} + {{- end }} + {{- end -}} + {{- with index .Env "DIRTY_SUFFIX" -}} + {{ . }} + {{- end -}}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }} + {{- end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: >- + {{ .ProjectName }}_ + {{- with index .Env "RELEASE_VERSION" -}} + {{ . }} + {{- else -}} + {{- if .IsSnapshot }}{{ .ShortCommit }} + {{- else }}{{ .Version }} + {{- end }} + {{- end -}} + {{- with index .Env "DIRTY_SUFFIX" -}} + {{ . }} + {{- end -}} + _checksums.txt + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go new file mode 100644 index 000000000..449f76694 --- /dev/null +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -0,0 +1,114 @@ +package blocktracker + +import ( + "context" + "log/slog" + "math/big" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type EthClient interface { + BlockNumber(ctx context.Context) (uint64, error) + BlockByNumber(ctx context.Context, blockNumber *big.Int) (*types.Block, error) +} + +type blockTracker struct { + latestBlockNo atomic.Uint64 + blocks map[uint64]*types.Block + client EthClient + log *slog.Logger + checkTrigger chan struct{} +} + +func NewBlockTracker(client EthClient, log *slog.Logger) *blockTracker { + return &blockTracker{ + latestBlockNo: atomic.Uint64{}, + blocks: make(map[uint64]*types.Block), + client: client, + log: log, + checkTrigger: make(chan struct{}, 1), + } +} + +func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { + done := make(chan struct{}) + ticker := time.NewTicker(500 * time.Millisecond) + go func() { + defer close(done) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + blockNo, err := b.client.BlockNumber(ctx) + if err != nil { + b.log.Error("Failed to get block number", "error", err) + continue + } + if blockNo > b.latestBlockNo.Load() { + block, err := b.client.BlockByNumber(ctx, big.NewInt(int64(blockNo))) + if err != nil { + b.log.Error("Failed to get block by number", "error", err) + continue + } + b.blocks[blockNo] = block + b.latestBlockNo.Store(block.NumberU64()) + b.log.Info("New block detected", "number", block.NumberU64(), "hash", block.Hash().Hex()) + b.triggerCheck() + } + } + } + }() + return done +} + +func (b *blockTracker) triggerCheck() { + select { + case b.checkTrigger <- struct{}{}: + default: + // Non-blocking send, if channel is full, we skip + } +} + +func (b *blockTracker) LatestBlockNumber() uint64 { + return b.latestBlockNo.Load() +} + +func (b *blockTracker) CheckTxnInclusion( + ctx context.Context, + txHash common.Hash, + blockNumber uint64, +) (bool, error) { +WaitForBlock: + for { + select { + case <-ctx.Done(): + return false, ctx.Err() + case <-b.checkTrigger: + if blockNumber <= b.latestBlockNo.Load() { + break WaitForBlock + } + } + } + + block, ok := b.blocks[blockNumber] + if !ok { + block, err := b.client.BlockByNumber(ctx, big.NewInt(int64(blockNumber))) + if err != nil { + b.log.Error("Failed to get block by number", "error", err, "blockNumber", blockNumber) + return false, err + } + b.blocks[blockNumber] = block + } + + for _, tx := range block.Transactions() { + if tx.Hash().Cmp(txHash) == 0 { + return true, nil + } + } + return false, nil +} diff --git a/tools/preconf-rpc/blocktracker/blocktracker_test.go b/tools/preconf-rpc/blocktracker/blocktracker_test.go new file mode 100644 index 000000000..9601f4e8b --- /dev/null +++ b/tools/preconf-rpc/blocktracker/blocktracker_test.go @@ -0,0 +1,139 @@ +package blocktracker_test + +import ( + "context" + "hash" + "log/slog" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/primev/mev-commit/tools/preconf-rpc/blocktracker" + "golang.org/x/crypto/sha3" +) + +type mockEthClient struct { + blockNumber chan uint64 + blocks map[uint64]*types.Block +} + +func (m *mockEthClient) BlockNumber(ctx context.Context) (uint64, error) { + select { + case blockNo := <-m.blockNumber: + return blockNo, nil + case <-ctx.Done(): + return 0, ctx.Err() + } +} + +func (m *mockEthClient) BlockByNumber(ctx context.Context, blockNumber *big.Int) (*types.Block, error) { + block, exists := m.blocks[blockNumber.Uint64()] + if !exists { + return nil, nil // Simulate block not found + } + return block, nil +} + +type testHasher struct { + hasher hash.Hash +} + +// NewHasher returns a new testHasher instance. +func NewHasher() *testHasher { + return &testHasher{hasher: sha3.NewLegacyKeccak256()} +} + +// Reset resets the hash state. +func (h *testHasher) Reset() { + h.hasher.Reset() +} + +// Update updates the hash state with the given key and value. +func (h *testHasher) Update(key, val []byte) error { + h.hasher.Write(key) + h.hasher.Write(val) + return nil +} + +// Hash returns the hash value. +func (h *testHasher) Hash() common.Hash { + return common.BytesToHash(h.hasher.Sum(nil)) +} + +func TestBlockTracker(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + + tx1 := types.NewTransaction(1, common.HexToAddress("0xabc"), big.NewInt(100), 21000, big.NewInt(1), nil) + tx2 := types.NewTransaction(2, common.HexToAddress("0xdef"), big.NewInt(200), 21000, big.NewInt(1), nil) + tx3 := types.NewTransaction(3, common.HexToAddress("0x123"), big.NewInt(300), 21000, big.NewInt(1), nil) + tx4 := types.NewTransaction(4, common.HexToAddress("0x456"), big.NewInt(400), 21000, big.NewInt(1), nil) + + blk1 := types.NewBlock( + &types.Header{ + Number: big.NewInt(100), + Time: 1622547800, + }, + &types.Body{Transactions: []*types.Transaction{tx1, tx2}}, + nil, // No receipts + NewHasher(), + ) + + blk2 := types.NewBlock( + &types.Header{ + Number: big.NewInt(101), + Time: 1622547900, + }, + &types.Body{Transactions: []*types.Transaction{tx3}}, + nil, // No receipts + NewHasher(), + ) + + client := &mockEthClient{ + blockNumber: make(chan uint64, 1), + blocks: map[uint64]*types.Block{ + 100: blk1, + 101: blk2, + }, + } + + tracker := blocktracker.NewBlockTracker(client, slog.Default()) + done := tracker.Start(ctx) + + blkNo := tracker.LatestBlockNumber() + if blkNo != 0 { + t.Fatalf("Expected latest block number to be 0, got %d", blkNo) + } + + client.blockNumber <- 100 + + included, err := tracker.CheckTxnInclusion(ctx, tx1.Hash(), 100) + if err != nil { + t.Fatalf("Error checking transaction inclusion: %v", err) + } + + if !included { + t.Fatalf("Expected transaction %s to be included in block 100", tx1.Hash().Hex()) + } + + blkNo = tracker.LatestBlockNumber() + if blkNo != 100 { + t.Fatalf("Expected latest block number to be 100, got %d", blkNo) + } + + client.blockNumber <- 101 + + included, err = tracker.CheckTxnInclusion(ctx, tx4.Hash(), 101) + if err != nil { + t.Fatalf("Error checking transaction inclusion: %v", err) + } + + if included { + t.Fatalf("Expected transaction %s not to be included in block 101", tx4.Hash().Hex()) + } + + cancel() + <-done // Wait for the tracker to finish +} diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go new file mode 100644 index 000000000..c45d2248c --- /dev/null +++ b/tools/preconf-rpc/handlers/handlers.go @@ -0,0 +1,821 @@ +package handlers + +import ( + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log/slog" + "math/big" + "strconv" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + "github.com/primev/mev-commit/tools/preconf-rpc/pricer" + "github.com/primev/mev-commit/tools/preconf-rpc/rpcserver" + optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" + "resenje.org/multex" +) + +const ( + blockTime = 12 // seconds, typical Ethereum block time +) + +var ( + preconfBlockHashPrefix = hex.EncodeToString([]byte("mev-commit")) +) + +type Bidder interface { + Estimate() (int64, error) + Bid( + ctx context.Context, + bidAmount *big.Int, + slashAmount *big.Int, + rawTx string, + opts *optinbidder.BidOpts, + ) (chan optinbidder.BidStatus, error) +} + +type Pricer interface { + EstimatePrice( + ctx context.Context, + txn *types.Transaction, + ) (*pricer.BlockPrice, error) +} + +type Store interface { + StorePreconfirmedTransaction( + ctx context.Context, + blockNumber int64, + txn *types.Transaction, + commitments []*bidderapiv1.Commitment, + ) error + GetPreconfirmedTransaction( + ctx context.Context, + txnHash common.Hash, + ) (*types.Transaction, []*bidderapiv1.Commitment, error) + GetPreconfirmedTransactionsForBlock( + ctx context.Context, + blockNumber int64, + ) ([]*types.Transaction, error) + DeductBalance(ctx context.Context, account common.Address, amount *big.Int) error + HasBalance(ctx context.Context, account common.Address, amount *big.Int) bool + GetBalance(ctx context.Context, account common.Address) (*big.Int, error) + AddBalance(ctx context.Context, account common.Address, amount *big.Int) error +} + +type BlockTracker interface { + CheckTxnInclusion(ctx context.Context, txnHash common.Hash, blockNumber uint64) (bool, error) + LatestBlockNumber() uint64 +} + +type accountNonce struct { + Account string `json:"account"` + Nonce uint64 `json:"nonce"` + Block int64 `json:"block"` +} + +type bidResult struct { + noOfProviders int + blockNumber uint64 + optedInSlot bool + bidAmount *big.Int + commitments []*bidderapiv1.Commitment +} + +type rpcMethodHandler struct { + logger *slog.Logger + bidder Bidder + store Store + pricer Pricer + blockTracker BlockTracker + owner common.Address + chainID *big.Int + nonceLock *multex.Multex[string] + nonceMap map[string]accountNonce + nonceMapLock sync.RWMutex +} + +func NewRPCMethodHandler( + logger *slog.Logger, + bidder Bidder, + store Store, + pricer Pricer, + blockTracker BlockTracker, + owner common.Address, + chainId *big.Int, +) *rpcMethodHandler { + return &rpcMethodHandler{ + logger: logger, + bidder: bidder, + store: store, + pricer: pricer, + blockTracker: blockTracker, + owner: owner, + chainID: chainId, + nonceLock: multex.New[string](), + nonceMap: make(map[string]accountNonce), + } +} + +func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { + // Ethereum JSON-RPC methods overridden + server.RegisterHandler("eth_getBlockNumber", h.handleGetBlockNumber) + server.RegisterHandler("eth_chainId", h.handleChainID) + server.RegisterHandler("eth_sendRawTransaction", h.handleSendRawTx) + server.RegisterHandler("eth_getTransactionReceipt", h.handleGetTxReceipt) + server.RegisterHandler("eth_getTransactionCount", h.handleGetTxCount) + server.RegisterHandler("eth_getBlockByHash", h.handleGetBlockByHash) + // Custom methods for MEV Commit + server.RegisterHandler("mevcommit_getTransactionCommitments", h.handleGetTxCommitments) + server.RegisterHandler("mevcommit_getBalance", h.handleMevCommitGetBalance) + server.RegisterHandler("mevcommit_estimateFastBid", h.handleMevCommitEstimateFastBid) +} + +func (h *rpcMethodHandler) handleGetBlockNumber( + ctx context.Context, + params ...any, +) (json.RawMessage, bool, error) { + if len(params) != 0 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "getBlockNumber does not require any parameters", + ) + } + + blockNumber := h.blockTracker.LatestBlockNumber() + h.logger.Info("Retrieved latest block number", "blockNumber", blockNumber) + + blockNumberJSON, err := json.Marshal(hexutil.Uint64(blockNumber)) + if err != nil { + h.logger.Error("Failed to marshal block number to JSON", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal block number", + ) + } + + return blockNumberJSON, false, nil +} + +func (h *rpcMethodHandler) handleChainID( + ctx context.Context, + params ...any, +) (json.RawMessage, bool, error) { + if len(params) != 0 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "chainID does not require any parameters", + ) + } + + chainIDJSON, err := json.Marshal(hexutil.Uint64(h.chainID.Uint64())) + if err != nil { + h.logger.Error("Failed to marshal chain ID to JSON", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal chain ID", + ) + } + + return chainIDJSON, false, nil +} + +func (h *rpcMethodHandler) handleGetBlockByHash( + ctx context.Context, + params ...any, +) (json.RawMessage, bool, error) { + if len(params) == 0 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "getBlockByHash requires one or two parameter", + ) + } + + if params[0] == nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getBlock parameter cannot be null", + ) + } + + blockHashStr := params[0].(string) + if !strings.HasPrefix(blockHashStr, preconfBlockHashPrefix) { + return nil, true, nil // Not a preconf block hash, proxy + } + + details := false + if len(params) > 1 && params[1] != nil { + details, _ = params[1].(bool) + } + + blockNumberWithPadding := strings.TrimPrefix(blockHashStr, preconfBlockHashPrefix) + blockNumber, err := strconv.ParseUint(blockNumberWithPadding[:8], 10, 64) + if err != nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getBlock parameter must be a valid preconf block hash", + ) + } + + txns, err := h.store.GetPreconfirmedTransactionsForBlock(ctx, int64(blockNumber)) + if err != nil { + h.logger.Error("Failed to get preconfirmed transactions for block", "error", err, "blockNumber", blockNumber) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to get preconfirmed transactions for block", + ) + } + + block := map[string]interface{}{ + "number": hexutil.Uint64(blockNumber), + "hash": blockHashStr, + "parentHash": (common.Hash{}).Hex(), + "nonce": "0x0000000000000000", + "sha3Uncles": (common.Hash{}).Hex(), + "logsBloom": hexutil.Bytes(types.Bloom{}.Bytes()), + "transactionsRoot": (common.Hash{}).Hex(), + "stateRoot": (common.Hash{}).Hex(), + "miner": h.owner.Hex(), + "difficulty": hexutil.Uint64(0), + "totalDifficulty": hexutil.Uint64(0), + "size": hexutil.Uint64(0), + "extraData": "0x", + "gasLimit": hexutil.Uint64(0), + "gasUsed": hexutil.Uint64(0), + "timestamp": hexutil.Uint64(0), + "baseFeePerGas": hexutil.EncodeBig(big.NewInt(0)), + "withdrawals": nil, + } + + var txnsToReturn any + for i, txn := range txns { + if !details { + if txnsToReturn == nil { + txnsToReturn = make([]string, 0, len(txns)) + } + txnsToReturn = append( + txnsToReturn.([]string), + txn.Hash().Hex(), + ) + continue + } + if txnsToReturn == nil { + txnsToReturn = make([]map[string]interface{}, len(txns)) + } + r, s, v := txn.RawSignatureValues() + sender, err := types.Sender(types.LatestSignerForChainID(txn.ChainId()), txn) + if err != nil { + h.logger.Error("Failed to get transaction sender", "error", err, "txnHash", txn.Hash().Hex()) + continue + } + txnsToReturn = append( + txnsToReturn.([]map[string]interface{}), + map[string]interface{}{ + "hash": txn.Hash().Hex(), + "blockHash": blockHashStr, + "blockNumber": hexutil.Uint64(blockNumber), + "transactionIndex": hexutil.Uint64(i), + "type": hexutil.Uint(txn.Type()), + "accessList": nil, // Access lists are not used in preconf blocks + "maxFeePerGas": hexutil.EncodeBig(txn.GasFeeCap()), + "maxPriorityFeePerGas": hexutil.EncodeBig(txn.GasTipCap()), + "to": txn.To().Hex(), + "value": hexutil.EncodeBig(txn.Value()), + "input": hexutil.Encode(txn.Data()), + "from": sender.Hex(), + "nonce": hexutil.Uint64(txn.Nonce()), + "gas": hexutil.Uint64(txn.Gas()), + "gasPrice": hexutil.EncodeBig(txn.GasPrice()), + "r": hexutil.EncodeBig(r), + "s": hexutil.EncodeBig(s), + "v": hexutil.EncodeBig(v), + }, + ) + } + block["transactions"] = txnsToReturn + blockJSON, err := json.Marshal(block) + if err != nil { + h.logger.Error("Failed to marshal block to JSON", "error", err, "blockNumber", blockNumber) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal block", + ) + } + + h.logger.Info("Retrieved preconf block", "blockNumber", blockNumber, "txCount", len(txns)) + return blockJSON, false, nil +} + +func (h *rpcMethodHandler) handleSendRawTx( + ctx context.Context, + params ...any, +) (json.RawMessage, bool, error) { + if len(params) != 1 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "sendRawTx requires exactly one parameter", + ) + } + if params[0] == nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "sendRawTx parameter cannot be null", + ) + } + + rawTxHex := params[0].(string) + if len(rawTxHex) < 2 || rawTxHex[:2] != "0x" { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "sendRawTx parameter must be a hex string starting with '0x'", + ) + } + + decodedTxn, err := hex.DecodeString(rawTxHex[2:]) + if err != nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "sendRawTx parameter must be a valid hex string", + ) + } + + txn := new(types.Transaction) + if err := txn.UnmarshalBinary(decodedTxn); err != nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "sendRawTx parameter must be a valid transaction", + ) + } + + sender, err := types.Sender(types.LatestSignerForChainID(txn.ChainId()), txn) + if err != nil { + h.logger.Error("Failed to get transaction sender", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to get transaction sender", + ) + } + + // Once we are ready to send the bid, we need to ensure that the nonce for the + // sender is not locked by another transaction. + h.nonceLock.Lock(sender.Hex()) + defer h.nonceLock.Unlock(sender.Hex()) + + // This is a txn to add balance to the bidder's account, so we will pay this + // out of the owner's account. We will add the balance to the bidder's + // account and then proceed with the bid process. + depositTxn := txn.To().Cmp(h.owner) == 0 && txn.Value().Cmp(big.NewInt(0)) > 0 + +BID_LOOP: + for { + select { + case <-ctx.Done(): + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "context cancelled while processing transaction", + ) + default: + } + + result, err := h.sendBid(ctx, txn, sender, rawTxHex, depositTxn) + switch { + case err != nil: + h.logger.Error("Failed to send bid", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to send bid", + ) + case result.optedInSlot: + if result.noOfProviders == len(result.commitments) { + // This means that all builders have committed to the bid and it + // is a primev opted in slot. We can safely proceed to inform the + // user that the txn was successfully sent and will be processed + if err := h.storePreconfAndDeductBalance( + ctx, + txn, + result.commitments, + sender, + int64(result.blockNumber), + result.bidAmount, + depositTxn, + ); err != nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to update preconfirmed transaction and deduct balance", + ) + } + // Update the nonce locally if user wants to send more transactions + h.nonceMapLock.Lock() + h.nonceMap[sender.Hex()] = accountNonce{ + Account: sender.Hex(), + Nonce: txn.Nonce() + 1, + Block: int64(result.blockNumber), + } + h.nonceMapLock.Unlock() + break BID_LOOP + } + default: + } + + // Wait for block number to be updated to confirm transaction. If failed + // we will retry the bid process till user cancels the operation + included, err := h.blockTracker.CheckTxnInclusion(ctx, txn.Hash(), result.blockNumber) + if err != nil { + h.logger.Error("Failed to check transaction inclusion", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to check transaction inclusion", + ) + } + if included { + if err := h.storePreconfAndDeductBalance( + ctx, + txn, + result.commitments, + sender, + int64(result.blockNumber), + result.bidAmount, + depositTxn, + ); err != nil { + h.logger.Error("Failed to update preconfirmed transaction and deduct balance", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to update preconfirmed transaction and deduct balance", + ) + } + break BID_LOOP + } + } + + // If we reach here, we have a successful bid with commitments + txHashJSON, err := json.Marshal(txn.Hash().Hex()) + if err != nil { + h.logger.Error("Failed to marshal transaction hash to JSON", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal transaction hash", + ) + } + + return txHashJSON, false, nil +} + +func (h *rpcMethodHandler) sendBid( + ctx context.Context, + txn *types.Transaction, + sender common.Address, + rawTxHex string, + depositTxn bool, +) (bidResult, error) { + timeToOptIn, err := h.bidder.Estimate() + if err != nil { + h.logger.Error("Failed to estimate time to opt-in", "error", err) + if !errors.Is(err, optinbidder.ErrNoSlotInCurrentEpoch) && !errors.Is(err, optinbidder.ErrNoEpochInfo) { + return bidResult{}, err + } + // If we cannot estimate the time to opt-in, we assume a default value and + // proceed with the bid process. The default value should be higher than + // the typical block time to ensure we consider the next slot as a non-opt-in slot. + timeToOptIn = blockTime * 32 + } + + optedInSlot := timeToOptIn <= blockTime + + price, err := h.pricer.EstimatePrice(ctx, txn) + if err != nil { + h.logger.Error("Failed to estimate transaction price", "error", err) + return bidResult{}, fmt.Errorf("failed to estimate transaction price: %w", err) + } + + if !depositTxn && !h.store.HasBalance(ctx, sender, price.BidAmount) { + h.logger.Error("Insufficient balance for sender", "sender", sender.Hex()) + return bidResult{}, fmt.Errorf("insufficient balance for sender: %s", sender.Hex()) + } + + bidC, err := h.bidder.Bid( + ctx, + price.BidAmount, + big.NewInt(0), + rawTxHex[2:], + &optinbidder.BidOpts{ + WaitForOptIn: optedInSlot, + // BlockNumber: uint64(price.BlockNumber), + }, + ) + if err != nil { + h.logger.Error("Failed to place bid", "error", err) + return bidResult{}, fmt.Errorf("failed to place bid: %w", err) + } + + result := bidResult{ + commitments: make([]*bidderapiv1.Commitment, 0), + bidAmount: price.BidAmount, + } +BID_LOOP: + for { + select { + case <-ctx.Done(): + h.logger.Info("Context cancelled while waiting for bid status") + return bidResult{}, ctx.Err() + case bidStatus, more := <-bidC: + if !more { + h.logger.Info("Bid channel closed, no more bid statuses") + break BID_LOOP + } + switch bidStatus.Type { + case optinbidder.BidStatusNoOfProviders: + result.noOfProviders = bidStatus.Arg.(int) + case optinbidder.BidStatusAttempted: + result.blockNumber = bidStatus.Arg.(uint64) + case optinbidder.BidStatusCommitment: + result.commitments = append(result.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) + case optinbidder.BidStatusCancelled: + h.logger.Warn("Bid context cancelled by the bidder") + break BID_LOOP + case optinbidder.BidStatusFailed: + h.logger.Error("Bid failed", "error", bidStatus.Arg) + break BID_LOOP + } + } + } + if len(result.commitments) == 0 { + h.logger.Error("Bid completed with no commitments") + return bidResult{}, fmt.Errorf("bid completed with no commitments") + } + h.logger.Info( + "Bid successful with commitments", + "noOfProviders", result.noOfProviders, + "noOfCommitments", len(result.commitments), + "blockNumber", result.blockNumber, + "optedInSlot", optedInSlot, + ) + + result.optedInSlot = optedInSlot + return result, nil +} + +func (h *rpcMethodHandler) storePreconfAndDeductBalance( + ctx context.Context, + txn *types.Transaction, + commitments []*bidderapiv1.Commitment, + sender common.Address, + blockNumber int64, + amount *big.Int, + depositTxn bool, +) error { + if err := h.store.StorePreconfirmedTransaction(ctx, blockNumber, txn, commitments); err != nil { + h.logger.Error("Failed to store preconfirmed transaction", "error", err) + return fmt.Errorf("failed to store preconfirmed transaction: %w", err) + } + + if !depositTxn { + if err := h.store.DeductBalance(ctx, sender, amount); err != nil { + h.logger.Error("Failed to deduct balance for sender", "sender", sender.Hex(), "error", err) + return fmt.Errorf("failed to deduct balance for sender: %w", err) + } + } else { + if err := h.store.AddBalance(ctx, sender, txn.Value()); err != nil { + h.logger.Error("Failed to add balance for sender", "sender", sender.Hex(), "error", err) + return fmt.Errorf("failed to add balance for sender: %w", err) + } + } + + return nil +} + +func (h *rpcMethodHandler) handleGetTxReceipt(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + if len(params) != 1 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "getTxReceipt requires exactly one parameter", + ) + } + if params[0] == nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getTxReceipt parameter cannot be null", + ) + } + + txHashStr := params[0].(string) + if len(txHashStr) < 2 || txHashStr[:2] != "0x" { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getTxReceipt parameter must be a hex string starting with '0x'", + ) + } + + txHash := common.HexToHash(txHashStr) + + h.logger.Info("Retrieving transaction receipt", "txHash", txHash) + txn, commitments, err := h.store.GetPreconfirmedTransaction(ctx, txHash) + if err != nil { + return nil, true, nil + } + + if h.blockTracker.LatestBlockNumber() > uint64(commitments[0].BlockNumber) { + return nil, true, nil + } + + sender, err := types.Sender(types.LatestSignerForChainID(txn.ChainId()), txn) + if err != nil { + h.logger.Error("Failed to get transaction sender", "error", err, "txHash", txHash) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to get transaction sender", + ) + } + + blockHash := fmt.Sprintf("%s%08d", preconfBlockHashPrefix, commitments[0].BlockNumber) + padding := strings.Repeat("0", 66-len(blockHash)) + blockHash = blockHash + padding + + result := map[string]interface{}{ + "type": hexutil.Uint(txn.Type()), + "transactionHash": txn.Hash().Hex(), + "transactionIndex": hexutil.Uint(0), + "blockHash": blockHash, + "blockNumber": hexutil.EncodeBig(big.NewInt(commitments[0].BlockNumber)), + "from": sender.Hex(), + "to": nil, + "contractAddress": (common.Address{}).Hex(), + "gasUsed": hexutil.Uint64(0), + "cumulativeGasUsed": hexutil.Uint64(1), + "logs": []*types.Log{}, // should be [] not null + "logsBloom": hexutil.Bytes(types.Bloom{}.Bytes()), + "status": hexutil.Uint64(types.ReceiptStatusSuccessful), + "effectiveGasPrice": hexutil.EncodeBig(big.NewInt(0)), + } + + receiptJSON, err := json.Marshal(result) + if err != nil { + h.logger.Error("Failed to marshal receipt to JSON", "error", err, "txHash", txHash) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal receipt", + ) + } + + return receiptJSON, false, nil +} + +func (h *rpcMethodHandler) handleGetTxCount(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + if len(params) == 2 { + state := params[1].(string) + if state != "latest" && state != "pending" { + return nil, true, nil + } + } + + if params[0] == nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getTxCount parameter cannot be null", + ) + } + + account := params[0].(string) + if len(account) < 2 || account[:2] != "0x" { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getTxCount parameter must be a hex string starting with '0x'", + ) + } + + h.nonceLock.Lock(account) + defer h.nonceLock.Unlock(account) + + h.nonceMapLock.RLock() + accNonce, found := h.nonceMap[account] + h.nonceMapLock.RUnlock() + + if !found { + return nil, true, nil + } + + if h.blockTracker.LatestBlockNumber() > uint64(accNonce.Block) { + h.nonceMapLock.Lock() + delete(h.nonceMap, account) + h.nonceMapLock.Unlock() + return nil, true, nil + } + + nonceJSON, err := json.Marshal(accNonce.Nonce) + if err != nil { + h.logger.Error("Failed to marshal nonce to JSON", "error", err, "account", account) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal nonce", + ) + } + + h.logger.Info("Retrieved account nonce from cache", "account", account, "nonce", accNonce.Nonce) + return nonceJSON, false, nil +} + +func (h *rpcMethodHandler) handleGetTxCommitments( + ctx context.Context, + params ...any, +) (json.RawMessage, bool, error) { + if len(params) != 1 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "getTxCommitments requires exactly one parameter", + ) + } + + if params[0] == nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getTxCommitments parameter cannot be null", + ) + } + + txHashStr := params[0].(string) + if len(txHashStr) < 2 || txHashStr[:2] != "0x" { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "getTxCommitments parameter must be a hex string starting with '0x'", + ) + } + + txHash := common.HexToHash(txHashStr) + + _, commitments, err := h.store.GetPreconfirmedTransaction(ctx, txHash) + if err != nil { + return nil, true, nil + } + + if len(commitments) == 0 { + h.logger.Info("No commitments found for transaction", "txHash", txHash) + return json.RawMessage("[]"), false, nil + } + + commitmentsJSON, err := json.Marshal(commitments) + if err != nil { + h.logger.Error("Failed to marshal commitments to JSON", "error", err, "txHash", txHash) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal commitments", + ) + } + + return commitmentsJSON, false, nil +} + +func (h *rpcMethodHandler) handleMevCommitGetBalance(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + if len(params) != 1 { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeInvalidRequest, + "mevcommit_getBalance requires exactly one parameter", + ) + } + + if params[0] == nil { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "mevcommit_getBalance parameters cannot be null", + ) + } + + account := params[0].(string) + if len(account) < 2 || account[:2] != "0x" { + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeParseError, + "mevcommit_getBalance account must be a hex string starting with '0x'", + ) + } + + balance, err := h.store.GetBalance(ctx, common.HexToAddress(account)) + if err != nil { + h.logger.Error("Failed to get balance for account", "error", err, "account", account) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to get balance for account", + ) + } + + return json.RawMessage(fmt.Sprintf(`{"balance": "%s"}`, balance)), false, nil +} + +func (h *rpcMethodHandler) handleMevCommitEstimateFastBid( + ctx context.Context, + _ ...any, +) (json.RawMessage, bool, error) { + timeToOptIn, err := h.bidder.Estimate() + if err != nil { + h.logger.Error("Failed to estimate fast bid", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to estimate fast bid", + ) + } + + return json.RawMessage(fmt.Sprintf(`{"timeInSecs": "%d"}`, timeToOptIn)), false, nil +} diff --git a/tools/preconf-rpc/main.go b/tools/preconf-rpc/main.go new file mode 100644 index 000000000..ede6b1d3c --- /dev/null +++ b/tools/preconf-rpc/main.go @@ -0,0 +1,267 @@ +package main + +import ( + "fmt" + "math/big" + "os" + "os/signal" + "slices" + "strings" + "syscall" + + "github.com/ethereum/go-ethereum/common" + "github.com/primev/mev-commit/tools/preconf-rpc/service" + "github.com/primev/mev-commit/x/keysigner" + "github.com/primev/mev-commit/x/util" + "github.com/urfave/cli/v2" +) + +var ( + optionHTTPPort = &cli.IntFlag{ + Name: "http-port", + Usage: "port for the HTTP server", + EnvVars: []string{"PRECONF_RPC_HTTP_PORT"}, + Value: 8080, + } + + optionKeystorePath = &cli.StringFlag{ + Name: "keystore-dir", + Usage: "directory where keystore file is stored", + EnvVars: []string{"PRECONF_RPC_KEYSTORE_DIR"}, + Required: true, + } + + optionKeystorePassword = &cli.StringFlag{ + Name: "keystore-password", + Usage: "use to access keystore", + EnvVars: []string{"PRECONF_RPC_KEYSTORE_PASSWORD"}, + Required: true, + } + + optionDataDir = &cli.StringFlag{ + Name: "data-dir", + Usage: "directory where data is stored", + EnvVars: []string{"PRECONF_RPC_DATA_DIR"}, + Value: "~/data", + } + + optionL1RPCUrls = &cli.StringSliceFlag{ + Name: "l1-rpc-urls", + Usage: "URLs for L1 RPC", + EnvVars: []string{"PRECONF_RPC_L1_RPC_URLS"}, + Required: true, + } + + optionSettlementRPCUrl = &cli.StringFlag{ + Name: "settlement-rpc-url", + Usage: "URL for settlement RPC", + EnvVars: []string{"PRECONF_RPC_SETTLEMENT_RPC_URL"}, + Required: true, + } + + optionBidderRPCUrl = &cli.StringFlag{ + Name: "bidder-rpc-url", + Usage: "URL for mev-commit bidder RPC", + EnvVars: []string{"PRECONF_RPC_BIDDER_RPC_URL"}, + Required: true, + } + + optionL1ContractAddr = &cli.StringFlag{ + Name: "l1-contract-addr", + Usage: "address of the L1 gateway contract", + EnvVars: []string{"PRECONF_RPC_L1_CONTRACT_ADDR"}, + Required: true, + } + + optionSettlementThreshold = &cli.StringFlag{ + Name: "settlement-threshold", + Usage: "Minimum threshold for settlement chain balance", + EnvVars: []string{"PRECONF_RPC_SETTLEMENT_THRESHOLD"}, + Value: "5000000000000000000", // 5 ETH + } + + optionSettlementTopup = &cli.StringFlag{ + Name: "settlement-topup", + Usage: "topup for settlement", + EnvVars: []string{"PRECONF_RPC_SETTLEMENT_TOPUP"}, + Value: "10000000000000000000", // 10 ETH + } + + optionAutoDepositAmount = &cli.StringFlag{ + Name: "auto-deposit-amount", + Usage: "auto deposit amount", + EnvVars: []string{"PRECONF_RPC_AUTO_DEPOSIT_AMOUNT"}, + Value: "1000000000000000000", // 1 ETH + } + + optionGasTipCap = &cli.StringFlag{ + Name: "gas-tip-cap", + Usage: "gas tip cap", + EnvVars: []string{"PRECONF_RPC_GAS_TIP_CAP"}, + Value: "50000000", // 0.05 gWEI + } + + optionGasFeeCap = &cli.StringFlag{ + Name: "gas-fee-cap", + Usage: "gas fee cap", + EnvVars: []string{"PRECONF_RPC_GAS_FEE_CAP"}, + Value: "60000000", // 0.06 gWEI + } + + optionSettlementContractAddr = &cli.StringFlag{ + Name: "settlement-contract-addr", + Usage: "address of the settlement gateway contract", + EnvVars: []string{"PRECONF_RPC_SETTLEMENT_CONTRACT_ADDR"}, + Required: true, + } + + optionLogFmt = &cli.StringFlag{ + Name: "log-fmt", + Usage: "log format to use, options are 'text' or 'json'", + EnvVars: []string{"PRECONF_RPC_LOG_FMT"}, + Value: "text", + Action: func(ctx *cli.Context, s string) error { + if !slices.Contains([]string{"text", "json"}, s) { + return fmt.Errorf("invalid log-fmt, expecting 'text' or 'json'") + } + return nil + }, + } + + optionLogLevel = &cli.StringFlag{ + Name: "log-level", + Usage: "log level to use, options are 'debug', 'info', 'warn', 'error'", + EnvVars: []string{"PRECONF_RPC_LOG_LEVEL"}, + Value: "info", + Action: func(ctx *cli.Context, s string) error { + if !slices.Contains([]string{"debug", "info", "warn", "error"}, s) { + return fmt.Errorf("invalid log-level, expecting 'debug', 'info', 'warn', 'error'") + } + return nil + }, + } + + optionLogTags = &cli.StringFlag{ + Name: "log-tags", + Usage: "log tags is a comma-separated list of pairs that will be inserted into each log line", + EnvVars: []string{"PRECONF_RPC_LOG_TAGS"}, + Action: func(ctx *cli.Context, s string) error { + for i, p := range strings.Split(s, ",") { + if len(strings.Split(p, ":")) != 2 { + return fmt.Errorf("invalid log-tags at index %d, expecting ", i) + } + } + return nil + }, + } +) + +func main() { + app := &cli.App{ + Name: "preconf-rpc", + Usage: "Preconf RPC service", + Flags: []cli.Flag{ + optionHTTPPort, + optionDataDir, + optionLogFmt, + optionLogLevel, + optionLogTags, + optionKeystorePath, + optionKeystorePassword, + optionL1RPCUrls, + optionSettlementRPCUrl, + optionBidderRPCUrl, + optionL1ContractAddr, + optionSettlementThreshold, + optionSettlementTopup, + optionGasTipCap, + optionGasFeeCap, + optionSettlementContractAddr, + optionAutoDepositAmount, + }, + Action: func(c *cli.Context) error { + logger, err := util.NewLogger( + c.String(optionLogLevel.Name), + c.String(optionLogFmt.Name), + c.String(optionLogTags.Name), + c.App.Writer, + ) + if err != nil { + return fmt.Errorf("failed to create logger: %w", err) + } + + gasTipCap, ok := new(big.Int).SetString(c.String(optionGasTipCap.Name), 10) + if !ok { + return fmt.Errorf("failed to parse gas-tip-cap") + } + + gasFeeCap, ok := new(big.Int).SetString(c.String(optionGasFeeCap.Name), 10) + if !ok { + return fmt.Errorf("failed to parse gas-fee-cap") + } + + autoDepositAmount, ok := new(big.Int).SetString(c.String(optionAutoDepositAmount.Name), 10) + if !ok { + return fmt.Errorf("failed to parse auto-deposit-amount") + } + + settlementThreshold, ok := new(big.Int).SetString(c.String(optionSettlementThreshold.Name), 10) + if !ok { + return fmt.Errorf("failed to parse settlement-threshold") + } + + settlementTopup, ok := new(big.Int).SetString(c.String(optionSettlementTopup.Name), 10) + if !ok { + return fmt.Errorf("failed to parse settlement-topup") + } + + signer, err := keysigner.NewKeystoreSigner( + c.String(optionKeystorePath.Name), + c.String(optionKeystorePassword.Name), + ) + if err != nil { + return fmt.Errorf("failed to create signer: %w", err) + } + + if _, err := os.Stat(c.String(optionDataDir.Name)); os.IsNotExist(err) { + if err := os.MkdirAll(c.String(optionDataDir.Name), 0755); err != nil { + return fmt.Errorf("failed to create data directory: %w", err) + } + } + + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) + + config := service.Config{ + HTTPPort: c.Int(optionHTTPPort.Name), + DataDir: c.String(optionDataDir.Name), + Logger: logger, + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + AutoDepositAmount: autoDepositAmount, + SettlementThreshold: settlementThreshold, + SettlementTopup: settlementTopup, + SettlementRPCUrl: c.String(optionSettlementRPCUrl.Name), + BidderRPC: c.String(optionBidderRPCUrl.Name), + L1RPCUrls: c.StringSlice(optionL1RPCUrls.Name), + L1ContractAddr: common.HexToAddress(c.String(optionL1ContractAddr.Name)), + SettlementContractAddr: common.HexToAddress(c.String(optionSettlementContractAddr.Name)), + Signer: signer, + } + + s, err := service.New(&config) + if err != nil { + return fmt.Errorf("failed to create service: %w", err) + } + + <-sigc + logger.Info("shutting down...") + + return s.Close() + }, + } + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + } +} diff --git a/tools/preconf-rpc/pricer/pricer.go b/tools/preconf-rpc/pricer/pricer.go new file mode 100644 index 000000000..8c05cd619 --- /dev/null +++ b/tools/preconf-rpc/pricer/pricer.go @@ -0,0 +1,89 @@ +package pricer + +import ( + "context" + "encoding/json" + "errors" + "io" + "math/big" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/core/types" +) + +var apiURL = "https://api.blocknative.com/gasprices/blockprices?chainid=1" + +type blockPrice struct { + CurrentBlockNumber int64 `json:"currentBlockNumber"` + BlockPrices []struct { + BlockNumber int64 `json:"blockNumber"` + EstimatedPrices []struct { + Confidence int `json:"confidence"` + PriorityFeePerGas float64 `json:"maxPriorityFeePerGas"` + } + } +} + +type BlockPrice struct { + BlockNumber int64 + BidAmount *big.Int +} + +type BidPricer struct{} + +func (b *BidPricer) EstimatePrice(ctx context.Context, txn *types.Transaction) (*BlockPrice, error) { + client := &http.Client{ + Timeout: 10 * time.Second, + } + + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to fetch price estimate: " + resp.Status) + } + + respBuf, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) + if err != nil { + return nil, err + } + + var bp blockPrice + if err := json.Unmarshal(respBuf, &bp); err != nil { + return nil, err + } + + if len(bp.BlockPrices) == 0 { + return nil, errors.New("no block prices available") + } + + for _, price := range bp.BlockPrices { + if price.BlockNumber == bp.CurrentBlockNumber+1 { + for _, p := range price.EstimatedPrices { + if p.Confidence == 99 { // Assuming we want the 99% confidence price + // Convert the priority fee from Gwei to Wei + // 1 Gwei = 1e9 Wei + priorityFee := p.PriorityFeePerGas * 1e9 + bidAmount := big.NewInt(0).Mul(big.NewInt(int64(priorityFee)), big.NewInt(int64(txn.Gas()))) + return &BlockPrice{BlockNumber: price.BlockNumber, BidAmount: bidAmount}, nil + } + } + } + } + + // If we reach here, it means we didn't find a suitable price. + // This could happen if the API response format changes or if no 99% confidence price is available. + return nil, errors.New("no suitable price found for the next block") +} diff --git a/tools/preconf-rpc/pricer/pricer_test.go b/tools/preconf-rpc/pricer/pricer_test.go new file mode 100644 index 000000000..b84d54ad0 --- /dev/null +++ b/tools/preconf-rpc/pricer/pricer_test.go @@ -0,0 +1,42 @@ +package pricer_test + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/primev/mev-commit/tools/preconf-rpc/pricer" +) + +func TestEstimatePrice(t *testing.T) { + t.Parallel() + + bp := pricer.BidPricer{} + + ctx := context.Background() + txn := types.NewTransaction( + 0, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(1000000000), // 1 Gwei + 21000, // gas limit + big.NewInt(1000000000), // gas price + nil, // no data + ) + + price, err := bp.EstimatePrice(ctx, txn) + if err != nil { + t.Fatalf("failed to estimate price: %v", err) + } + + if price.BlockNumber == 0 { + t.Error("expected non-zero block number in estimated price") + } + + if price.BidAmount.Cmp(big.NewInt(0)) <= 0 { + t.Error("expected estimated price to be greater than zero") + } + + t.Logf("Estimated price: %s at block %d", price.BidAmount.String(), price.BlockNumber) +} diff --git a/tools/preconf-rpc/rpcserver/rpcserver.go b/tools/preconf-rpc/rpcserver/rpcserver.go new file mode 100644 index 000000000..e1307a6e0 --- /dev/null +++ b/tools/preconf-rpc/rpcserver/rpcserver.go @@ -0,0 +1,219 @@ +package rpcserver + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "log/slog" + "net/http" + "sync" + "time" +) + +const ( + defaultTimeout = 5 * time.Second + defaultMaxBodySize = 30 * 1024 * 1024 // 30 MB + + CodeParseError = -32700 + CodeInvalidRequest = -32600 + CodeCustomError = -32000 +) + +type JSONErr struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (e *JSONErr) Error() string { + return e.Message +} + +func NewJSONErr(code int, message string) *JSONErr { + return &JSONErr{ + Code: code, + Message: message, + } +} + +type jsonRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id"` + Method string `json:"method"` + Params []any `json:"params"` +} + +type jsonRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id"` + Result *json.RawMessage `json:"result,omitempty"` + Error *jsonRPCError `json:"error,omitempty"` +} + +type methodHandler func(ctx context.Context, params ...interface{}) (json.RawMessage, bool, error) + +type jsonRPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data *any `json:"data,omitempty"` +} + +type JSONRPCServer struct { + rwLock sync.RWMutex + methods map[string]methodHandler + proxyURL string + logger *slog.Logger +} + +func NewJSONRPCServer(proxyURL string, logger *slog.Logger) *JSONRPCServer { + return &JSONRPCServer{ + proxyURL: proxyURL, + methods: make(map[string]methodHandler), + logger: logger, + } +} + +func (s *JSONRPCServer) RegisterHandler(method string, handler methodHandler) { + s.rwLock.Lock() + s.methods[method] = handler + s.rwLock.Unlock() +} + +func (s *JSONRPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + s.logger.Info("Received JSON-RPC request", "method", r.Method) + + if r.Header.Get("Content-Type") != "application/json" { + http.Error(w, "Invalid content type", http.StatusUnsupportedMediaType) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, defaultMaxBodySize) + defer func() { + _ = r.Body.Close() + }() + + body, err := io.ReadAll(r.Body) + if err != nil { + s.writeError(w, nil, CodeInvalidRequest, "Failed to read request body") + return + } + + var req jsonRPCRequest + err = json.Unmarshal(body, &req) + if err != nil { + s.writeError(w, nil, CodeParseError, "Failed to parse request") + return + } + + if req.JSONRPC != "2.0" { + s.writeError(w, nil, CodeInvalidRequest, "Invalid JSON-RPC version") + return + } + + s.logger.Info("Processing JSON-RPC request", "method", req.Method, "id", req.ID) + defer s.logger.Info("Finished processing JSON-RPC request", "method", req.Method, "id", req.ID) + + s.rwLock.RLock() + handler, ok := s.methods[req.Method] + s.rwLock.RUnlock() + if !ok { + s.proxyRequest(w, body) + return + } + + resp, proxy, err := handler(r.Context(), req.Params...) + switch { + case err != nil: + var jsonErr *JSONErr + if ok := errors.As(err, &jsonErr); ok { + // If the error is a JSONErr, we can use it directly. + s.writeError(w, req.ID, jsonErr.Code, jsonErr.Message) + return + } + s.writeError(w, req.ID, CodeCustomError, err.Error()) + return + case proxy: + s.proxyRequest(w, body) + return + case resp == nil: + s.writeError(w, req.ID, CodeCustomError, "No response") + return + } + + s.writeResponse(w, req.ID, &resp) +} + +func (s *JSONRPCServer) writeResponse(w http.ResponseWriter, id any, result *json.RawMessage) { + s.logger.Info("Writing JSON-RPC response", "id", id, "result", result) + response := jsonRPCResponse{ + JSONRPC: "2.0", + ID: id, + Result: result, + Error: nil, + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } +} + +func (s *JSONRPCServer) writeError(w http.ResponseWriter, id any, code int, message string) { + s.logger.Error("JSON-RPC error", "id", id, "code", code, "message", message) + response := jsonRPCResponse{ + JSONRPC: "2.0", + ID: id, + Result: nil, + Error: &jsonRPCError{ + Code: code, + Message: message, + Data: nil, + }, + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to write error response", http.StatusInternalServerError) + return + } +} + +func (s *JSONRPCServer) proxyRequest(w http.ResponseWriter, body []byte) { + client := &http.Client{ + Timeout: defaultTimeout, + } + req, err := http.NewRequest(http.MethodPost, s.proxyURL, bytes.NewReader(body)) + if err != nil { + http.Error(w, "Failed to create proxy request", http.StatusInternalServerError) + return + } + req.Header.Set("Content-Type", "application/json") + + s.logger.Info("Proxying request", "url", s.proxyURL, "body", string(body)) + resp, err := client.Do(req) + if err != nil { + http.Error(w, "Failed to execute proxy request", http.StatusInternalServerError) + return + } + defer func() { + _ = resp.Body.Close() + }() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.StatusCode) + rdr := io.LimitReader(resp.Body, defaultMaxBodySize) + n, err := io.Copy(w, rdr) + if err != nil { + http.Error(w, "Failed to copy proxy response", http.StatusInternalServerError) + return + } + if n == 0 { + http.Error(w, "Empty response from proxy", http.StatusInternalServerError) + return + } +} diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go new file mode 100644 index 000000000..04a912f09 --- /dev/null +++ b/tools/preconf-rpc/service/service.go @@ -0,0 +1,215 @@ +package service + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "log/slog" + "math/big" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" + notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" + "github.com/primev/mev-commit/tools/preconf-rpc/blocktracker" + "github.com/primev/mev-commit/tools/preconf-rpc/handlers" + "github.com/primev/mev-commit/tools/preconf-rpc/pricer" + "github.com/primev/mev-commit/tools/preconf-rpc/rpcserver" + "github.com/primev/mev-commit/tools/preconf-rpc/store" + "github.com/primev/mev-commit/x/accountsync" + "github.com/primev/mev-commit/x/contracts/ethwrapper" + "github.com/primev/mev-commit/x/health" + "github.com/primev/mev-commit/x/keysigner" + bidder "github.com/primev/mev-commit/x/opt-in-bidder" + "github.com/primev/mev-commit/x/transfer" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type Config struct { + Logger *slog.Logger + DataDir string + Signer keysigner.KeySigner + BidderRPC string + AutoDepositAmount *big.Int + L1RPCUrls []string + SettlementRPCUrl string + L1ContractAddr common.Address + SettlementContractAddr common.Address + SettlementThreshold *big.Int + SettlementTopup *big.Int + HTTPPort int + GasTipCap *big.Int + GasFeeCap *big.Int +} + +type Service struct { + cancel context.CancelFunc + closers []io.Closer +} + +func New(config *Config) (*Service, error) { + s := &Service{} + + conn, err := grpc.NewClient( + config.BidderRPC, + grpc.WithTransportCredentials(credentials.NewTLS( + &tls.Config{InsecureSkipVerify: true}, + )), + ) + if err != nil { + return nil, err + } + + s.closers = append(s.closers, conn) + + l1RPCClient, err := ethwrapper.NewClient( + config.Logger.With("module", "ethwrapper"), + config.L1RPCUrls, + ethwrapper.EthClientWithMaxRetries(5), + ) + if err != nil { + return nil, err + } + settlementClient, err := ethclient.Dial(config.SettlementRPCUrl) + if err != nil { + return nil, err + } + + l1ChainID, err := l1RPCClient.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to get L1 chain ID: %w", err) + } + + bidderCli := bidderapiv1.NewBidderClient(conn) + topologyCli := debugapiv1.NewDebugServiceClient(conn) + notificationsCli := notificationsapiv1.NewNotificationsClient(conn) + + status, err := bidderCli.AutoDepositStatus(context.Background(), &bidderapiv1.EmptyMessage{}) + if err != nil { + return nil, err + } + + if !status.IsAutodepositEnabled { + _, err := bidderCli.AutoDeposit( + context.Background(), + &bidderapiv1.DepositRequest{ + Amount: config.AutoDepositAmount.String(), + }, + ) + if err != nil { + return nil, err + } + } + + bridgeConfig := transfer.BridgeConfig{ + Signer: config.Signer, + L1ContractAddr: config.L1ContractAddr, + SettlementContractAddr: config.SettlementContractAddr, + L1RPCUrl: config.L1RPCUrls[0], + SettlementRPCUrl: config.SettlementRPCUrl, + } + + syncer := accountsync.NewAccountSync(config.Signer.GetAddress(), settlementClient) + bridger := transfer.NewBridger( + config.Logger.With("module", "bridger"), + syncer, + bridgeConfig, + config.SettlementThreshold, + config.SettlementTopup, + ) + + bidderClient := bidder.NewBidderClient( + config.Logger.With("module", "bidder"), + bidderCli, + topologyCli, + notificationsCli, + l1RPCClient, + ) + + ctx, cancel := context.WithCancel(context.Background()) + s.cancel = cancel + + healthChecker := health.New() + + bridgerDone := bridger.Start(ctx) + healthChecker.Register(health.CloseChannelHealthCheck("Bridger", bridgerDone)) + s.closers = append(s.closers, channelCloser(bridgerDone)) + + bidderDone := bidderClient.Start(ctx) + healthChecker.Register(health.CloseChannelHealthCheck("BidderService", bidderDone)) + s.closers = append(s.closers, channelCloser(bidderDone)) + + rpcServer := rpcserver.NewJSONRPCServer( + config.L1RPCUrls[0], + config.Logger.With("module", "rpcserver"), + ) + + bidpricer := &pricer.BidPricer{} + + rpcstore, err := store.New(config.DataDir) + if err != nil { + return nil, fmt.Errorf("failed to create store: %w", err) + } + + blockTracker := blocktracker.NewBlockTracker( + l1RPCClient, + config.Logger.With("module", "blocktracker"), + ) + blockTrackerDone := blockTracker.Start(ctx) + healthChecker.Register(health.CloseChannelHealthCheck("BlockTracker", blockTrackerDone)) + + handlers := handlers.NewRPCMethodHandler( + config.Logger.With("module", "handlers"), + bidderClient, + rpcstore, + bidpricer, + blockTracker, + config.Signer.GetAddress(), + l1ChainID, + ) + + handlers.RegisterMethods(rpcServer) + + srv := http.Server{ + Addr: fmt.Sprintf(":%d", config.HTTPPort), + Handler: rpcServer, + } + + go func() { + if err := srv.ListenAndServe(); err != nil { + config.Logger.Error("failed to start HTTP server", "error", err) + } + }() + + s.closers = append(s.closers, &srv) + + return s, nil +} + +func (s *Service) Close() error { + s.cancel() + + for _, c := range s.closers { + if err := c.Close(); err != nil { + return err + } + } + return nil +} + +type channelCloser <-chan struct{} + +func (c channelCloser) Close() error { + select { + case <-c: + case <-time.After(5 * time.Second): + return errors.New("timed out waiting for channel to close") + } + return nil +} diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go new file mode 100644 index 000000000..73432c314 --- /dev/null +++ b/tools/preconf-rpc/store/store.go @@ -0,0 +1,279 @@ +package store + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/cockroachdb/pebble" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" +) + +var ( + ErrInsufficientBalance = errors.New("insufficient balance") +) + +type rpcstore struct { + db *pebble.DB +} + +func New(path string) (*rpcstore, error) { + db, err := pebble.Open(path, &pebble.Options{}) + if err != nil { + return nil, err + } + return &rpcstore{ + db: db, + }, nil +} + +func (s *rpcstore) Close() error { + return errors.Join(s.db.Flush(), s.db.Close()) +} + +func (s *rpcstore) StorePreconfirmedTransaction( + ctx context.Context, + blockNumber int64, + txn *types.Transaction, + commitments []*bidderapiv1.Commitment, +) error { + if blockNumber <= 0 || txn == nil || commitments == nil { + return errors.New("invalid input parameters") + } + + // Serialize the transaction and commitments + txnData, err := txn.MarshalBinary() + if err != nil { + return err + } + + txnDataLenBuf := make([]byte, 8) + binary.BigEndian.PutUint64(txnDataLenBuf, uint64(len(txnData))) + txnDataWithLen := append(txnDataLenBuf, txnData...) + + commitmentsData, err := json.Marshal(commitments) + if err != nil { + return err + } + + // Create a composite key for the block number and transaction hash + key := []byte(fmt.Sprintf("%d:%s", blockNumber, txn.Hash().Hex())) + // Store the transaction and commitments in the database + if err := s.db.Set(key, append(txnDataWithLen, commitmentsData...), nil); err != nil { + return err + } + + blockNumBuf := make([]byte, 8) + binary.BigEndian.PutUint64(blockNumBuf, uint64(blockNumber)) + + txnKey := []byte(fmt.Sprintf("txn:%s", txn.Hash().Hex())) + if err := s.db.Set(txnKey, blockNumBuf, nil); err != nil { + return err + } + + return nil +} + +func (s *rpcstore) GetPreconfirmedTransaction( + ctx context.Context, + txnHash common.Hash, +) (*types.Transaction, []*bidderapiv1.Commitment, error) { + if txnHash == (common.Hash{}) { + return nil, nil, errors.New("transaction hash cannot be empty") + } + + txnKey := []byte(fmt.Sprintf("txn:%s", txnHash.Hex())) + blkNumBuf, closer, err := s.db.Get(txnKey) + if err != nil { + return nil, nil, err + } + + blockNumber := binary.BigEndian.Uint64(blkNumBuf) + if blockNumber == 0 { + return nil, nil, fmt.Errorf("transaction %s not found", txnHash) + } + + _ = closer.Close() // Close the closer from Get + + key := []byte(fmt.Sprintf("%d:%s", blockNumber, txnHash.Hex())) + txnData, closer, err := s.db.Get(key) + if err != nil { + return nil, nil, err + } + defer func() { + _ = closer.Close() + }() + + // The first 8 bytes are the length of the transaction data + txnDataLen := binary.BigEndian.Uint64(txnData[:8]) + + txn := new(types.Transaction) + if err := txn.UnmarshalBinary(txnData[8 : 8+txnDataLen]); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal transaction: %w", err) + } + + var commitments []*bidderapiv1.Commitment + if err := json.Unmarshal(txnData[8+txnDataLen:], &commitments); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal commitments: %w", err) + } + + return txn, commitments, nil +} + +func (s *rpcstore) GetPreconfirmedTransactionsForBlock( + ctx context.Context, + blockNumber int64, +) ([]*types.Transaction, error) { + if blockNumber <= 0 { + return nil, errors.New("invalid block number") + } + + keyPrefix := []byte(fmt.Sprintf("%d:", blockNumber)) + iter, err := s.db.NewIter(&pebble.IterOptions{ + LowerBound: keyPrefix, + UpperBound: append(keyPrefix, 0xFF), + }) + if err != nil { + return nil, fmt.Errorf("failed to create iterator for block %d: %w", blockNumber, err) + } + defer func() { + _ = iter.Close() + }() + + var transactions []*types.Transaction + for iter.First(); iter.Valid(); iter.Next() { + if !bytes.Equal(iter.Key()[:len(keyPrefix)], keyPrefix) { + continue + } + txnData := iter.Value() + if len(txnData) < 8 { + return nil, fmt.Errorf("invalid transaction data length for block %d", blockNumber) + } + txnDataLen := binary.BigEndian.Uint64(txnData[:8]) + if len(txnData) < int(8+txnDataLen) { + return nil, fmt.Errorf("invalid transaction data length for block %d", blockNumber) + } + + txn := new(types.Transaction) + if err := txn.UnmarshalBinary(txnData[8 : 8+txnDataLen]); err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) + } + transactions = append(transactions, txn) + } + return transactions, nil +} + +func (s *rpcstore) DeductBalance( + ctx context.Context, + account common.Address, + amount *big.Int, +) error { + if account == (common.Address{}) || amount == nil || amount.Sign() <= 0 { + return errors.New("invalid account or amount") + } + + balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) + currentBalance, closer, err := s.db.Get(balanceKey) + if err != nil { + return err + } + defer func() { + _ = closer.Close() + }() + + currentBalanceBig := new(big.Int).SetBytes(currentBalance) + if currentBalanceBig.Cmp(amount) < 0 { + return fmt.Errorf("insufficient balance for account %s: %w", account, ErrInsufficientBalance) + } + newBalance := new(big.Int).Sub(currentBalanceBig, amount) + if err := s.db.Set(balanceKey, newBalance.Bytes(), nil); err != nil { + return fmt.Errorf("failed to update balance for account %s: %w", account, err) + } + + return nil +} + +func (s *rpcstore) AddBalance( + ctx context.Context, + account common.Address, + amount *big.Int, +) error { + if account == (common.Address{}) || amount == nil || amount.Sign() <= 0 { + return errors.New("invalid account or amount") + } + + balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) + currentBalance, closer, err := s.db.Get(balanceKey) + if err != nil { + if errors.Is(err, pebble.ErrNotFound) { + // If the account does not exist, we create a new one with the initial balance + bal := new(big.Int) + currentBalance = bal.Bytes() // Default balance for a new account + } else { + return fmt.Errorf("failed to get balance for account %s: %w", account, err) + } + } + defer func() { + if closer != nil { + _ = closer.Close() + } + }() + + currentBalanceBig := new(big.Int).SetBytes(currentBalance) + + newBalance := new(big.Int).Add(currentBalanceBig, amount) + if err := s.db.Set(balanceKey, newBalance.Bytes(), nil); err != nil { + return fmt.Errorf("failed to update balance for account %s: %w", account, err) + } + + return nil +} + +func (s *rpcstore) HasBalance( + ctx context.Context, + account common.Address, + amount *big.Int, +) bool { + if account == (common.Address{}) || amount == nil || amount.Sign() <= 0 { + return false + } + + balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) + currentBalance, closer, err := s.db.Get(balanceKey) + if err != nil { + return false + } + defer func() { + _ = closer.Close() + }() + + currentBalanceBig := new(big.Int).SetBytes(currentBalance) + + return currentBalanceBig.Cmp(amount) >= 0 +} + +func (s *rpcstore) GetBalance( + ctx context.Context, + account common.Address, +) (*big.Int, error) { + if account == (common.Address{}) { + return nil, errors.New("account cannot be empty") + } + + balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) + currentBalance, closer, err := s.db.Get(balanceKey) + if err != nil { + return nil, err + } + defer func() { + _ = closer.Close() + }() + + return new(big.Int).SetBytes(currentBalance), nil +} diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go new file mode 100644 index 000000000..105d30234 --- /dev/null +++ b/tools/preconf-rpc/store/store_test.go @@ -0,0 +1,120 @@ +package store_test + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + "github.com/primev/mev-commit/tools/preconf-rpc/store" +) + +func TestStore(t *testing.T) { + t.Parallel() + + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatalf("failed to create store: %v", err) + } + + t.Cleanup(func() { + if err := st.Close(); err != nil { + t.Errorf("failed to close store: %v", err) + } + }) + + t.Run("StorePreconfirmedTransaction", func(t *testing.T) { + txn := types.NewTransaction( + 0, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(1000000000), // 1 Gwei + 21000, // gas limit + big.NewInt(1000000000), // gas price + nil, // no data + ) + commitments := []*bidderapiv1.Commitment{ + { + TxHashes: []string{txn.Hash().Hex()}, + BidAmount: big.NewInt(1000000000).String(), + BlockNumber: 1, + ReceivedBidDigest: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ReceivedBidSignature: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + CommitmentDigest: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + CommitmentSignature: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + DecayStartTimestamp: time.Now().UnixMilli(), + DecayEndTimestamp: time.Now().Add(24 * time.Hour).UnixMilli(), + }, + { + TxHashes: []string{txn.Hash().Hex()}, + BidAmount: big.NewInt(1000000000).String(), + BlockNumber: 1, + ReceivedBidDigest: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ReceivedBidSignature: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + CommitmentDigest: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + CommitmentSignature: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + DecayStartTimestamp: time.Now().UnixMilli(), + DecayEndTimestamp: time.Now().Add(24 * time.Hour).UnixMilli(), + }, + } + + err := st.StorePreconfirmedTransaction(context.Background(), 1, txn, commitments) + if err != nil { + t.Errorf("failed to store preconfirmed transaction: %v", err) + } + + storedTxn, storedCommitments, err := st.GetPreconfirmedTransaction(context.Background(), txn.Hash()) + if err != nil { + t.Errorf("failed to get preconfirmed transaction: %v", err) + } + + if txn.Hash().Hex() != storedTxn.Hash().Hex() { + t.Errorf("expected transaction hash %s, got %s", txn.Hash().Hex(), storedTxn.Hash().Hex()) + } + if len(storedCommitments) != len(commitments) { + t.Errorf("expected %d commitments, got %d", len(commitments), len(storedCommitments)) + } + + for i, commitment := range commitments { + if diff := cmp.Diff(commitment, storedCommitments[i], cmpopts.IgnoreUnexported(bidderapiv1.Commitment{})); diff != "" { + t.Errorf("commitment mismatch (-want +got):\n%s", diff) + } + } + }) + + t.Run("Account Balance", func(t *testing.T) { + address := common.HexToAddress("0x1234567890123456789012345678901234567890") + initialBalance := big.NewInt(1000000000) // 1 Gwei + + err := st.AddBalance(context.Background(), address, initialBalance) + if err != nil { + t.Errorf("failed to add balance: %v", err) + } + + if !st.HasBalance(context.Background(), address, initialBalance) { + t.Errorf("expected balance %s, but has no balance", initialBalance.String()) + } + + // Check if the balance is correctly stored + balance, err := st.GetBalance(context.Background(), address) + if err != nil { + t.Errorf("failed to get balance: %v", err) + } + if balance.Cmp(initialBalance) != 0 { + t.Errorf("expected balance %s, got %s", initialBalance.String(), balance.String()) + } + + err = st.DeductBalance(context.Background(), address, initialBalance) + if err != nil { + t.Errorf("failed to deduct balance: %v", err) + } + + if st.HasBalance(context.Background(), address, initialBalance) { + t.Errorf("expected no balance after deduction, but still has %s", initialBalance.String()) + } + }) +} diff --git a/tools/instant-bridge/accountsync/accountsync.go b/x/accountsync/accountsync.go similarity index 100% rename from tools/instant-bridge/accountsync/accountsync.go rename to x/accountsync/accountsync.go diff --git a/tools/instant-bridge/accountsync/accountsync_test.go b/x/accountsync/accountsync_test.go similarity index 95% rename from tools/instant-bridge/accountsync/accountsync_test.go rename to x/accountsync/accountsync_test.go index e52813e78..53f2b3bcb 100644 --- a/tools/instant-bridge/accountsync/accountsync_test.go +++ b/x/accountsync/accountsync_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/primev/mev-commit/tools/instant-bridge/accountsync" + "github.com/primev/mev-commit/x/accountsync" ) type mockBalanceGetter struct { diff --git a/x/go.mod b/x/go.mod index 6f840f43d..b3c59f6ba 100644 --- a/x/go.mod +++ b/x/go.mod @@ -7,7 +7,9 @@ toolchain go1.24.0 require ( github.com/ethereum/go-ethereum v1.15.11 github.com/google/go-cmp v0.6.0 + github.com/primev/mev-commit/bridge/standard v0.0.1 github.com/primev/mev-commit/contracts-abi v0.0.1 + github.com/primev/mev-commit/p2p v0.0.1 github.com/prometheus/client_golang v1.19.1 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.28.0 @@ -16,9 +18,11 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.34.2 ) require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -67,9 +71,12 @@ require ( golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) replace github.com/primev/mev-commit/contracts-abi => ../contracts-abi + +replace github.com/primev/mev-commit/bridge/standard => ../bridge/standard + +replace github.com/primev/mev-commit/p2p => ../p2p diff --git a/x/go.sum b/x/go.sum index 6bf72b15c..9c7fc382a 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,3 +1,5 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1 h1:AmmAwHbvaeOIxDKG2+aTn5C36HjmFIMkrdTp49rp80Q= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1/go.mod h1:tiTMKD8j6Pd/D2WzREoweufjzaJKHZg35f/VGcZ2v3I= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -78,8 +80,10 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -151,6 +155,7 @@ github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks= github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc= @@ -228,12 +233,15 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tools/instant-bridge/bidder/bidder.go b/x/opt-in-bidder/bidder.go similarity index 74% rename from tools/instant-bridge/bidder/bidder.go rename to x/opt-in-bidder/bidder.go index 02eea2e1d..69a7cc281 100644 --- a/tools/instant-bridge/bidder/bidder.go +++ b/x/opt-in-bidder/bidder.go @@ -1,8 +1,9 @@ -package bidder +package optinbidder import ( "context" "errors" + "fmt" "io" "log/slog" "math/big" @@ -23,6 +24,7 @@ const ( var ( ErrNoEpochInfo = errors.New("no epoch info available") ErrNoSlotInCurrentEpoch = errors.New("no slot available in current epoch") + ErrNoProviders = errors.New("no connected providers found") ) var nowFunc = time.Now @@ -172,22 +174,36 @@ const ( BidStatusNoOfProviders BidStatusType = iota BidStatusWaitSecs BidStatusAttempted - BidStatusSucceeded BidStatusFailed + BidStatusCancelled + BidStatusCommitment ) type BidStatus struct { Type BidStatusType - Arg1 int - Arg2 string + Arg any +} + +type BidOpts struct { + WaitForOptIn bool + BlockNumber uint64 +} + +var defaultBidOpts = &BidOpts{ + WaitForOptIn: true, } func (b *BidderClient) Bid( ctx context.Context, bidAmount *big.Int, - bridgeAmount *big.Int, + slashAmount *big.Int, rawTx string, + opts *BidOpts, ) (chan BidStatus, error) { + if opts == nil { + opts = defaultBidOpts + } + topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) if err != nil { b.logger.Error("failed to get topology", "error", err) @@ -196,7 +212,7 @@ func (b *BidderClient) Bid( providers := topo.Topology.Fields["connected_providers"].GetListValue() if providers == nil || len(providers.Values) == 0 { - return nil, errors.New("no connected providers") + return nil, ErrNoProviders } // Channel length chosen is 3 so that sending the bid is not blocked by the first @@ -204,60 +220,72 @@ func (b *BidderClient) Bid( res := make(chan BidStatus, 3) b.bigWg.Add(1) go func() { + defer fmt.Println("BidderClient goroutine exiting") defer close(res) defer b.bigWg.Done() - res <- BidStatus{Type: BidStatusNoOfProviders, Arg1: len(providers.Values)} + res <- BidStatus{Type: BidStatusNoOfProviders, Arg: len(providers.Values)} - nextSlot, err := b.getNextSlot() - if err != nil { - b.logger.Error("failed to get next slot", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg2: err.Error()} - return - } - - bidTime := nextSlot.startTime.Add(-1 * time.Second) - wait := bidTime.Sub(nowFunc()) - res <- BidStatus{Type: BidStatusWaitSecs, Arg1: int(wait.Seconds())} - - if wait > 0 { - b.logger.Info("waiting for next slot", "wait", wait) - select { - case <-time.After(wait): - case <-ctx.Done(): - res <- BidStatus{Type: BidStatusFailed, Arg2: ctx.Err().Error()} + if opts.WaitForOptIn { + nextSlot, err := b.getNextSlot() + if err != nil { + b.logger.Error("failed to get next slot", "error", err) + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} return } + + bidTime := nextSlot.startTime.Add(-1 * time.Second) + wait := bidTime.Sub(nowFunc()) + res <- BidStatus{Type: BidStatusWaitSecs, Arg: int(wait.Seconds())} + + if wait > 0 { + b.logger.Info("waiting for next slot", "wait", wait) + select { + case <-time.After(wait): + case <-ctx.Done(): + res <- BidStatus{Type: BidStatusCancelled, Arg: ctx.Err().Error()} + return + } + } } - blkNumber, err := b.blkNumberGetter.BlockNumber(ctx) - if err != nil { - b.logger.Error("failed to get block number", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg2: err.Error()} - return + blkNumber := opts.BlockNumber + if blkNumber == 0 { + bNo, err := b.blkNumberGetter.BlockNumber(ctx) + if err != nil { + b.logger.Error("failed to get block number", "error", err) + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} + return + } + blkNumber = bNo + 1 } - res <- BidStatus{Type: BidStatusAttempted, Arg1: int(blkNumber + 1)} + res <- BidStatus{Type: BidStatusAttempted, Arg: blkNumber} + b.logger.Info( + "attempting to send bid", + "blockNumber", blkNumber, + "bidAmount", bidAmount, + "slashAmount", slashAmount, + ) pc, err := b.bidderClient.SendBid(ctx, &bidderapiv1.Bid{ Amount: bidAmount.String(), - BlockNumber: int64(blkNumber + 1), + BlockNumber: int64(blkNumber), RawTransactions: []string{rawTx}, - DecayStartTimestamp: nowFunc().UnixMilli(), + DecayStartTimestamp: nowFunc().Add(100 * time.Millisecond).UnixMilli(), DecayEndTimestamp: nowFunc().Add(12 * time.Second).UnixMilli(), - SlashAmount: bridgeAmount.String(), + SlashAmount: slashAmount.String(), }) if err != nil { b.logger.Error("failed to send bid", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg2: err.Error()} + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} return } - commitments := make([]*bidderapiv1.Commitment, 0) for { select { case <-ctx.Done(): - res <- BidStatus{Type: BidStatusFailed, Arg2: ctx.Err().Error()} + res <- BidStatus{Type: BidStatusCancelled, Arg: ctx.Err().Error()} return default: } @@ -265,26 +293,20 @@ func (b *BidderClient) Bid( msg, err := pc.Recv() if err != nil { if errors.Is(err, io.EOF) { - break + return + } + if errors.Is(err, context.Canceled) { + res <- BidStatus{Type: BidStatusCancelled, Arg: err.Error()} + return } b.logger.Error("failed to receive commitment", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg2: err.Error()} + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} return } - commitments = append(commitments, msg) + res <- BidStatus{Type: BidStatusCommitment, Arg: msg} } - if len(commitments) == len(providers.Values) { - b.logger.Info("all commitments received") - } else { - b.logger.Warn( - "not all commitments received", - "received", len(commitments), - "expected", len(providers.Values), - ) - } - res <- BidStatus{Type: BidStatusSucceeded, Arg1: len(commitments)} }() return res, nil diff --git a/tools/instant-bridge/bidder/bidder_test.go b/x/opt-in-bidder/bidder_test.go similarity index 82% rename from tools/instant-bridge/bidder/bidder_test.go rename to x/opt-in-bidder/bidder_test.go index d4bca666a..9a8bdb81b 100644 --- a/tools/instant-bridge/bidder/bidder_test.go +++ b/x/opt-in-bidder/bidder_test.go @@ -1,4 +1,4 @@ -package bidder_test +package optinbidder_test import ( "context" @@ -13,7 +13,7 @@ import ( bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" - "github.com/primev/mev-commit/tools/instant-bridge/bidder" + optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" "github.com/primev/mev-commit/x/util" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/structpb" @@ -118,7 +118,7 @@ func TestBidderClient(t *testing.T) { now: clock, } - bidder.SetNowFunc(timeSetter.Now) + optinbidder.SetNowFunc(timeSetter.Now) topoVal, err := structpb.NewStruct(map[string]interface{}{ "connected_providers": []any{"provider1", "provider2"}, @@ -137,7 +137,7 @@ func TestBidderClient(t *testing.T) { } blockNumberGetter := &testBlockNumberGetter{blockNumber: 10} - bidderClient := bidder.NewBidderClient( + bidderClient := optinbidder.NewBidderClient( util.NewTestLogger(os.Stdout), rpcServices, rpcServices, @@ -149,8 +149,8 @@ func TestBidderClient(t *testing.T) { done := bidderClient.Start(ctx) _, err = bidderClient.Estimate() - if err != bidder.ErrNoEpochInfo { - t.Fatalf("expected error %v, got %v", bidder.ErrNoEpochInfo, err) + if err != optinbidder.ErrNoEpochInfo { + t.Fatalf("expected error %v, got %v", optinbidder.ErrNoEpochInfo, err) } // Send a notification. @@ -195,7 +195,7 @@ func TestBidderClient(t *testing.T) { _, _ = rand.Read(buf) txString := hex.EncodeToString(buf) - _, err = bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString) + _, err = bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) if err == nil { t.Fatal("expected error, got nil") } @@ -204,30 +204,37 @@ func TestBidderClient(t *testing.T) { Topology: topoVal, } - statusC, err := bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString) + statusC, err := bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) if err != nil { t.Fatal(err) } -WaitLoop: + commitments := 0 +waitLoop: for { select { - case status := <-statusC: + case status, more := <-statusC: + if !more { + break waitLoop + } switch status.Type { - case bidder.BidStatusNoOfProviders: - if status.Arg1 != 2 { - t.Fatalf("expected 2 providers, got %d", status.Arg1) + case optinbidder.BidStatusNoOfProviders: + if status.Arg.(int) != 2 { + t.Fatalf("expected 2 providers, got %d", status.Arg) + } + case optinbidder.BidStatusWaitSecs: + if status.Arg.(int) != 2 { + t.Fatalf("expected 2 seconds, got %d", status.Arg) } - case bidder.BidStatusWaitSecs: - if status.Arg1 != 2 { - t.Fatalf("expected 2 seconds, got %d", status.Arg1) + case optinbidder.BidStatusAttempted: + if status.Arg.(uint64) != 11 { + t.Fatalf("expected 11, got %d", status.Arg) } - case bidder.BidStatusAttempted: - if status.Arg1 != 11 { - t.Fatalf("expected 11, got %d", status.Arg1) + case optinbidder.BidStatusCommitment: + if status.Arg.(*bidderapiv1.Commitment).BlockNumber != 11 { + t.Fatalf("expected block number 11, got %d", status.Arg.(*bidderapiv1.Commitment).BlockNumber) } - case bidder.BidStatusSucceeded: - break WaitLoop + commitments++ } case bid := <-rpcServices.bidChan: if bid.Amount != big.NewInt(1).String() { @@ -249,6 +256,10 @@ WaitLoop: } } + if commitments != 2 { + t.Fatalf("expected 2 commitments, got %d", commitments) + } + cancel() <-done } diff --git a/tools/instant-bridge/bidder/export_test.go b/x/opt-in-bidder/export_test.go similarity index 77% rename from tools/instant-bridge/bidder/export_test.go rename to x/opt-in-bidder/export_test.go index 35e7047a0..31674fc5f 100644 --- a/tools/instant-bridge/bidder/export_test.go +++ b/x/opt-in-bidder/export_test.go @@ -1,4 +1,4 @@ -package bidder +package optinbidder import "time" diff --git a/tools/instant-bridge/transfer/bridger.go b/x/transfer/bridger.go similarity index 92% rename from tools/instant-bridge/transfer/bridger.go rename to x/transfer/bridger.go index 6380daa8b..e007407f6 100644 --- a/tools/instant-bridge/transfer/bridger.go +++ b/x/transfer/bridger.go @@ -6,10 +6,12 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/primev/mev-commit/bridge/standard/pkg/transfer" + bridgetransfer "github.com/primev/mev-commit/bridge/standard/pkg/transfer" "github.com/primev/mev-commit/x/keysigner" ) +var transferFunc = bridgetransfer.NewTransferToSettlement + type AccountSyncer interface { Subscribe(ctx context.Context, threshold *big.Int) <-chan struct{} } @@ -64,7 +66,7 @@ func (b *Bridger) Start(ctx context.Context) <-chan struct{} { "topup", b.topup, "address", b.config.Signer.GetAddress().Hex(), ) - tx, err := transfer.NewTransferToSettlement( + tx, err := transferFunc( b.topup, b.config.Signer.GetAddress(), b.config.Signer, diff --git a/x/transfer/bridger_test.go b/x/transfer/bridger_test.go new file mode 100644 index 000000000..61e38ca35 --- /dev/null +++ b/x/transfer/bridger_test.go @@ -0,0 +1,153 @@ +package transfer_test + +import ( + "context" + "log/slog" + "math/big" + "os" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + bridgetransfer "github.com/primev/mev-commit/bridge/standard/pkg/transfer" + "github.com/primev/mev-commit/x/keysigner" + "github.com/primev/mev-commit/x/transfer" +) + +type MockAccountSyncer struct { + trigger chan struct{} +} + +func (m *MockAccountSyncer) Subscribe(ctx context.Context, threshold *big.Int) <-chan struct{} { + ch := make(chan struct{}) + go func() { + <-m.trigger + close(ch) + }() + return ch +} + +type MockTransfer struct { + mtx sync.Mutex + called int + amount *big.Int +} + +func (m *MockTransfer) Do(ctx context.Context) <-chan bridgetransfer.TransferStatus { + ch := make(chan bridgetransfer.TransferStatus, 1) + ch <- bridgetransfer.TransferStatus{ + Message: "Transfer Done", + Error: nil, + } + close(ch) + m.mtx.Lock() + m.called++ + m.mtx.Unlock() + return ch +} + +func (m *MockTransfer) getAmount() *big.Int { + m.mtx.Lock() + defer m.mtx.Unlock() + return m.amount +} + +func (m *MockTransfer) setAmount(amount *big.Int) { + m.mtx.Lock() + defer m.mtx.Unlock() + m.amount = amount +} + +func (m *MockTransfer) calls() int { + m.mtx.Lock() + defer m.mtx.Unlock() + return m.called +} + +type keySigner struct { + keysigner.KeySigner +} + +func (k *keySigner) GetAddress() common.Address { + return common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") +} + +func TestBridger(t *testing.T) { + t.Parallel() + + syncer := &MockAccountSyncer{ + trigger: make(chan struct{}), + } + + txfer := &MockTransfer{} + + done := transfer.SetTransferFunc(func( + amount *big.Int, + destAddress common.Address, + signer keysigner.KeySigner, + settlementRPCUrl string, + l1RPCUrl string, + l1ContractAddr common.Address, + settlementContractAddr common.Address, + ) (bridgetransfer.Transfer, error) { + txfer.setAmount(amount) + return txfer, nil + }, + ) + t.Cleanup(done) + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + bridger := transfer.NewBridger( + logger, + syncer, + transfer.BridgeConfig{ + Signer: &keySigner{}, + L1ContractAddr: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), + SettlementContractAddr: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), + L1RPCUrl: "http://localhost:8545", + SettlementRPCUrl: "http://localhost:8546", + }, + big.NewInt(1000000000000000000), + big.NewInt(1000000000000000000), + ) + ctx, cancel := context.WithCancel(context.Background()) + closed := bridger.Start(ctx) + + // Simulate the syncer triggering the event + syncer.trigger <- struct{}{} + + start := time.Now() + for { + if time.Since(start) > 5*time.Second { + t.Fatal("Timeout waiting for transfer to be called") + } + if txfer.calls() == 1 { + break + } + } + + if txfer.getAmount().Cmp(big.NewInt(1000000000000000000)) != 0 { + t.Fatalf("Expected amount to be 1 ETH, got %s", txfer.amount.String()) + } + + syncer.trigger <- struct{}{} + + start = time.Now() + for { + if time.Since(start) > 5*time.Second { + t.Fatal("Timeout waiting for transfer to be called again") + } + if txfer.calls() == 2 { + break + } + } + + if txfer.getAmount().Cmp(big.NewInt(1000000000000000000)) != 0 { + t.Fatalf("Expected amount to be 1 ETH, got %s", txfer.amount.String()) + } + + cancel() + <-closed + +} diff --git a/x/transfer/export_test.go b/x/transfer/export_test.go new file mode 100644 index 000000000..b6422acc9 --- /dev/null +++ b/x/transfer/export_test.go @@ -0,0 +1,25 @@ +package transfer + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + bridgetransfer "github.com/primev/mev-commit/bridge/standard/pkg/transfer" + "github.com/primev/mev-commit/x/keysigner" +) + +func SetTransferFunc(f func( + amount *big.Int, + destAddress common.Address, + signer keysigner.KeySigner, + settlementRPCUrl string, + l1RPCUrl string, + l1ContractAddr common.Address, + settlementContractAddr common.Address, +) (bridgetransfer.Transfer, error)) func() { + prev := transferFunc + transferFunc = f + return func() { + transferFunc = prev + } +} diff --git a/tools/instant-bridge/transfer/transfer.go b/x/transfer/transfer.go similarity index 57% rename from tools/instant-bridge/transfer/transfer.go rename to x/transfer/transfer.go index 4ad8c9094..4e0f995fe 100644 --- a/tools/instant-bridge/transfer/transfer.go +++ b/x/transfer/transfer.go @@ -11,50 +11,82 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/primev/mev-commit/x/keysigner" ) +type EthClient interface { + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + SendTransaction(ctx context.Context, tx *types.Transaction) error + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) +} + +type Signer interface { + GetAddress() common.Address + SignTx(tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) +} + type Transferer struct { - mtx sync.Mutex - logger *slog.Logger - client *ethclient.Client - l1ChainID *big.Int - settlementChainID *big.Int - signer keysigner.KeySigner - gasTip *big.Int - gasFeeCap *big.Int + mtx sync.Mutex + logger *slog.Logger + client EthClient + signer Signer + gasTip *big.Int + gasFeeCap *big.Int } func NewTransferer( logger *slog.Logger, - client *ethclient.Client, - l1ChainID *big.Int, - settlementChainID *big.Int, - signer keysigner.KeySigner, + client EthClient, + signer Signer, gasTip *big.Int, gasFeeCap *big.Int, ) *Transferer { return &Transferer{ - logger: logger, - client: client, - l1ChainID: l1ChainID, - settlementChainID: settlementChainID, - signer: signer, - gasTip: gasTip, - gasFeeCap: gasFeeCap, + logger: logger, + client: client, + signer: signer, + gasTip: gasTip, + gasFeeCap: gasFeeCap, } } -func (t *Transferer) TransferOnSettlement( +func (t *Transferer) Transfer( ctx context.Context, to common.Address, + chainID *big.Int, amount *big.Int, ) error { // Only one transfer at a time t.mtx.Lock() defer t.mtx.Unlock() + if to == (common.Address{}) { + t.logger.Error("invalid address") + return errors.New("invalid address") + } + + if amount.Sign() <= 0 { + t.logger.Error("invalid amount") + return errors.New("invalid amount") + } + + if chainID.Cmp(big.NewInt(0)) <= 0 { + t.logger.Error("invalid chain ID") + return errors.New("invalid chain ID") + } + + // Check if the account is a contract + code, err := t.client.CodeAt(ctx, to, nil) + if err != nil { + t.logger.Error("failed to get code", "error", err) + return err + } + + if len(code) > 0 { + t.logger.Error("address is a contract") + return errors.New("address is a contract") + } + nonce, err := t.client.PendingNonceAt(ctx, t.signer.GetAddress()) if err != nil { t.logger.Error("failed to get nonce", "error", err) @@ -62,6 +94,7 @@ func (t *Transferer) TransferOnSettlement( } txData := &types.DynamicFeeTx{ To: &to, + ChainID: chainID, Nonce: nonce, GasFeeCap: t.gasFeeCap, GasTipCap: t.gasTip, @@ -71,7 +104,7 @@ func (t *Transferer) TransferOnSettlement( tx := types.NewTx(txData) - signedTx, err := t.signer.SignTx(tx, t.settlementChainID) + signedTx, err := t.signer.SignTx(tx, chainID) if err != nil { t.logger.Error("failed to sign tx", "error", err) return err @@ -97,7 +130,7 @@ func (t *Transferer) TransferOnSettlement( return nil } -func (t *Transferer) ValidateL1Tx(rawTx string) (*types.Transaction, error) { +func (t *Transferer) ValidateTx(rawTx string, chainID *big.Int) (*types.Transaction, error) { txBytes, err := hex.DecodeString(rawTx) if err != nil { t.logger.Error("failed to decode tx", "error", err) @@ -110,8 +143,8 @@ func (t *Transferer) ValidateL1Tx(rawTx string) (*types.Transaction, error) { return nil, err } - if tx.ChainId().Cmp(t.l1ChainID) != 0 { - t.logger.Error("tx has wrong chain ID", "chainID", tx.ChainId(), "expected", t.l1ChainID) + if tx.ChainId().Cmp(chainID) != 0 { + t.logger.Error("tx has wrong chain ID", "chainID", tx.ChainId(), "expected", chainID) return nil, errors.New("tx has wrong chain ID") } diff --git a/x/transfer/transfer_test.go b/x/transfer/transfer_test.go new file mode 100644 index 000000000..7b2a8d6a1 --- /dev/null +++ b/x/transfer/transfer_test.go @@ -0,0 +1,97 @@ +package transfer_test + +import ( + "context" + "log/slog" + "math/big" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/primev/mev-commit/x/transfer" +) + +type MockEthClient struct{} + +func (m *MockEthClient) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + return 0, nil +} + +func (m *MockEthClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { + return nil +} + +func (m *MockEthClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + return &types.Receipt{ + Status: 1, + }, nil +} + +func (m *MockEthClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { + return []byte{}, nil +} + +type MockKeySigner struct { + transaction *types.Transaction +} + +func (m *MockKeySigner) GetAddress() common.Address { + return common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") +} + +func (m *MockKeySigner) SignTx(tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + m.transaction = tx + return tx, nil +} + +func TestTransferrer(t *testing.T) { + t.Parallel() + + // Mock the EthClient and KeySigner interfaces + client := new(MockEthClient) + signer := new(MockKeySigner) + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + gasTip := big.NewInt(1000000000) // 1 Gwei + gasFeeCap := big.NewInt(2000000000) // 2 Gwei + transferer := transfer.NewTransferer(logger, client, signer, gasTip, gasFeeCap) + + // Mock the context + ctx := context.Background() + // Mock the address and amount + to := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + chainID := big.NewInt(1) // Mainnet + amount := big.NewInt(1000000000000000000) // 1 ETH + + // Call the Transfer method + err := transferer.Transfer(ctx, to, chainID, amount) + if err != nil { + t.Fatalf("Transfer failed: %v", err) + } + + // Check if the transaction was signed correctly + if signer.transaction == nil { + t.Fatal("Transaction was not signed") + } + // Check if the transaction was sent correctly + if signer.transaction.To() == nil { + t.Fatal("Transaction was not sent") + } + if signer.transaction.Value().Cmp(amount) != 0 { + t.Fatalf("Transaction amount mismatch: expected %s, got %s", amount.String(), signer.transaction.Value().String()) + } + if signer.transaction.GasTipCap().Cmp(gasTip) != 0 { + t.Fatalf("Transaction gas tip cap mismatch: expected %s, got %s", gasTip.String(), signer.transaction.GasTipCap().String()) + } + if signer.transaction.GasFeeCap().Cmp(gasFeeCap) != 0 { + t.Fatalf("Transaction gas fee cap mismatch: expected %s, got %s", gasFeeCap.String(), signer.transaction.GasFeeCap().String()) + } + // Check if the transaction was sent to the correct address + if signer.transaction.To().Hex() != to.Hex() { + t.Fatalf("Transaction to address mismatch: expected %s, got %s", to.Hex(), signer.transaction.To().Hex()) + } + // Check if the transaction was sent with the correct chain ID + if signer.transaction.ChainId().Cmp(chainID) != 0 { + t.Fatalf("Transaction chain ID mismatch: expected %s, got %s", chainID.String(), signer.transaction.ChainId().String()) + } +}