diff --git a/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 b/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 index c184c8e28..5c7b1e0d5 100644 --- a/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 +++ b/infrastructure/nomad/playbooks/templates/jobs/preconf-rpc.nomad.j2 @@ -42,9 +42,119 @@ job "{{ job.name }}" { port = "{{ port_name }}" tags = ["{{ port_name }}"] provider = "nomad" + {% if port_name == "http" %} + check { + type = "http" + path = "/health" + interval = "10s" + timeout = "2s" + } + {% endif %} } {% endfor %} + task "db" { + driver = "exec" + + lifecycle { + hook = "prestart" + sidecar = true + } + + {% if profile == 'testnet' or profile == 'mainnet' %} + resources { + cores = 4 + memory = 8192 + } + {% elif profile == 'stressnet' or profile == 'stressnet-wl1' %} + resources { + memory = 4096 + } + {% endif %} + + template { + data = <<-EOH + POSTGRES_VERSION="15" + POSTGRES_DB="preconf-rpc" + POSTGRES_USERNAME="preconf-rpc" + POSTGRES_PASSWORD="{{ lookup('password', '/dev/null', length=64) }}" + {%- raw %} + POSTGRES_DATA="/local/pgdata-{{ env "NOMAD_ALLOC_INDEX" }}" + {{- range nomadService "{% endraw %}{{ job.name }}{% raw %}" }} + {{- if contains "db" .Tags }} + POSTGRES_PORT="{{ .Port }}" + {{- end }} + {{- end }} + {% endraw %} + EOH + destination = "alloc/data/postgres.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 }} + + if [ -d "${POSTGRES_DATA}" ]; then + echo "Initialized and configured database found." + cp "${POSTGRES_DATA}/.env" /alloc/data/postgres.env + source "${POSTGRES_DATA}/.env" + postgres -D ${POSTGRES_DATA} + exit $? + fi + + export PATH="/usr/lib/postgresql/${POSTGRES_VERSION}/bin:${PATH}" + mkdir -p /var/run/postgresql > /dev/null 2>&1 + pg_ctl initdb --silent --pgdata="${POSTGRES_DATA}" + if [ $? -ne 0 ]; then + echo "Failed to initialize PostgreSQL." + exit 1 + fi + cp /alloc/data/postgres.env "${POSTGRES_DATA}/.env" + + pg_ctl start --pgdata="${POSTGRES_DATA}" --silent --wait --timeout=300 > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Failed to start PostgreSQL." + exit 1 + fi + + createuser --superuser postgres > /dev/null 2>&1 + createuser --username=postgres --createdb "${POSTGRES_USERNAME}" + createdb --username="${POSTGRES_USERNAME}" "${POSTGRES_DB}" + psql --quiet \ + --username="${POSTGRES_USERNAME}" \ + --dbname="${POSTGRES_DB}" \ + --command="ALTER USER ${POSTGRES_USERNAME} WITH PASSWORD '${POSTGRES_PASSWORD}'; \ + GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_USERNAME};" + echo "Database initialized and configured successfully." + + pg_ctl stop --pgdata="${POSTGRES_DATA}" --silent --wait --timeout=300 > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Failed to stop PostgreSQL." + exit 1 + fi + + exec postgres -D "${POSTGRES_DATA}" -p "${POSTGRES_PORT}" + {% endraw %} + EOH + destination = "local/run.sh" + change_mode = "noop" + perms = "0755" + } + + config { + command = "bash" + args = ["-c", "exec local/run.sh"] + } + } + task "preconfrpc" { driver = "exec" @@ -90,6 +200,12 @@ job "{{ job.name }}" { PRECONF_RPC_BIDDER_RPC_URL="{{ .Address }}:{{ .Port }}" {{- end }} {{- end }} + {{- range nomadService "{% endraw %}{{ job.name }}{% raw %}" }} + {{- if contains "db" .Tags }} + PRECONF_RPC_PG_HOST="localhost" + PRECONF_RPC_PG_PORT="{{ .Port }}" + {{- end }} + {{- end }} {% endraw %} XDG_CONFIG_HOME="local/.config" {% if profile == 'preconf-rpc-test' %} @@ -177,6 +293,19 @@ job "{{ job.name }}" { echo "Failed to send funds to: ${ADDRESS}" fi {{- end }} + + source alloc/data/postgres.env + export PRECONF_RPC_PG_USER="${POSTGRES_USERNAME}" + export PRECONF_RPC_PG_PASSWORD="${POSTGRES_PASSWORD}" + export PRECONF_RPC_PG_DBNAME="${POSTGRES_DB}" + + export PRECONF_RPC_DEPOSIT_ADDRESS="$(echo '{{ $secret.Data.data.deposit_keystore }}' | jq -r '.address')" + export PRECONF_RPC_BRIDGE_ADDRESS="$(echo '{{ $secret.Data.data.bridge_keystore }}' | jq -r '.address')" + + if ! timeout 5m bash -c 'until pg_isready -h ${PRECONF_RPC_PG_HOST} -p ${PRECONF_RPC_PG_PORT} -U ${PRECONF_RPC_PG_USER} -d ${PRECONF_RPC_PG_DBNAME}; do sleep 2; done'; then + echo "Waiting for PostgreSQL to start..." + sleep 3 + fi {% endraw %} chmod +x local/preconf-rpc diff --git a/infrastructure/nomad/playbooks/variables/profiles.yml b/infrastructure/nomad/playbooks/variables/profiles.yml index fc2f0256c..2f651a713 100644 --- a/infrastructure/nomad/playbooks/variables/profiles.yml +++ b/infrastructure/nomad/playbooks/variables/profiles.yml @@ -748,10 +748,16 @@ jobs: - *preconf_rpc_artifact - keystores: preconf_rpc_keystore: + deposit_keystore: + bridge_keystore: count: 1 target: *mev_commit_bidder_node1_job ports: - - http: + - db: + static: 5434 + to: 5434 + http: + static: 10545 to: 8080 env: l1_chain_id: "{{ environments[env].chain_id }}" diff --git a/tools/go.mod b/tools/go.mod index ebe3e9044..2b36901fd 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -23,29 +23,49 @@ require ( 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 + github.com/testcontainers/testcontainers-go v0.27.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 + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/containerd v1.7.11 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v25.0.6+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect - github.com/getsentry/sentry-go v0.28.1 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // 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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.11 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect ) @@ -95,7 +115,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // 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/sync v0.11.0 golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect diff --git a/tools/go.sum b/tools/go.sum index a689056f3..4730aca99 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,11 +1,19 @@ 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= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 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= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= @@ -14,14 +22,14 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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= @@ -38,6 +46,12 @@ github.com/consensys/bavard v0.1.27 h1:j6hKUrGAy/H+gpNrpLU3I26n1yc+VMGmd6ID5+gAh github.com/consensys/bavard v0.1.27/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/gnark-crypto v0.16.0 h1:8Dl4eYmUWK9WmlP1Bj6je688gBRJCJbT8Mw4KoTAawo= github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpLvzbcE2MqljY7diU= +github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= +github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= @@ -46,7 +60,10 @@ 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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -57,6 +74,14 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg= +github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/domDngBU= @@ -65,6 +90,8 @@ github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cn github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk= github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -73,9 +100,13 @@ 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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.2.6/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= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -88,6 +119,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS 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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= @@ -146,6 +179,10 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -163,16 +200,28 @@ github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= 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= @@ -184,11 +233,13 @@ 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.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -199,7 +250,6 @@ 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= @@ -208,16 +258,35 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ= +github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/testcontainers/testcontainers-go v0.27.0 h1:IeIrJN4twonTDuMuBNQdKZ+K97yd7VrmNGu+lDpYcDk= +github.com/testcontainers/testcontainers-go v0.27.0/go.mod h1:+HgYZcd17GshBUZv9b+jKFJ198heWPQq3KQIp2+N+7U= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= @@ -226,6 +295,24 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC 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= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 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= @@ -250,7 +337,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h 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.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= @@ -284,9 +377,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index c45d2248c..8ff8e9d18 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -4,13 +4,11 @@ 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" @@ -18,12 +16,7 @@ import ( 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 + "github.com/primev/mev-commit/tools/preconf-rpc/sender" ) var ( @@ -32,101 +25,104 @@ var ( 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) + 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 + GetTransactionByHash(ctx context.Context, txnHash common.Hash) (*sender.Transaction, error) + GetTransactionsForBlock(ctx context.Context, blockNumber int64) ([]*sender.Transaction, error) + GetTransactionCommitments(ctx context.Context, txnHash common.Hash) ([]*bidderapiv1.Commitment, error) GetBalance(ctx context.Context, account common.Address) (*big.Int, error) - AddBalance(ctx context.Context, account common.Address, amount *big.Int) error + GetCurrentNonce(ctx context.Context, account common.Address) uint64 } 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 Sender interface { + Enqueue(ctx context.Context, txn *sender.Transaction) error } 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 + logger *slog.Logger + pricer Pricer + bidder Bidder + store Store + blockTracker BlockTracker + sndr Sender + depositAddress common.Address + bridgeAddress common.Address + chainID *big.Int } func NewRPCMethodHandler( logger *slog.Logger, + pricer Pricer, bidder Bidder, store Store, - pricer Pricer, blockTracker BlockTracker, - owner common.Address, + sndr Sender, + depositAddress common.Address, + bridgeAddress 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), + logger: logger, + pricer: pricer, + bidder: bidder, + store: store, + blockTracker: blockTracker, + sndr: sndr, + depositAddress: depositAddress, + bridgeAddress: bridgeAddress, + chainID: chainId, } } 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_getBlockNumber", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + blockNumber := h.blockTracker.LatestBlockNumber() + + 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 + }) + server.RegisterHandler("eth_chainId", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + 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 + }) + server.RegisterHandler("eth_maxPriorityFeePerGas", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + // Return zero value for maxPriorityFeePerGas + maxPriorityFee := big.NewInt(0) + maxPriorityFeeJSON, err := json.Marshal(hexutil.EncodeBig(maxPriorityFee)) + if err != nil { + h.logger.Error("Failed to marshal maxPriorityFeePerGas to JSON", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal maxPriorityFeePerGas", + ) + } + return maxPriorityFeeJSON, false, nil + }) server.RegisterHandler("eth_sendRawTransaction", h.handleSendRawTx) server.RegisterHandler("eth_getTransactionReceipt", h.handleGetTxReceipt) server.RegisterHandler("eth_getTransactionCount", h.handleGetTxCount) @@ -134,56 +130,85 @@ func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { // 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", + server.RegisterHandler("mevcommit_optInBlock", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + timeToOptIn, err := h.bidder.Estimate() + if err != nil { + h.logger.Error("Failed to estimate time to opt in", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to estimate opt in time", + ) + } + return json.RawMessage(fmt.Sprintf(`{"timeInSecs": "%d"}`, timeToOptIn)), false, nil + }) + server.RegisterHandler("mevcommit_estimateDeposit", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + blockPrice, err := h.pricer.EstimatePrice( + ctx, + types.NewTransaction(0, h.depositAddress, big.NewInt(0), 21000, big.NewInt(0), nil), ) - } + if err != nil { + h.logger.Error("Failed to estimate deposit price", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to estimate deposit price", + ) + } + if blockPrice == nil { + h.logger.Warn("No block price estimated for deposit") + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "no block price available for deposit", + ) + } + result := map[string]interface{}{ + "bidAmount": hexutil.EncodeBig(blockPrice.BidAmount), + "depositAddress": h.depositAddress.Hex(), + } - 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", + resultJSON, err := json.Marshal(result) + if err != nil { + h.logger.Error("Failed to marshal deposit estimate to JSON", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal deposit estimate", + ) + } + h.logger.Debug("Estimated deposit price", "bidAmount", blockPrice.BidAmount, "depositAddress", h.depositAddress.Hex()) + return resultJSON, false, nil + }) + server.RegisterHandler("mevcommit_estimateBridge", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { + blockPrice, err := h.pricer.EstimatePrice( + ctx, + types.NewTransaction(0, h.bridgeAddress, big.NewInt(0), 21000, big.NewInt(0), nil), ) - } + if err != nil { + h.logger.Error("Failed to estimate bridge price", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to estimate bridge price", + ) + } + if blockPrice == nil { + h.logger.Warn("No block price estimated for bridge") + return nil, true, nil // No price available, proxy + } + bridgeCost := new(big.Int).Mul(blockPrice.BidAmount, big.NewInt(2)) + result := map[string]interface{}{ + "bidAmount": hexutil.EncodeBig(bridgeCost), + "bridgeAddress": h.bridgeAddress.Hex(), + } - return chainIDJSON, false, nil + resultJSON, err := json.Marshal(result) + if err != nil { + h.logger.Error("Failed to marshal bridge estimate to JSON", "error", err) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal bridge estimate", + ) + } + h.logger.Debug("Estimated bridge price", "bidAmount", blockPrice.BidAmount, "bridgeAddress", h.bridgeAddress.Hex()) + return resultJSON, false, nil + }) } func (h *rpcMethodHandler) handleGetBlockByHash( @@ -205,6 +230,8 @@ func (h *rpcMethodHandler) handleGetBlockByHash( } blockHashStr := params[0].(string) + // Remove "0x" prefix if present + blockHashStr = strings.TrimPrefix(blockHashStr, "0x") if !strings.HasPrefix(blockHashStr, preconfBlockHashPrefix) { return nil, true, nil // Not a preconf block hash, proxy } @@ -223,7 +250,7 @@ func (h *rpcMethodHandler) handleGetBlockByHash( ) } - txns, err := h.store.GetPreconfirmedTransactionsForBlock(ctx, int64(blockNumber)) + txns, err := h.store.GetTransactionsForBlock(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( @@ -241,7 +268,7 @@ func (h *rpcMethodHandler) handleGetBlockByHash( "logsBloom": hexutil.Bytes(types.Bloom{}.Bytes()), "transactionsRoot": (common.Hash{}).Hex(), "stateRoot": (common.Hash{}).Hex(), - "miner": h.owner.Hex(), + "miner": (common.Address{}).Hex(), "difficulty": hexutil.Uint64(0), "totalDifficulty": hexutil.Uint64(0), "size": hexutil.Uint64(0), @@ -255,6 +282,10 @@ func (h *rpcMethodHandler) handleGetBlockByHash( var txnsToReturn any for i, txn := range txns { + if txn.Status != sender.TxStatusPreConfirmed { + h.logger.Warn("Skipping transaction not in preconfirmed state", "txnHash", txn.Hash().Hex(), "status", txn.Status) + continue + } if !details { if txnsToReturn == nil { txnsToReturn = make([]string, 0, len(txns)) @@ -269,11 +300,6 @@ func (h *rpcMethodHandler) handleGetBlockByHash( 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{}{ @@ -281,14 +307,14 @@ func (h *rpcMethodHandler) handleGetBlockByHash( "blockHash": blockHashStr, "blockNumber": hexutil.Uint64(blockNumber), "transactionIndex": hexutil.Uint64(i), - "type": hexutil.Uint(txn.Type()), + "type": hexutil.Uint(txn.Transaction.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(), + "from": txn.Sender.Hex(), "nonce": hexutil.Uint64(txn.Nonce()), "gas": hexutil.Uint64(txn.Gas()), "gasPrice": hexutil.EncodeBig(txn.GasPrice()), @@ -353,7 +379,7 @@ func (h *rpcMethodHandler) handleSendRawTx( ) } - sender, err := types.Sender(types.LatestSignerForChainID(txn.ChainId()), txn) + txSender, 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( @@ -362,98 +388,28 @@ func (h *rpcMethodHandler) handleSendRawTx( ) } - // 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: - } + txType := sender.TxTypeRegular + switch { + case txn.To().Cmp(h.depositAddress) == 0: + txType = sender.TxTypeDeposit + case txn.To().Cmp(h.bridgeAddress) == 0: + txType = sender.TxTypeInstantBridge + } - // 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 - } + err = h.sndr.Enqueue(ctx, &sender.Transaction{ + Transaction: txn, + Raw: rawTxHex, + Sender: txSender, + Type: txType, + }) + if err != nil { + h.logger.Error("Failed to enqueue transaction for sending", "error", err, "sender", txSender.Hex()) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to enqueue transaction for sending", + ) } - // 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) @@ -466,129 +422,6 @@ BID_LOOP: 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( @@ -613,46 +446,40 @@ func (h *rpcMethodHandler) handleGetTxReceipt(ctx context.Context, params ...any txHash := common.HexToHash(txHashStr) - h.logger.Info("Retrieving transaction receipt", "txHash", txHash) - txn, commitments, err := h.store.GetPreconfirmedTransaction(ctx, txHash) + txn, err := h.store.GetTransactionByHash(ctx, txHash) if err != nil { return nil, true, nil } - if h.blockTracker.LatestBlockNumber() > uint64(commitments[0].BlockNumber) { + if txn.Status != sender.TxStatusFailed && + (txn.Status != sender.TxStatusPreConfirmed || h.blockTracker.LatestBlockNumber() > uint64(txn.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()), + "type": hexutil.Uint(txn.Transaction.Type()), "transactionHash": txn.Hash().Hex(), "transactionIndex": hexutil.Uint(0), - "blockHash": blockHash, - "blockNumber": hexutil.EncodeBig(big.NewInt(commitments[0].BlockNumber)), - "from": sender.Hex(), + "from": txn.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)), } + if txn.Status == sender.TxStatusFailed { + result["status"] = hexutil.Uint64(types.ReceiptStatusFailed) + } else { + result["status"] = hexutil.Uint64(types.ReceiptStatusSuccessful) + blockHash := fmt.Sprintf("0x%s%08d", preconfBlockHashPrefix, txn.BlockNumber) + blockHash += strings.Repeat("0", 66-len(blockHash)) + result["blockHash"] = blockHash + result["blockNumber"] = hexutil.EncodeBig(big.NewInt(txn.BlockNumber)) + } + receiptJSON, err := json.Marshal(result) if err != nil { h.logger.Error("Failed to marshal receipt to JSON", "error", err, "txHash", txHash) @@ -688,25 +515,14 @@ func (h *rpcMethodHandler) handleGetTxCount(ctx context.Context, params ...any) ) } - h.nonceLock.Lock(account) - defer h.nonceLock.Unlock(account) - - h.nonceMapLock.RLock() - accNonce, found := h.nonceMap[account] - h.nonceMapLock.RUnlock() - - if !found { + accNonce := h.store.GetCurrentNonce(ctx, common.HexToAddress(account)) + if accNonce == 0 { 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 - } + accNonce += 1 - nonceJSON, err := json.Marshal(accNonce.Nonce) + nonceJSON, err := json.Marshal(accNonce) if err != nil { h.logger.Error("Failed to marshal nonce to JSON", "error", err, "account", account) return nil, false, rpcserver.NewJSONErr( @@ -715,7 +531,7 @@ func (h *rpcMethodHandler) handleGetTxCount(ctx context.Context, params ...any) ) } - h.logger.Info("Retrieved account nonce from cache", "account", account, "nonce", accNonce.Nonce) + h.logger.Info("Retrieved account nonce from cache", "account", account, "nonce", accNonce) return nonceJSON, false, nil } @@ -747,9 +563,12 @@ func (h *rpcMethodHandler) handleGetTxCommitments( txHash := common.HexToHash(txHashStr) - _, commitments, err := h.store.GetPreconfirmedTransaction(ctx, txHash) + commitments, err := h.store.GetTransactionCommitments(ctx, txHash) if err != nil { - return nil, true, nil + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to get transaction commitments", + ) } if len(commitments) == 0 { @@ -803,19 +622,3 @@ func (h *rpcMethodHandler) handleMevCommitGetBalance(ctx context.Context, params 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 index ede6b1d3c..cc617b810 100644 --- a/tools/preconf-rpc/main.go +++ b/tools/preconf-rpc/main.go @@ -38,11 +38,39 @@ var ( Required: true, } - optionDataDir = &cli.StringFlag{ - Name: "data-dir", - Usage: "directory where data is stored", - EnvVars: []string{"PRECONF_RPC_DATA_DIR"}, - Value: "~/data", + optionPgHost = &cli.StringFlag{ + Name: "pg-host", + Usage: "PostgreSQL host", + EnvVars: []string{"PRECONF_RPC_PG_HOST"}, + Value: "localhost", + } + + optionPgPort = &cli.IntFlag{ + Name: "pg-port", + Usage: "PostgreSQL port", + EnvVars: []string{"PRECONF_RPC_PG_PORT"}, + Value: 5432, + } + + optionPgUser = &cli.StringFlag{ + Name: "pg-user", + Usage: "PostgreSQL user", + EnvVars: []string{"PRECONF_RPC_PG_USER"}, + Value: "postgres", + } + + optionPgPassword = &cli.StringFlag{ + Name: "pg-password", + Usage: "PostgreSQL password", + EnvVars: []string{"PRECONF_RPC_PG_PASSWORD"}, + Value: "postgres", + } + + optionPgDbname = &cli.StringFlag{ + Name: "pg-dbname", + Usage: "PostgreSQL database name", + EnvVars: []string{"PRECONF_RPC_PG_DBNAME"}, + Value: "mev_oracle", } optionL1RPCUrls = &cli.StringSliceFlag{ @@ -115,6 +143,32 @@ var ( Required: true, } + optionDepositAddress = &cli.StringFlag{ + Name: "deposit-address", + Usage: "address to deposit funds to", + EnvVars: []string{"PRECONF_RPC_DEPOSIT_ADDRESS"}, + Required: true, + Action: func(ctx *cli.Context, s string) error { + if !common.IsHexAddress(s) { + return fmt.Errorf("invalid deposit address: %s", s) + } + return nil + }, + } + + optionBridgeAddress = &cli.StringFlag{ + Name: "bridge-address", + Usage: "address to bridge to", + EnvVars: []string{"PRECONF_RPC_BRIDGE_ADDRESS"}, + Required: true, + Action: func(ctx *cli.Context, s string) error { + if !common.IsHexAddress(s) { + return fmt.Errorf("invalid bridge address: %s", s) + } + return nil + }, + } + optionLogFmt = &cli.StringFlag{ Name: "log-fmt", Usage: "log format to use, options are 'text' or 'json'", @@ -162,7 +216,11 @@ func main() { Usage: "Preconf RPC service", Flags: []cli.Flag{ optionHTTPPort, - optionDataDir, + optionPgHost, + optionPgPort, + optionPgUser, + optionPgPassword, + optionPgDbname, optionLogFmt, optionLogLevel, optionLogTags, @@ -178,6 +236,8 @@ func main() { optionGasFeeCap, optionSettlementContractAddr, optionAutoDepositAmount, + optionDepositAddress, + optionBridgeAddress, }, Action: func(c *cli.Context) error { logger, err := util.NewLogger( @@ -223,18 +283,16 @@ func main() { 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), + PgHost: c.String(optionPgHost.Name), + PgPort: c.Int(optionPgPort.Name), + PgUser: c.String(optionPgUser.Name), + PgPassword: c.String(optionPgPassword.Name), + PgDbname: c.String(optionPgDbname.Name), Logger: logger, GasTipCap: gasTipCap, GasFeeCap: gasFeeCap, @@ -247,6 +305,8 @@ func main() { L1ContractAddr: common.HexToAddress(c.String(optionL1ContractAddr.Name)), SettlementContractAddr: common.HexToAddress(c.String(optionSettlementContractAddr.Name)), Signer: signer, + DepositAddress: common.HexToAddress(c.String(optionDepositAddress.Name)), + BridgeAddress: common.HexToAddress(c.String(optionBridgeAddress.Name)), } s, err := service.New(&config) diff --git a/tools/preconf-rpc/rpcserver/rpcserver.go b/tools/preconf-rpc/rpcserver/rpcserver.go index e1307a6e0..ce2c81ac2 100644 --- a/tools/preconf-rpc/rpcserver/rpcserver.go +++ b/tools/preconf-rpc/rpcserver/rpcserver.go @@ -86,7 +86,7 @@ func (s *JSONRPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - s.logger.Info("Received JSON-RPC request", "method", r.Method) + s.logger.Debug("Received JSON-RPC request", "method", r.Method) if r.Header.Get("Content-Type") != "application/json" { http.Error(w, "Invalid content type", http.StatusUnsupportedMediaType) @@ -116,8 +116,10 @@ func (s *JSONRPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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) + start := time.Now() + defer func() { + s.logger.Info("Request processing time", "method", req.Method, "id", req.ID, "duration", time.Since(start)) + }() s.rwLock.RLock() handler, ok := s.methods[req.Method] @@ -150,7 +152,7 @@ func (s *JSONRPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (s *JSONRPCServer) writeResponse(w http.ResponseWriter, id any, result *json.RawMessage) { - s.logger.Info("Writing JSON-RPC response", "id", id, "result", result) + s.logger.Debug("Writing JSON-RPC response", "id", id, "result", result) response := jsonRPCResponse{ JSONRPC: "2.0", ID: id, @@ -194,7 +196,7 @@ func (s *JSONRPCServer) proxyRequest(w http.ResponseWriter, body []byte) { } req.Header.Set("Content-Type", "application/json") - s.logger.Info("Proxying request", "url", s.proxyURL, "body", string(body)) + s.logger.Debug("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) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go new file mode 100644 index 000000000..0048fa327 --- /dev/null +++ b/tools/preconf-rpc/sender/sender.go @@ -0,0 +1,496 @@ +package sender + +import ( + "context" + "errors" + "fmt" + "log/slog" + "math/big" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/common" + "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" + optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" + "golang.org/x/sync/errgroup" +) + +type TxType int + +const ( + TxTypeRegular TxType = iota + TxTypeDeposit + TxTypeInstantBridge +) + +type TxStatus string + +const ( + TxStatusPending TxStatus = "pending" + TxStatusPreConfirmed TxStatus = "pre-confirmed" + TxStatusConfirmed TxStatus = "confirmed" + TxStatusFailed TxStatus = "failed" +) + +const ( + blockTime = 12 // seconds, typical Ethereum block time +) + +var ( + ErrInvalidTransaction = errors.New("invalid transaction") + ErrUnsupportedTxType = errors.New("unsupported transaction type") + ErrEmptyRawTransaction = errors.New("empty raw transaction") + ErrEmptyTransactionTo = errors.New("empty transaction 'to' address") + ErrNegativeTransactionValue = errors.New("negative transaction value") + ErrZeroGasLimit = errors.New("zero gas limit") +) + +type Transaction struct { + *types.Transaction + Sender common.Address + Raw string + Type TxType + Status TxStatus + Details string + BlockNumber int64 +} + +type Store interface { + AddQueuedTransaction(ctx context.Context, tx *Transaction) error + GetQueuedTransactions(ctx context.Context) ([]*Transaction, error) + GetCurrentNonce(ctx context.Context, sender common.Address) uint64 + HasBalance(ctx context.Context, sender common.Address, amount *big.Int) bool + AddBalance(ctx context.Context, account common.Address, amount *big.Int) error + DeductBalance(ctx context.Context, account common.Address, amount *big.Int) error + StoreTransaction(ctx context.Context, txn *Transaction, commitments []*bidderapiv1.Commitment) error +} + +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 BlockTracker interface { + CheckTxnInclusion(ctx context.Context, txnHash common.Hash, blockNumber uint64) (bool, error) +} + +type Transferer interface { + Transfer(ctx context.Context, to common.Address, chainID *big.Int, amount *big.Int) error +} + +type TxSender struct { + logger *slog.Logger + store Store + bidder Bidder + pricer Pricer + blockTracker BlockTracker + transferer Transferer + settlementChainId *big.Int + eg *errgroup.Group + egCtx context.Context + trigger chan struct{} + workerPool chan struct{} + inflightTxns map[common.Hash]struct{} + inflightAccount map[common.Address]struct{} + inflightMu sync.Mutex +} + +func NewTxSender( + st Store, + bidder Bidder, + pricer Pricer, + blockTracker BlockTracker, + transferer Transferer, + settlementChainId *big.Int, + logger *slog.Logger, +) *TxSender { + return &TxSender{ + store: st, + bidder: bidder, + pricer: pricer, + blockTracker: blockTracker, + transferer: transferer, + settlementChainId: settlementChainId, + logger: logger.With("component", "TxSender"), + workerPool: make(chan struct{}, 512), + trigger: make(chan struct{}, 1), + inflightTxns: make(map[common.Hash]struct{}), + inflightAccount: make(map[common.Address]struct{}), + } +} + +func validateTransaction(tx *Transaction) error { + if tx == nil || tx.Transaction == nil { + return ErrInvalidTransaction + } + if tx.Type < TxTypeRegular || tx.Type > TxTypeInstantBridge { + return ErrUnsupportedTxType + } + if tx.Raw == "" { + return ErrEmptyRawTransaction + } + if tx.To() == nil { + return ErrEmptyTransactionTo + } + if tx.Value().Sign() < 0 { + return ErrNegativeTransactionValue + } + if tx.Gas() == 0 { + return ErrZeroGasLimit + } + return nil +} + +func (t *TxSender) hasLowerNonce(ctx context.Context, tx *Transaction) bool { + currentNonce := t.store.GetCurrentNonce(ctx, tx.Sender) + return tx.Nonce() < currentNonce +} + +func (t *TxSender) triggerSender() { + select { + case t.trigger <- struct{}{}: + default: + // Non-blocking send, if the channel is full, we do nothing + } +} + +func (t *TxSender) Enqueue(ctx context.Context, tx *Transaction) error { + if err := validateTransaction(tx); err != nil { + t.logger.Error("Invalid transaction", "error", err, "transaction", tx.Raw) + return err + } + + if t.hasLowerNonce(ctx, tx) { + return errors.New("transaction has a lower nonce than the current highest nonce") + } + + if err := t.store.AddQueuedTransaction(ctx, tx); err != nil { + return err + } + + t.triggerSender() + + return nil +} + +func (t *TxSender) Start(ctx context.Context) chan struct{} { + t.eg, t.egCtx = errgroup.WithContext(ctx) + done := make(chan struct{}) + + t.eg.Go(func() error { + for { + select { + case <-t.egCtx.Done(): + t.logger.Info("Context cancelled, stopping TxSender") + return ctx.Err() + case <-t.trigger: + t.processQueuedTransactions(t.egCtx) + } + } + }) + + go func() { + defer close(done) + if err := t.eg.Wait(); err != nil { + t.logger.Error("Error in TxSender", "error", err) + return + } + }() + + return done +} + +func (t *TxSender) markInflight(txn *Transaction) bool { + t.inflightMu.Lock() + defer t.inflightMu.Unlock() + + if _, ok := t.inflightTxns[txn.Hash()]; ok { + t.logger.Debug("Transaction already in flight, skipping", "hash", txn.Hash().Hex()) + return false + } + if _, ok := t.inflightAccount[txn.Sender]; ok { + t.logger.Debug("Transaction sender already has an inflight transaction, skipping", "sender", txn.Sender.Hex()) + t.triggerSender() // Trigger to reprocess later + return false + } + + t.inflightTxns[txn.Hash()] = struct{}{} + t.inflightAccount[txn.Sender] = struct{}{} + return true +} + +func (t *TxSender) markCompleted(txn *Transaction) { + t.inflightMu.Lock() + defer t.inflightMu.Unlock() + + delete(t.inflightTxns, txn.Hash()) + delete(t.inflightAccount, txn.Sender) +} + +func (t *TxSender) processQueuedTransactions(ctx context.Context) { + txns, err := t.store.GetQueuedTransactions(ctx) + if err != nil { + t.logger.Error("Failed to get queued transactions", "error", err) + return + } + if len(txns) == 0 { + t.logger.Info("No queued transactions to process") + return + } + t.logger.Info("Processing queued transactions", "count", len(txns)) + for _, txn := range txns { + txn := txn // capture range variable + select { + case <-ctx.Done(): + t.logger.Info("Context cancelled, stopping transaction processing") + return + case t.workerPool <- struct{}{}: + t.eg.Go(func() error { + defer func() { <-t.workerPool }() + if !t.markInflight(txn) { + // Transaction is already being processed or sender has an inflight transaction + return nil + } + defer t.markCompleted(txn) + + t.logger.Info("Processing transaction", "sender", txn.Sender.Hex(), "type", txn.Type) + if err := t.processTransaction(ctx, txn); err != nil { + t.logger.Error("Failed to process transaction", "sender", txn.Sender.Hex(), "error", err) + txn.Status = TxStatusFailed + txn.Details = err.Error() + return t.store.StoreTransaction(ctx, txn, nil) + } + return nil + }) + } + } +} + +func (t *TxSender) processTransaction(ctx context.Context, txn *Transaction) error { + var ( + result bidResult + err error + ) +BID_LOOP: + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + result, err = t.sendBid(ctx, txn) + switch { + case err != nil: + return err + 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 + txn.Status = TxStatusPreConfirmed + txn.BlockNumber = int64(result.blockNumber) + t.logger.Info( + "Transaction pre-confirmed", + "sender", txn.Sender.Hex(), + "type", txn.Type, + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + 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 := t.blockTracker.CheckTxnInclusion(ctx, txn.Hash(), result.blockNumber) + if err != nil { + t.logger.Error("Failed to check transaction inclusion", "error", err) + return fmt.Errorf("failed to check transaction inclusion: %w", err) + } + if included { + txn.Status = TxStatusConfirmed + txn.BlockNumber = int64(result.blockNumber) + t.logger.Info( + "Transaction confirmed for non opted-in slot", + "sender", txn.Sender.Hex(), + "type", txn.Type, + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + break BID_LOOP + } + } + + if err := t.store.StoreTransaction(ctx, txn, result.commitments); err != nil { + return fmt.Errorf("failed to store preconfirmed transaction: %w", err) + } + + switch txn.Type { + case TxTypeRegular: + if err := t.store.DeductBalance(ctx, txn.Sender, result.bidAmount); err != nil { + t.logger.Error("Failed to deduct balance for sender", "sender", txn.Sender.Hex(), "error", err) + return fmt.Errorf("failed to deduct balance for sender: %w", err) + } + case TxTypeDeposit: + balanceToAdd := new(big.Int).Sub(txn.Value(), result.bidAmount) + if err := t.store.AddBalance(ctx, txn.Sender, balanceToAdd); err != nil { + t.logger.Error("Failed to add balance for sender", "sender", txn.Sender.Hex(), "error", err) + return fmt.Errorf("failed to add balance for sender: %w", err) + } + case TxTypeInstantBridge: + amountToBridge := new(big.Int).Sub(txn.Value(), new(big.Int).Mul(result.bidAmount, big.NewInt(2))) + if err := t.transferer.Transfer(ctx, txn.Sender, t.settlementChainId, amountToBridge); err != nil { + t.logger.Error("Failed to transfer funds for instant bridge", "sender", txn.Sender.Hex(), "error", err) + return fmt.Errorf("failed to transfer funds for instant bridge: %w", err) + } + } + + return nil +} + +type bidResult struct { + noOfProviders int + blockNumber uint64 + optedInSlot bool + bidAmount *big.Int + commitments []*bidderapiv1.Commitment +} + +func (t *TxSender) sendBid( + ctx context.Context, + txn *Transaction, +) (bidResult, error) { + timeToOptIn, err := t.bidder.Estimate() + if err != nil { + t.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 := t.pricer.EstimatePrice(ctx, txn.Transaction) + if err != nil { + t.logger.Error("Failed to estimate transaction price", "error", err) + return bidResult{}, fmt.Errorf("failed to estimate transaction price: %w", err) + } + + switch txn.Type { + case TxTypeRegular: + if !t.store.HasBalance(ctx, txn.Sender, price.BidAmount) { + t.logger.Error("Insufficient balance for sender", "sender", txn.Sender.Hex()) + return bidResult{}, fmt.Errorf("insufficient balance for sender: %s", txn.Sender.Hex()) + } + case TxTypeDeposit: + if txn.Value().Cmp(price.BidAmount) < 0 { + t.logger.Error( + "Deposit amount is less than price of deposit", + "sender", txn.Sender.Hex(), + "deposit", txn.Value().String(), + "price", price.BidAmount.String(), + ) + return bidResult{}, fmt.Errorf( + "deposit amount is less than price of deposit: %s, deposit: %s, price: %s", + txn.Sender.Hex(), + txn.Value().String(), + price.BidAmount.String(), + ) + } + case TxTypeInstantBridge: + costOfBridge := new(big.Int).Mul(price.BidAmount, big.NewInt(2)) // 2x the price for instant bridge + if txn.Value().Cmp(costOfBridge) < 0 { + t.logger.Error( + "Instant bridge amount is less than price of bridge", + "sender", txn.Sender.Hex(), + "bridge", txn.Value().String(), + "price", costOfBridge.String(), + ) + return bidResult{}, fmt.Errorf( + "instant bridge amount is less than price of bridge: %s, bridge: %s, price: %s", + txn.Sender.Hex(), + txn.Value().String(), + costOfBridge.String(), + ) + } + } + + bidC, err := t.bidder.Bid( + ctx, + price.BidAmount, + big.NewInt(0), + strings.TrimPrefix(txn.Raw, "0x"), + &optinbidder.BidOpts{ + WaitForOptIn: optedInSlot, + BlockNumber: uint64(price.BlockNumber), + }, + ) + if err != nil { + t.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(): + t.logger.Info("Context cancelled while waiting for bid status") + return bidResult{}, ctx.Err() + case bidStatus, more := <-bidC: + if !more { + t.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: + t.logger.Warn("Bid context cancelled by the bidder") + break BID_LOOP + case optinbidder.BidStatusFailed: + t.logger.Error("Bid failed", "error", bidStatus.Arg) + break BID_LOOP + } + } + } + if len(result.commitments) == 0 { + t.logger.Error("Bid completed with no commitments") + return bidResult{}, fmt.Errorf("bid completed with no commitments") + } + t.logger.Info( + "Bid successful with commitments", + "noOfProviders", result.noOfProviders, + "noOfCommitments", len(result.commitments), + "blockNumber", result.blockNumber, + "optedInSlot", optedInSlot, + ) + + result.optedInSlot = optedInSlot + return result, nil +} diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go new file mode 100644 index 000000000..ab31b5fc5 --- /dev/null +++ b/tools/preconf-rpc/sender/sender_test.go @@ -0,0 +1,438 @@ +package sender_test + +import ( + "context" + "errors" + "math/big" + "os" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "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/sender" + optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" + "github.com/primev/mev-commit/x/util" +) + +type result struct { + txn *sender.Transaction + commitments []*bidderapiv1.Commitment + blockNumber int64 +} + +type mockStore struct { + mu sync.Mutex + queued map[common.Address][]*sender.Transaction + nonce map[common.Address]uint64 + balances map[common.Address]*big.Int + preconfirmedTxns chan result +} + +func newMockStore() *mockStore { + return &mockStore{ + queued: make(map[common.Address][]*sender.Transaction), + nonce: make(map[common.Address]uint64), + balances: make(map[common.Address]*big.Int), + preconfirmedTxns: make(chan result, 10), + } +} + +func (m *mockStore) AddQueuedTransaction(_ context.Context, tx *sender.Transaction) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.queued[tx.Sender] = append(m.queued[tx.Sender], tx) + m.nonce[tx.Sender] = tx.Nonce() + + return nil +} + +func (m *mockStore) GetQueuedTransactions(_ context.Context) ([]*sender.Transaction, error) { + m.mu.Lock() + defer m.mu.Unlock() + var txns []*sender.Transaction + + for _, acctTxns := range m.queued { + if len(acctTxns) == 0 { + continue + } + txns = append(txns, acctTxns[0]) + } + + return txns, nil +} + +func (m *mockStore) GetCurrentNonce(_ context.Context, sender common.Address) uint64 { + m.mu.Lock() + defer m.mu.Unlock() + + nonce, exists := m.nonce[sender] + if !exists { + return 0 + } + + return nonce +} + +func (m *mockStore) HasBalance(ctx context.Context, sender common.Address, amount *big.Int) bool { + m.mu.Lock() + defer m.mu.Unlock() + + balance, exists := m.balances[sender] + if !exists { + return false + } + return balance.Cmp(amount) >= 0 +} + +func (m *mockStore) AddBalance(ctx context.Context, account common.Address, amount *big.Int) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.balances[account]; !exists { + m.balances[account] = amount + } else { + newBalance := new(big.Int).Add(m.balances[account], amount) + m.balances[account] = newBalance + } + + return nil +} + +func (m *mockStore) DeductBalance(ctx context.Context, account common.Address, amount *big.Int) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.balances[account]; !exists { + return errors.New("account does not exist") + } + newBalance := new(big.Int).Sub(m.balances[account], amount) + if newBalance.Sign() < 0 { + return errors.New("insufficient balance") + } + m.balances[account] = newBalance + return nil +} + +func (m *mockStore) StoreTransaction( + ctx context.Context, + txn *sender.Transaction, + commitments []*bidderapiv1.Commitment, +) error { + m.preconfirmedTxns <- result{ + txn: txn, + commitments: commitments, + blockNumber: txn.BlockNumber, + } + m.mu.Lock() + defer m.mu.Unlock() + for i, queuedTxn := range m.queued[txn.Sender] { + if queuedTxn.Hash() == txn.Hash() { + // Remove the transaction from the queue + m.queued[txn.Sender] = append(m.queued[txn.Sender][:i], m.queued[txn.Sender][i+1:]...) + break + } + } + return nil +} + +type bidOp struct { + bidAmount *big.Int + slashAmount *big.Int + rawTx string + opts *optinbidder.BidOpts +} + +type mockBidder struct { + optinEstimate chan int64 + in chan bidOp + out chan chan optinbidder.BidStatus +} + +func (m *mockBidder) Estimate() (int64, error) { + estimate := <-m.optinEstimate + return estimate, nil +} + +func (m *mockBidder) Bid( + ctx context.Context, + bidAmount *big.Int, + slashAmount *big.Int, + rawTx string, + opts *optinbidder.BidOpts, +) (chan optinbidder.BidStatus, error) { + m.in <- bidOp{ + bidAmount: bidAmount, + slashAmount: slashAmount, + rawTx: rawTx, + opts: opts, + } + res := <-m.out + + return res, nil +} + +type mockPricer struct { + in chan *types.Transaction + out chan *pricer.BlockPrice +} + +func (m *mockPricer) EstimatePrice( + ctx context.Context, + txn *types.Transaction, +) (*pricer.BlockPrice, error) { + m.in <- txn + select { + case price := <-m.out: + if price == nil { + return nil, errors.New("nil price returned") + } + return price, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +type op struct { + hash common.Hash + block uint64 +} + +type mockBlockTracker struct { + in chan op + out chan bool +} + +func (m *mockBlockTracker) CheckTxnInclusion(ctx context.Context, txnHash common.Hash, blockNumber uint64) (bool, error) { + m.in <- op{ + hash: txnHash, + block: blockNumber, + } + select { + case included := <-m.out: + return included, nil + case <-ctx.Done(): + return false, ctx.Err() + } +} + +type mockTransferer struct{} + +func (m *mockTransferer) Transfer(ctx context.Context, to common.Address, chainID *big.Int, amount *big.Int) error { + return nil +} + +func TestSender(t *testing.T) { + t.Parallel() + + st := newMockStore() + testPricer := &mockPricer{ + in: make(chan *types.Transaction, 10), + out: make(chan *pricer.BlockPrice, 10), + } + bidder := &mockBidder{ + optinEstimate: make(chan int64, 10), + in: make(chan bidOp, 10), + out: make(chan chan optinbidder.BidStatus, 10), + } + blockTracker := &mockBlockTracker{ + in: make(chan op, 10), + out: make(chan bool, 10), + } + + sndr := sender.NewTxSender( + st, + bidder, + testPricer, + blockTracker, + &mockTransferer{}, + big.NewInt(1), // Settlement chain ID + util.NewTestLogger(os.Stdout), + ) + + ctx, cancel := context.WithCancel(context.Background()) + + done := sndr.Start(ctx) + + tx1 := &sender.Transaction{ + Transaction: types.NewTransaction( + 1, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(100), + 21000, + big.NewInt(1), + nil, + ), + Sender: common.HexToAddress("0x1234567890123456789012345678901234567890"), + Type: sender.TxTypeRegular, + Raw: "0x1234567890123456789012345678901234567890", + } + + if err := st.AddBalance(ctx, tx1.Sender, big.NewInt(1000)); err != nil { + t.Fatalf("failed to add balance: %v", err) + } + + if err := sndr.Enqueue(ctx, tx1); err != nil { + t.Fatalf("failed to enqueue transaction: %v", err) + } + + // Simulate opted in block + bidder.optinEstimate <- 1 + + // Simulate a price estimate + op := <-testPricer.in + if op.Hash().Hex() != tx1.Hash().Hex() { + t.Fatalf("expected transaction hash %s, got %s", tx1.Hash().Hex(), op.Hash().Hex()) + } + testPricer.out <- &pricer.BlockPrice{ + BlockNumber: 1, + BidAmount: big.NewInt(100), + } + + // Simulate a bid response + bidOp := <-bidder.in + if bidOp.rawTx != tx1.Raw[2:] { + t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) + } + resC := make(chan optinbidder.BidStatus, 3) + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusNoOfProviders, + Arg: 1, + } + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusAttempted, + Arg: uint64(1), + } + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusCommitment, + Arg: &bidderapiv1.Commitment{ + TxHashes: []string{tx1.Hash().Hex()}, + BidAmount: big.NewInt(100).String(), + BlockNumber: 1, + ProviderAddress: "provider1", + }, + } + close(resC) + bidder.out <- resC + + res := <-st.preconfirmedTxns + if res.txn == nil { + t.Fatal("expected a preconfirmed transaction, got nil") + } + if res.blockNumber != 1 { + t.Fatalf("expected block number 1, got %d", res.blockNumber) + } + if res.txn.Sender != tx1.Sender { + t.Fatalf("expected sender %s, got %s", tx1.Sender.Hex(), res.txn.Sender.Hex()) + } + if res.txn.Nonce() != tx1.Nonce() { + t.Fatalf("expected nonce %d, got %d", tx1.Nonce(), res.txn.Nonce()) + } + if res.txn.Type != tx1.Type { + t.Fatalf("expected transaction type %d, got %d", tx1.Type, res.txn.Type) + } + if res.txn.Hash() != tx1.Hash() { + t.Fatalf("expected transaction hash %s, got %s", tx1.Hash().Hex(), res.txn.Hash().Hex()) + } + // Check that the commitments are as expected + if len(res.commitments) != 1 { + t.Fatalf("expected 1 commitment, got %d", len(res.commitments)) + } + + tx2 := &sender.Transaction{ + Transaction: types.NewTransaction( + 2, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(1000), + 21000, + big.NewInt(1), + nil, + ), + Sender: common.HexToAddress("0x1234567890123456789012345678901234567890"), + Type: sender.TxTypeDeposit, + Raw: "0x1234567890123456789012345678901234567890", + } + + if err := sndr.Enqueue(ctx, tx2); err != nil { + t.Fatalf("failed to enqueue transaction: %v", err) + } + + // Simulate non opted in block + bidder.optinEstimate <- 20 + + // Simulate a price estimate + op = <-testPricer.in + if op.Hash().Hex() != tx2.Hash().Hex() { + t.Fatalf("expected transaction hash %s, got %s", tx2.Hash().Hex(), op.Hash().Hex()) + } + testPricer.out <- &pricer.BlockPrice{ + BlockNumber: 2, + BidAmount: big.NewInt(100), + } + + // Simulate a bid response + bidOp = <-bidder.in + if bidOp.rawTx != tx2.Raw[2:] { + t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) + } + resC = make(chan optinbidder.BidStatus, 3) + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusNoOfProviders, + Arg: 1, + } + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusAttempted, + Arg: uint64(2), + } + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusCommitment, + Arg: &bidderapiv1.Commitment{ + TxHashes: []string{tx1.Hash().Hex()}, + BidAmount: big.NewInt(100).String(), + BlockNumber: 2, + ProviderAddress: "provider1", + }, + } + close(resC) + bidder.out <- resC + + checkOp := <-blockTracker.in + if checkOp.hash != tx2.Hash() { + t.Fatalf("expected transaction hash %s, got %s", tx2.Hash().Hex(), checkOp.hash.Hex()) + } + if checkOp.block != 2 { + t.Fatalf("expected block number 2, got %d", checkOp.block) + } + // Simulate transaction inclusion + blockTracker.out <- true + + res = <-st.preconfirmedTxns + if res.txn == nil { + t.Fatal("expected a preconfirmed transaction, got nil") + } + if res.blockNumber != 2 { + t.Fatalf("expected block number 2, got %d", res.blockNumber) + } + if res.txn.Sender != tx2.Sender { + t.Fatalf("expected sender %s, got %s", tx2.Sender.Hex(), res.txn.Sender.Hex()) + } + if res.txn.Nonce() != tx2.Nonce() { + t.Fatalf("expected nonce %d, got %d", tx2.Nonce(), res.txn.Nonce()) + } + if res.txn.Type != tx2.Type { + t.Fatalf("expected transaction type %d, got %d", tx2.Type, res.txn.Type) + } + if res.txn.Hash() != tx2.Hash() { + t.Fatalf("expected transaction hash %s, got %s", tx2.Hash().Hex(), res.txn.Hash().Hex()) + } + // Check that the commitments are as expected + if len(res.commitments) != 1 { + t.Fatalf("expected 1 commitment, got %d", len(res.commitments)) + } + + cancel() + <-done +} diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index 04a912f09..3287401f5 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -3,6 +3,7 @@ package service import ( "context" "crypto/tls" + "database/sql" "errors" "fmt" "io" @@ -13,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + _ "github.com/lib/pq" 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" @@ -20,6 +22,7 @@ import ( "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/sender" "github.com/primev/mev-commit/tools/preconf-rpc/store" "github.com/primev/mev-commit/x/accountsync" "github.com/primev/mev-commit/x/contracts/ethwrapper" @@ -33,7 +36,11 @@ import ( type Config struct { Logger *slog.Logger - DataDir string + PgHost string + PgPort int + PgUser string + PgPassword string + PgDbname string Signer keysigner.KeySigner BidderRPC string AutoDepositAmount *big.Int @@ -41,6 +48,8 @@ type Config struct { SettlementRPCUrl string L1ContractAddr common.Address SettlementContractAddr common.Address + DepositAddress common.Address + BridgeAddress common.Address SettlementThreshold *big.Int SettlementTopup *big.Int HTTPPort int @@ -86,6 +95,11 @@ func New(config *Config) (*Service, error) { return nil, fmt.Errorf("failed to get L1 chain ID: %w", err) } + settlementChainID, err := settlementClient.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to get settlement chain ID: %w", err) + } + bidderCli := bidderapiv1.NewBidderClient(conn) topologyCli := debugapiv1.NewDebugServiceClient(conn) notificationsCli := notificationsapiv1.NewNotificationsClient(conn) @@ -132,6 +146,14 @@ func New(config *Config) (*Service, error) { l1RPCClient, ) + transferer := transfer.NewTransferer( + config.Logger.With("module", "transferer"), + settlementClient, + config.Signer, + config.GasTipCap, + config.GasFeeCap, + ) + ctx, cancel := context.WithCancel(context.Background()) s.cancel = cancel @@ -152,7 +174,12 @@ func New(config *Config) (*Service, error) { bidpricer := &pricer.BidPricer{} - rpcstore, err := store.New(config.DataDir) + db, err := initDB(config) + if err != nil { + return nil, fmt.Errorf("failed to initialize database: %w", err) + } + + rpcstore, err := store.New(db) if err != nil { return nil, fmt.Errorf("failed to create store: %w", err) } @@ -164,21 +191,47 @@ func New(config *Config) (*Service, error) { blockTrackerDone := blockTracker.Start(ctx) healthChecker.Register(health.CloseChannelHealthCheck("BlockTracker", blockTrackerDone)) + sndr := sender.NewTxSender( + rpcstore, + bidderClient, + bidpricer, + blockTracker, + transferer, + settlementChainID, + config.Logger.With("module", "txsender"), + ) + + senderDone := sndr.Start(ctx) + healthChecker.Register(health.CloseChannelHealthCheck("TxSender", senderDone)) + handlers := handlers.NewRPCMethodHandler( config.Logger.With("module", "handlers"), + bidpricer, bidderClient, rpcstore, - bidpricer, blockTracker, - config.Signer.GetAddress(), + sndr, + config.DepositAddress, + config.BridgeAddress, l1ChainID, ) handlers.RegisterMethods(rpcServer) + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + if err := healthChecker.Health(); err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + }) + mux.Handle("/", rpcServer) + srv := http.Server{ Addr: fmt.Sprintf(":%d", config.HTTPPort), - Handler: rpcServer, + Handler: mux, } go func() { @@ -213,3 +266,29 @@ func (c channelCloser) Close() error { } return nil } + +func initDB(opts *Config) (db *sql.DB, err error) { + // Connection string + psqlInfo := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + opts.PgHost, opts.PgPort, opts.PgUser, opts.PgPassword, opts.PgDbname, + ) + + // Open a connection + db, err = sql.Open("postgres", psqlInfo) + if err != nil { + return nil, err + } + + // Check the connection + err = db.Ping() + if err != nil { + return nil, err + } + + db.SetMaxOpenConns(50) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(1 * time.Hour) + + return db, err +} diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index 73432c314..1fb34479d 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -1,172 +1,367 @@ package store import ( - "bytes" "context" - "encoding/binary" - "encoding/json" + "database/sql" + "encoding/hex" "errors" "fmt" "math/big" + "strings" - "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" + "github.com/primev/mev-commit/tools/preconf-rpc/sender" + "google.golang.org/protobuf/proto" ) var ( ErrInsufficientBalance = errors.New("insufficient balance") + ErrNotFound = errors.New("not found") ) +var transactionsTable = ` +CREATE TABLE IF NOT EXISTS mcTransactions ( + hash TEXT PRIMARY KEY, + nonce BIGINT, + raw_transaction TEXT, + block_number BIGINT, + sender TEXT, + tx_type INTEGER, + status TEXT, + details TEXT +);` + +var commitmentsTable = ` +CREATE TABLE IF NOT EXISTS commitments ( + commitment_digest TEXT PRIMARY KEY, + transaction_hash TEXT, + provider_address TEXT, + commitment_data BYTEA, + FOREIGN KEY (transaction_hash) REFERENCES mcTransactions (hash) ON DELETE CASCADE +);` + +var balancesTable = ` +CREATE TABLE IF NOT EXISTS balances ( + account TEXT PRIMARY KEY, + balance NUMERIC(24, 0) +);` + type rpcstore struct { - db *pebble.DB + db *sql.DB } -func New(path string) (*rpcstore, error) { - db, err := pebble.Open(path, &pebble.Options{}) - if err != nil { - return nil, err +func New(db *sql.DB) (*rpcstore, error) { + for _, table := range []string{ + transactionsTable, + commitmentsTable, + balancesTable, + } { + _, err := db.Exec(table) + 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()) + return 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") +func (s *rpcstore) AddQueuedTransaction(ctx context.Context, tx *sender.Transaction) error { + insertQuery := ` + INSERT INTO mcTransactions (hash, nonce, raw_transaction, sender, tx_type, status) + VALUES ($1, $2, $3, $4, $5, $6); + ` + _, err := s.db.ExecContext( + ctx, + insertQuery, + tx.Hash().Hex(), + tx.Nonce(), + tx.Raw, + tx.Sender.Hex(), + int(tx.Type), + string(sender.TxStatusPending), + ) + if err != nil { + return fmt.Errorf("failed to add queued transaction: %w", err) } - // Serialize the transaction and commitments - txnData, err := txn.MarshalBinary() - if err != nil { - return err + return nil +} + +func parseTransactionsFromRows(rows *sql.Rows) ([]*sender.Transaction, error) { + var transactions []*sender.Transaction + for rows.Next() { + var ( + rawTransaction string + senderAddress string + txType int + blockNum sql.NullInt64 + status string + details sql.NullString + ) + err := rows.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + txStr, err := hex.DecodeString(strings.TrimPrefix(rawTransaction, "0x")) + if err != nil { + return nil, fmt.Errorf("failed to decode raw transaction: %w", err) + } + parsedTxn := new(types.Transaction) + if err := parsedTxn.UnmarshalBinary(txStr); err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) + } + txn := &sender.Transaction{ + Transaction: parsedTxn, + Raw: rawTransaction, + BlockNumber: blockNum.Int64, + Sender: common.HexToAddress(senderAddress), + Type: sender.TxType(txType), + Status: sender.TxStatus(status), + Details: details.String, + } + transactions = append(transactions, txn) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows: %w", err) } - txnDataLenBuf := make([]byte, 8) - binary.BigEndian.PutUint64(txnDataLenBuf, uint64(len(txnData))) - txnDataWithLen := append(txnDataLenBuf, txnData...) + return transactions, nil +} - commitmentsData, err := json.Marshal(commitments) +// GetQueuedTransactions retrieves the next pending transaction for each sender. +func (s *rpcstore) GetQueuedTransactions(ctx context.Context) ([]*sender.Transaction, error) { + query := ` + SELECT t1.raw_transaction, t1.block_number, t1.sender, t1.tx_type, t1.status, t1.details + FROM mcTransactions t1 + INNER JOIN ( + SELECT sender, MIN(nonce) AS min_nonce + FROM mcTransactions + WHERE status = 'pending' + GROUP BY sender + ) t2 + ON t1.sender = t2.sender AND t1.nonce = t2.min_nonce + WHERE t1.status = 'pending'; + ` + + rows, err := s.db.QueryContext(ctx, query) if err != nil { - return err + if errors.Is(err, sql.ErrNoRows) { + return []*sender.Transaction{}, nil // No pending transactions found + } + return nil, fmt.Errorf("failed to get queued transactions: %w", 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 + transactions, err := parseTransactionsFromRows(rows) + if err != nil { + return nil, fmt.Errorf("failed to parse transactions from rows: %w", 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 + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows: %w", err) } - return nil + return transactions, 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") +func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash) (*sender.Transaction, error) { + query := ` + SELECT raw_transaction, block_number, sender, tx_type, status, details + FROM mcTransactions + WHERE hash = $1; + ` + row := s.db.QueryRowContext(ctx, query, txnHash.Hex()) + var ( + rawTransaction string + senderAddress string + txType int + status string + blockNum sql.NullInt64 + details sql.NullString + ) + err := row.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("transaction %s not found: %w", txnHash.Hex(), ErrNotFound) + } + return nil, fmt.Errorf("failed to get transaction by hash: %w", err) } - - txnKey := []byte(fmt.Sprintf("txn:%s", txnHash.Hex())) - blkNumBuf, closer, err := s.db.Get(txnKey) + txStr, err := hex.DecodeString(strings.TrimPrefix(rawTransaction, "0x")) if err != nil { - return nil, nil, err + return nil, fmt.Errorf("failed to decode raw transaction: %w", err) } - - blockNumber := binary.BigEndian.Uint64(blkNumBuf) - if blockNumber == 0 { - return nil, nil, fmt.Errorf("transaction %s not found", txnHash) + parsedTxn := new(types.Transaction) + if err := parsedTxn.UnmarshalBinary(txStr); err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) + } + txn := &sender.Transaction{ + Transaction: parsedTxn, + Raw: rawTransaction, + BlockNumber: blockNum.Int64, + Sender: common.HexToAddress(senderAddress), + Type: sender.TxType(txType), + Status: sender.TxStatus(status), + Details: details.String, } - _ = closer.Close() // Close the closer from Get + return txn, nil +} - key := []byte(fmt.Sprintf("%d:%s", blockNumber, txnHash.Hex())) - txnData, closer, err := s.db.Get(key) +func (s *rpcstore) GetTransactionsForBlock(ctx context.Context, blockNumber int64) ([]*sender.Transaction, error) { + query := ` + SELECT raw_transaction, block_number, sender, tx_type, status, details + FROM mcTransactions + WHERE block_number = $1 AND status = 'pre-confirmed'; + ` + rows, err := s.db.QueryContext(ctx, query, blockNumber) if err != nil { - return nil, nil, err + if errors.Is(err, sql.ErrNoRows) { + return []*sender.Transaction{}, nil // No transactions found for this block + } + return nil, fmt.Errorf("failed to get transactions for block %d: %w", blockNumber, err) + } + transactions, err := parseTransactionsFromRows(rows) + if err != nil { + return nil, fmt.Errorf("failed to parse transactions from rows: %w", 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) + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows for block %d: %w", blockNumber, 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) + // If no transactions found, return an empty slice + if len(transactions) == 0 { + return []*sender.Transaction{}, nil } - return txn, commitments, nil + return transactions, nil } -func (s *rpcstore) GetPreconfirmedTransactionsForBlock( +func (s *rpcstore) StoreTransaction( ctx context.Context, - blockNumber int64, -) ([]*types.Transaction, error) { - if blockNumber <= 0 { - return nil, errors.New("invalid block number") + txn *sender.Transaction, + commitments []*bidderapiv1.Commitment, +) error { + if txn.Status == sender.TxStatusPending { + return fmt.Errorf("transaction must not be in pending status, got %s", txn.Status) } - keyPrefix := []byte(fmt.Sprintf("%d:", blockNumber)) - iter, err := s.db.NewIter(&pebble.IterOptions{ - LowerBound: keyPrefix, - UpperBound: append(keyPrefix, 0xFF), - }) + if txn.BlockNumber == 0 && txn.Status != sender.TxStatusFailed { + return fmt.Errorf("block number must be set for successful transactions, got %d", txn.BlockNumber) + } + + dbTxn, err := s.db.BeginTx(ctx, nil) if err != nil { - return nil, fmt.Errorf("failed to create iterator for block %d: %w", blockNumber, err) + return fmt.Errorf("failed to begin transaction: %w", 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) + updateTxns := ` + UPDATE mcTransactions + SET block_number = $1, status = $2, details = $3 + WHERE hash = $4; + ` + + _, err = dbTxn.ExecContext(ctx, updateTxns, txn.BlockNumber, string(txn.Status), txn.Details, txn.Hash().Hex()) + if err != nil { + _ = dbTxn.Rollback() + return fmt.Errorf("failed to update transaction %s: %w", txn.Hash().Hex(), err) + } + + if txn.Status != sender.TxStatusFailed { + for _, commitment := range commitments { + insertCommitment := ` + INSERT INTO commitments (commitment_digest, transaction_hash, provider_address, commitment_data) + VALUES ($1, $2, $3, $4) + ON CONFLICT (commitment_digest) DO NOTHING; + ` + commitmentData, err := proto.Marshal(commitment) + if err != nil { + _ = dbTxn.Rollback() + return fmt.Errorf("failed to marshal commitment: %w", err) + } + + _, err = dbTxn.ExecContext( + ctx, + insertCommitment, + commitment.CommitmentDigest, + txn.Hash().Hex(), + commitment.ProviderAddress, + commitmentData, + ) + if err != nil { + _ = dbTxn.Rollback() + return fmt.Errorf("failed to insert commitment for transaction %s: %w", txn.Hash().Hex(), err) + } } - txnDataLen := binary.BigEndian.Uint64(txnData[:8]) - if len(txnData) < int(8+txnDataLen) { - return nil, fmt.Errorf("invalid transaction data length for block %d", blockNumber) + } + + if err := dbTxn.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + return nil +} + +func (s *rpcstore) GetTransactionCommitments(ctx context.Context, txnHash common.Hash) ([]*bidderapiv1.Commitment, error) { + query := ` + SELECT commitment_data + FROM commitments + WHERE transaction_hash = $1; + ` + rows, err := s.db.QueryContext(ctx, query, txnHash.Hex()) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("no commitments found for transaction %s: %w", txnHash.Hex(), ErrNotFound) } + return nil, fmt.Errorf("failed to get commitments for transaction %s: %w", txnHash.Hex(), err) + } - txn := new(types.Transaction) - if err := txn.UnmarshalBinary(txnData[8 : 8+txnDataLen]); err != nil { - return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) + var commitments []*bidderapiv1.Commitment + for rows.Next() { + var commitmentData []byte + err := rows.Scan(&commitmentData) + if err != nil { + return nil, fmt.Errorf("failed to scan commitment data for transaction %s: %w", txnHash.Hex(), err) } - transactions = append(transactions, txn) + commitment := &bidderapiv1.Commitment{} + if err := proto.Unmarshal(commitmentData, commitment); err != nil { + return nil, fmt.Errorf("failed to unmarshal commitment data for transaction %s: %w", txnHash.Hex(), err) + } + commitments = append(commitments, commitment) } - return transactions, nil + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows for transaction %s: %w", txnHash.Hex(), err) + } + if len(commitments) == 0 { + return nil, fmt.Errorf("no commitments found for transaction %s: %w", txnHash.Hex(), ErrNotFound) + } + return commitments, nil +} + +// GetCurrentNonce retrieves the next nonce for a given sender address by looking at the +// pending transactions in the database. If there are no pending transactions, it returns 0. +// The RPC would proxy this call to the underlying Ethereum node to get the current nonce in +// case if 0 is returned. +func (s *rpcstore) GetCurrentNonce(ctx context.Context, sender common.Address) uint64 { + query := ` + SELECT COALESCE(MAX(nonce), 0) + FROM mcTransactions + WHERE sender = $1 AND status = 'pending'; + ` + row := s.db.QueryRowContext(ctx, query, sender.Hex()) + var nextNonce uint64 + err := row.Scan(&nextNonce) + if err != nil { + return 0 // If no pending transactions found, return 0 as the next nonce + } + return nextNonce } func (s *rpcstore) DeductBalance( @@ -174,26 +369,17 @@ func (s *rpcstore) DeductBalance( 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) + query := ` + UPDATE balances + SET balance = balance - $1 + WHERE account = $2 AND balance >= $1; + ` + _, err := s.db.ExecContext(ctx, query, amount.String(), account.Hex()) 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) + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("account %s not found or insufficient balance: %w", account.Hex(), ErrInsufficientBalance) + } + return fmt.Errorf("failed to deduct balance for account %s: %w", account.Hex(), err) } return nil @@ -205,31 +391,22 @@ func (s *rpcstore) AddBalance( amount *big.Int, ) error { if account == (common.Address{}) || amount == nil || amount.Sign() <= 0 { - return errors.New("invalid account or amount") + return fmt.Errorf("invalid account or amount: account=%s, amount=%s", account.Hex(), amount.String()) } - balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) - currentBalance, closer, err := s.db.Get(balanceKey) + query := ` + INSERT INTO balances (account, balance) + VALUES ($1, $2) + ON CONFLICT (account) DO UPDATE SET balance = balances.balance + $2 + WHERE balances.balance + $2 >= 0; + ` + + _, err := s.db.ExecContext(ctx, query, account.Hex(), amount.String()) 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() + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("account %s not found or insufficient balance: %w", account.Hex(), ErrInsufficientBalance) } - }() - - 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 fmt.Errorf("failed to add balance for account %s: %w", account.Hex(), err) } return nil @@ -244,18 +421,24 @@ func (s *rpcstore) HasBalance( return false } - balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) - currentBalance, closer, err := s.db.Get(balanceKey) + query := ` + SELECT balance + FROM balances + WHERE account = $1; + ` + + row := s.db.QueryRowContext(ctx, query, account.Hex()) + var currentBalanceString string + err := row.Scan(¤tBalanceString) if err != nil { return false } - defer func() { - _ = closer.Close() - }() - - currentBalanceBig := new(big.Int).SetBytes(currentBalance) + currentBalance, ok := new(big.Int).SetString(currentBalanceString, 10) + if !ok { + return false + } - return currentBalanceBig.Cmp(amount) >= 0 + return currentBalance.Cmp(amount) >= 0 } func (s *rpcstore) GetBalance( @@ -266,14 +449,34 @@ func (s *rpcstore) GetBalance( return nil, errors.New("account cannot be empty") } - balanceKey := []byte(fmt.Sprintf("balance:%s", account.Hex())) - currentBalance, closer, err := s.db.Get(balanceKey) + query := ` + SELECT balance + FROM balances + WHERE account = $1; + ` + + row := s.db.QueryRowContext(ctx, query, account.Hex()) + if row.Err() != nil { + if errors.Is(row.Err(), sql.ErrNoRows) { + return nil, fmt.Errorf("account %s not found: %w", account.Hex(), ErrNotFound) + } + return nil, fmt.Errorf("failed to get balance for account %s: %w", account.Hex(), row.Err()) + } + + var balance string + err := row.Scan(&balance) if err != nil { - return nil, err + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("account %s not found: %w", account.Hex(), ErrNotFound) + } + return nil, fmt.Errorf("failed to scan balance for account %s: %w", account.Hex(), err) + } + + // Convert the balance string to a big.Int + balanceInt, ok := big.NewInt(0).SetString(balance, 10) + if !ok { + return nil, fmt.Errorf("failed to convert balance string to big.Int for account %s", account.Hex()) } - defer func() { - _ = closer.Close() - }() - return new(big.Int).SetBytes(currentBalance), nil + return balanceInt, nil } diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index 105d30234..6104a96bc 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -2,6 +2,9 @@ package store_test import ( "context" + "database/sql" + "encoding/hex" + "fmt" "math/big" "testing" "time" @@ -10,14 +13,64 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + _ "github.com/lib/pq" bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + "github.com/primev/mev-commit/tools/preconf-rpc/sender" "github.com/primev/mev-commit/tools/preconf-rpc/store" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" ) func TestStore(t *testing.T) { - t.Parallel() + ctx := context.Background() - st, err := store.New(t.TempDir()) + // Define the PostgreSQL container request + req := testcontainers.ContainerRequest{ + Image: "postgres:latest", + ExposedPorts: []string{"5432/tcp"}, + Env: map[string]string{ + "POSTGRES_DB": "testdb", + "POSTGRES_USER": "user", + "POSTGRES_PASSWORD": "password", + }, + WaitingFor: wait.ForListeningPort("5432/tcp"), + } + + // Start the PostgreSQL container + postgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + t.Fatalf("Failed to start PostgreSQL container: %s", err) + } + defer func() { + err := postgresContainer.Terminate(ctx) + if err != nil { + t.Errorf("Failed to terminate PostgreSQL container: %s", err) + } + }() + + // Retrieve the container's mapped port + mappedPort, err := postgresContainer.MappedPort(ctx, "5432") + if err != nil { + t.Fatalf("Failed to get mapped port: %s", err) + } + // Construct the database connection string + connStr := fmt.Sprintf("postgresql://user:password@localhost:%s/testdb?sslmode=disable", mappedPort.Port()) + + // Connect to the database + db, err := sql.Open("postgres", connStr) + if err != nil { + t.Fatalf("Failed to connect to PostgreSQL container: %s", err) + } + + err = db.Ping() + if err != nil { + t.Fatalf("Failed to ping PostgreSQL container: %s", err) + } + + st, err := store.New(db) if err != nil { t.Fatalf("failed to create store: %v", err) } @@ -28,62 +81,188 @@ func TestStore(t *testing.T) { } }) - 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) + // Test data common for all tests + txn1 := types.NewTransaction( + 0, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(1000000000), // 1 Gwei + 21000, // gas limit + big.NewInt(1000000000), // gas price + nil, // no data + ) + rawTxn1, err := txn1.MarshalBinary() + if err != nil { + t.Fatalf("failed to marshal transaction: %v", err) + } + wrappedTxn1 := &sender.Transaction{ + Transaction: txn1, + Raw: hex.EncodeToString(rawTxn1), + Sender: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), + Type: sender.TxTypeRegular, + Status: sender.TxStatusPending, + } + + txn2 := types.NewTransaction( + 1, + common.HexToAddress("0x0987654321098765432109876543210987654321"), + big.NewInt(2000000000), // 2 Gwei + 21000, // gas limit + big.NewInt(2000000000), // gas price + nil, // no data + ) + rawTxn2, err := txn2.MarshalBinary() + if err != nil { + t.Fatalf("failed to marshal second transaction: %v", err) + } + wrappedTxn2 := &sender.Transaction{ + Transaction: txn2, + Raw: hex.EncodeToString(rawTxn2), + Sender: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), + Type: sender.TxTypeRegular, + Status: sender.TxStatusPending, + } + + commitments := []*bidderapiv1.Commitment{ + { + TxHashes: []string{txn1.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{txn1.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(), + }, + } + + t.Run("AddQueuedTransaction", func(t *testing.T) { + err := st.AddQueuedTransaction(context.Background(), wrappedTxn1) if err != nil { - t.Errorf("failed to store preconfirmed transaction: %v", err) + t.Fatalf("failed to add queued transaction: %v", err) + } + + err = st.AddQueuedTransaction(context.Background(), wrappedTxn1) // Adding the same transaction again + if err == nil { + t.Fatalf("expected error when adding duplicate transaction, got nil") + } + + err = st.AddQueuedTransaction(context.Background(), wrappedTxn2) + if err != nil { + t.Fatalf("failed to add second queued transaction: %v", err) + } + }) + + t.Run("GetCurrentNonce", func(t *testing.T) { + senderAddress := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd") + nonce := st.GetCurrentNonce(context.Background(), senderAddress) + if nonce != 1 { + t.Fatalf("expected nonce 1, got %d", nonce) } + }) - storedTxn, storedCommitments, err := st.GetPreconfirmedTransaction(context.Background(), txn.Hash()) + t.Run("GetTransactionByHash", func(t *testing.T) { + retrievedTxn, err := st.GetTransactionByHash(context.Background(), wrappedTxn1.Hash()) if err != nil { - t.Errorf("failed to get preconfirmed transaction: %v", err) + t.Fatalf("failed to get transaction by hash: %v", err) + } + if diff := cmp.Diff(wrappedTxn1, retrievedTxn, cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + t.Fatalf("transaction mismatch (-want +got):\n%s", diff) } + }) - if txn.Hash().Hex() != storedTxn.Hash().Hex() { - t.Errorf("expected transaction hash %s, got %s", txn.Hash().Hex(), storedTxn.Hash().Hex()) + t.Run("GetQueuedTransactions", func(t *testing.T) { + retrievedTxns, err := st.GetQueuedTransactions(context.Background()) + if err != nil { + t.Fatalf("failed to get queued transactions: %v", err) + } + if len(retrievedTxns) != 1 { + t.Fatalf("expected 1 queued transaction, got %d", len(retrievedTxns)) } - if len(storedCommitments) != len(commitments) { - t.Errorf("expected %d commitments, got %d", len(commitments), len(storedCommitments)) + if diff := cmp.Diff(wrappedTxn1, retrievedTxns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + t.Fatalf("queued transaction mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("StoreTransaction", func(t *testing.T) { + wrappedTxn1.Status = sender.TxStatusPreConfirmed + wrappedTxn1.BlockNumber = 1 + + err := st.StoreTransaction(context.Background(), wrappedTxn1, commitments) + if err != nil { + t.Errorf("failed to store preconfirmed transaction: %v", err) } + commitments, err := st.GetTransactionCommitments(context.Background(), wrappedTxn1.Hash()) + if err != nil { + t.Errorf("failed to get transaction commitments: %v", err) + } + if len(commitments) != 2 { + t.Errorf("expected 2 commitments, got %d", len(commitments)) + } for i, commitment := range commitments { - if diff := cmp.Diff(commitment, storedCommitments[i], cmpopts.IgnoreUnexported(bidderapiv1.Commitment{})); diff != "" { + if diff := cmp.Diff(commitment, commitments[i], cmpopts.IgnoreUnexported(bidderapiv1.Commitment{}, types.Transaction{})); diff != "" { t.Errorf("commitment mismatch (-want +got):\n%s", diff) } } + + nextTxns, err := st.GetQueuedTransactions(context.Background()) + if err != nil { + t.Errorf("failed to get queued transactions: %v", err) + } + if len(nextTxns) != 1 { + t.Errorf("expected 1 queued transaction, got %d", len(nextTxns)) + } + if diff := cmp.Diff(wrappedTxn2, nextTxns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + t.Errorf("queued transaction mismatch (-want +got):\n%s", diff) + } + + txns, err := st.GetTransactionsForBlock(context.Background(), 1) + if err != nil { + t.Errorf("failed to get transactions for block: %v", err) + } + if len(txns) != 1 { + t.Errorf("expected 1 transaction for block 1, got %d", len(txns)) + } + if diff := cmp.Diff(wrappedTxn1, txns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + t.Errorf("transaction mismatch (-want +got):\n%s", diff) + } + + wrappedTxn2.Status = sender.TxStatusFailed + wrappedTxn2.Details = "Transaction failed due to insufficient funds" + wrappedTxn2.BlockNumber = 2 + err = st.StoreTransaction(context.Background(), wrappedTxn2, nil) + if err != nil { + t.Errorf("failed to store failed transaction: %v", err) + } + + failedTxn, err := st.GetTransactionByHash(context.Background(), wrappedTxn2.Hash()) + if err != nil { + t.Errorf("failed to get failed transaction by hash: %v", err) + } + + if diff := cmp.Diff(wrappedTxn2, failedTxn, cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + t.Errorf("failed transaction mismatch (-want +got):\n%s", diff) + } + + noTxns, err := st.GetTransactionsForBlock(context.Background(), 2) + if err != nil { + t.Errorf("failed to get transactions for block 2: %v", err) + } + if len(noTxns) != 0 { + t.Errorf("expected no transactions for block 2, got %d", len(noTxns)) + } }) t.Run("Account Balance", func(t *testing.T) {