From 1887fa953521eeb0d678a0cd1dbe2edfb9e77e70 Mon Sep 17 00:00:00 2001 From: Zehui Zheng Date: Mon, 2 Dec 2024 22:25:55 -0800 Subject: [PATCH] espresso reader --- .gitignore | 17 + .gitmodules | 0 .golangci.yml | 16 + CODE_OF_CONDUCT.md | 78 + LICENSE | 201 +++ Makefile | 18 + README.md | 9 + go.mod | 105 ++ go.sum | 367 +++++ internal/config/config.go | 147 ++ internal/config/generate/Config.toml | 265 ++++ internal/config/generate/code.go | 195 +++ internal/config/generate/docs.go | 63 + internal/config/generate/env.go | 33 + internal/config/generate/helpers.go | 59 + internal/config/generate/main.go | 24 + internal/config/generated.go | 541 +++++++ internal/espressoreader/espresso_db.go | 130 ++ internal/espressoreader/espresso_reader.go | 494 ++++++ internal/espressoreader/extract.go | 59 + .../service/espressoreader_service.go | 275 ++++ internal/evmreader/abi.json | 28 + internal/evmreader/application_adapter.go | 55 + internal/evmreader/claim.go | 251 ++++ internal/evmreader/claim_test.go | 731 +++++++++ internal/evmreader/consensus_adapter.go | 58 + internal/evmreader/evmreader.go | 358 +++++ internal/evmreader/evmreader_test.go | 765 ++++++++++ internal/evmreader/input.go | 387 +++++ internal/evmreader/input_test.go | 388 +++++ internal/evmreader/inputsource_adapter.go | 55 + internal/evmreader/output.go | 120 ++ internal/evmreader/output_test.go | 621 ++++++++ .../evmreader/retrypolicy/contractfactory.go | 71 + .../retrypolicy_application_delegator.go | 54 + .../retrypolicy_consensus_delegator.go | 73 + .../retrypolicy_ethclient_delegator.go | 64 + .../retrypolicy_ethwsclient_delegator.go | 61 + .../retrypolicy_inputsource_delegator.go | 67 + .../evmreader/service/evmreader_service.go | 87 ++ internal/evmreader/testdata/header_0.json | 14 + internal/evmreader/testdata/header_1.json | 14 + internal/evmreader/testdata/header_2.json | 14 + .../testdata/input_added_event_0.json | 20 + .../testdata/input_added_event_1.json | 20 + .../testdata/input_added_event_2.json | 20 + .../testdata/input_added_event_3.json | 20 + internal/evmreader/util.go | 57 + internal/model/models.go | 146 ++ internal/repository/base.go | 1333 +++++++++++++++++ internal/repository/base_test.go | 471 ++++++ internal/repository/evmreader.go | 455 ++++++ internal/repository/evmreader_test.go | 232 +++ ...ut_claim_output_report_nodeconfig.down.sql | 18 + ...nput_claim_output_report_nodeconfig.up.sql | 201 +++ .../000002_create_postgraphile_view.down.sql | 4 + .../000002_create_postgraphile_view.up.sql | 92 ++ internal/repository/schema/schema.go | 85 ++ internal/services/retry/retry.go | 49 + internal/services/retry/retry_test.go | 71 + internal/services/startup/startup.go | 77 + main.go | 98 ++ pkg/contracts/generate/main.go | 149 ++ pkg/contracts/iapplication/iapplication.go | 722 +++++++++ .../iapplicationfactory.go | 401 +++++ .../iauthorityfactory/iauthorityfactory.go | 388 +++++ pkg/contracts/iconsensus/iconsensus.go | 565 +++++++ pkg/contracts/iinputbox/iinputbox.go | 418 ++++++ pkg/contracts/inputs/inputs.go | 202 +++ .../iselfhostedapplicationfactory.go | 296 ++++ pkg/contracts/main.go | 10 + pkg/contracts/outputs/outputs.go | 244 +++ 72 files changed, 14296 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .golangci.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/generate/Config.toml create mode 100644 internal/config/generate/code.go create mode 100644 internal/config/generate/docs.go create mode 100644 internal/config/generate/env.go create mode 100644 internal/config/generate/helpers.go create mode 100644 internal/config/generate/main.go create mode 100644 internal/config/generated.go create mode 100644 internal/espressoreader/espresso_db.go create mode 100644 internal/espressoreader/espresso_reader.go create mode 100644 internal/espressoreader/extract.go create mode 100644 internal/espressoreader/service/espressoreader_service.go create mode 100644 internal/evmreader/abi.json create mode 100644 internal/evmreader/application_adapter.go create mode 100644 internal/evmreader/claim.go create mode 100644 internal/evmreader/claim_test.go create mode 100644 internal/evmreader/consensus_adapter.go create mode 100644 internal/evmreader/evmreader.go create mode 100644 internal/evmreader/evmreader_test.go create mode 100644 internal/evmreader/input.go create mode 100644 internal/evmreader/input_test.go create mode 100644 internal/evmreader/inputsource_adapter.go create mode 100644 internal/evmreader/output.go create mode 100644 internal/evmreader/output_test.go create mode 100644 internal/evmreader/retrypolicy/contractfactory.go create mode 100644 internal/evmreader/retrypolicy/retrypolicy_application_delegator.go create mode 100644 internal/evmreader/retrypolicy/retrypolicy_consensus_delegator.go create mode 100644 internal/evmreader/retrypolicy/retrypolicy_ethclient_delegator.go create mode 100644 internal/evmreader/retrypolicy/retrypolicy_ethwsclient_delegator.go create mode 100644 internal/evmreader/retrypolicy/retrypolicy_inputsource_delegator.go create mode 100644 internal/evmreader/service/evmreader_service.go create mode 100644 internal/evmreader/testdata/header_0.json create mode 100644 internal/evmreader/testdata/header_1.json create mode 100644 internal/evmreader/testdata/header_2.json create mode 100644 internal/evmreader/testdata/input_added_event_0.json create mode 100644 internal/evmreader/testdata/input_added_event_1.json create mode 100644 internal/evmreader/testdata/input_added_event_2.json create mode 100644 internal/evmreader/testdata/input_added_event_3.json create mode 100644 internal/evmreader/util.go create mode 100644 internal/model/models.go create mode 100644 internal/repository/base.go create mode 100644 internal/repository/base_test.go create mode 100644 internal/repository/evmreader.go create mode 100644 internal/repository/evmreader_test.go create mode 100644 internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.down.sql create mode 100644 internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.up.sql create mode 100644 internal/repository/schema/migrations/000002_create_postgraphile_view.down.sql create mode 100644 internal/repository/schema/migrations/000002_create_postgraphile_view.up.sql create mode 100644 internal/repository/schema/schema.go create mode 100644 internal/services/retry/retry.go create mode 100644 internal/services/retry/retry_test.go create mode 100644 internal/services/startup/startup.go create mode 100644 main.go create mode 100644 pkg/contracts/generate/main.go create mode 100644 pkg/contracts/iapplication/iapplication.go create mode 100644 pkg/contracts/iapplicationfactory/iapplicationfactory.go create mode 100644 pkg/contracts/iauthorityfactory/iauthorityfactory.go create mode 100644 pkg/contracts/iconsensus/iconsensus.go create mode 100644 pkg/contracts/iinputbox/iinputbox.go create mode 100644 pkg/contracts/inputs/inputs.go create mode 100644 pkg/contracts/iselfhostedapplicationfactory/iselfhostedapplicationfactory.go create mode 100644 pkg/contracts/main.go create mode 100644 pkg/contracts/outputs/outputs.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0e6dd3a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.vscode +.env +.DS_Store +build/deployments +**/.idea/ +machine-snapshot/** +/deployment.json +/anvil_state.json +/cartesi-rollups-authority-claimer +/cartesi-rollups-cli +/cartesi-rollups-evm-reader +/cartesi-rollups-node +/cartesi-rollups-advancer +/cartesi-rollups-validator +/cartesi-rollups-claimer +/rollups-contracts +espresso-reader diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..178b70c3 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,16 @@ +linters: + enable: + - exhaustive + - goconst + #- godox + - gofmt + - lll + - mnd + - misspell +linters-settings: + lll: + line-length: 120 + tab-width: 4 + mnd: + ignored-functions: + - "^make" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..24e7db3b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,78 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at contact@cartesi.foundation. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..fe4fea66 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright Cartesi and individual authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b972f75f --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +env: + @echo export CGO_CFLAGS=\"$(CGO_CFLAGS)\" + @echo export CGO_LDFLAGS=\"$(CGO_LDFLAGS)\" + @echo export CARTESI_LOG_LEVEL="debug" + @echo export CARTESI_BLOCKCHAIN_HTTP_ENDPOINT="" + @echo export CARTESI_BLOCKCHAIN_WS_ENDPOINT="" + @echo export CARTESI_BLOCKCHAIN_ID="11155111" + @echo export CARTESI_CONTRACTS_INPUT_BOX_ADDRESS="0x593E5BCf894D6829Dd26D0810DA7F064406aebB6" + @echo export CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER="6850934" + @echo export CARTESI_AUTH_MNEMONIC=\"test test test test test test test test test test test junk\" + @echo export CARTESI_POSTGRES_ENDPOINT="postgres://postgres:password@localhost:5432/rollupsdb?sslmode=disable" + @echo export CARTESI_TEST_POSTGRES_ENDPOINT="postgres://test_user:password@localhost:5432/test_rollupsdb?sslmode=disable" + @echo export CARTESI_TEST_MACHINE_IMAGES_PATH=\"$(CARTESI_TEST_MACHINE_IMAGES_PATH)\" + @echo export PATH=$(CURDIR):$$PATH + @echo export ESPRESSO_BASE_URL="https://query.decaf.testnet.espresso.network/v0" + @echo export ESPRESSO_STARTING_BLOCK="882494" + @echo export ESPRESSO_NAMESPACE="55555" + @echo export MAIN_SEQUENCER="espresso" # set to either espresso or ethereum diff --git a/README.md b/README.md new file mode 100644 index 00000000..8f77f5c8 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +To buid +``` +go build +``` + +To run +``` +./espresso-reader +``` diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..be96ea9f --- /dev/null +++ b/go.mod @@ -0,0 +1,105 @@ +module github.com/ZzzzHui/espresso-reader + +go 1.23.2 + +require ( + github.com/ethereum/go-ethereum v1.14.11 + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 + github.com/tyler-smith/go-bip32 v1.0.0 + github.com/tyler-smith/go-bip39 v1.1.0 +) + +require github.com/BurntSushi/toml v1.4.0 + +require ( + github.com/EspressoSystems/espresso-sequencer-go v0.0.26 + github.com/aws/aws-sdk-go-v2 v1.32.2 + github.com/aws/aws-sdk-go-v2/config v1.18.45 + github.com/aws/aws-sdk-go-v2/service/kms v1.37.2 + github.com/deepmap/oapi-codegen/v2 v2.1.0 + github.com/golang-migrate/migrate/v4 v4.18.1 + github.com/jackc/pgx/v5 v5.7.1 + github.com/lmittmann/tint v1.0.5 + github.com/mattn/go-isatty v0.0.20 + github.com/oapi-codegen/runtime v1.1.1 + github.com/tidwall/gjson v1.18.0 + golang.org/x/sync v0.8.0 + golang.org/x/text v0.19.0 +) + +require ( + github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect + github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect + github.com/aws/smithy-go v1.22.0 // indirect + github.com/bits-and-blooms/bitset v1.14.3 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/consensys/bavard v0.1.22 // indirect + github.com/consensys/gnark-crypto v0.14.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/ethereum/c-kzg-4844 v1.0.3 // indirect + github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/getkin/kin-openapi v0.123.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/holiman/uint256 v1.3.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/supranational/blst v0.3.13 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/tools v0.26.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..bcb1b6f6 --- /dev/null +++ b/go.sum @@ -0,0 +1,367 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/EspressoSystems/espresso-sequencer-go v0.0.26 h1:06qXjTG1EUUGZVkvgMMWRv3/Zak6qYQphCwncVqrzNk= +github.com/EspressoSystems/espresso-sequencer-go v0.0.26/go.mod h1:BbU8N23RGl45QXSf/bYc8OQ8TG/vlMaPC1GU1acqKmc= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.18.45 h1:Aka9bI7n8ysuwPeFdm77nfbyHCAKQ3z9ghB3S/38zes= +github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.2 h1:tfBABi5R6aSZlhgTWHxL+opYUDOnIGoNcJLwVYv0jLM= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.2/go.mod h1:dZYFcQwuoh+cLOlFnZItijZptmyDhRIkOKWFO1CfzV8= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +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.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA= +github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +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/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= +github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= +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-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A= +github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E= +github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/deepmap/oapi-codegen/v2 v2.1.0 h1:I/NMVhJCtuvL9x+S2QzZKpSjGi33oDZwPRdemvOZWyQ= +github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8= +github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= +github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +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 v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +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 v1.0.3 h1:IEnbOHwjixW2cTvKRUlAAUOeleV7nNM/umJR+qy4WDs= +github.com/ethereum/c-kzg-4844 v1.0.3/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.11 h1:8nFDCUUE67rPc6AKxFj7JKaOa2W/W1Rse3oS6LvvxEY= +github.com/ethereum/go-ethereum v1.14.11/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= +github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +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.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/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= +github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +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.8/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= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +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/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= +github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +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/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= +github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.0/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= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 00000000..34d4dc85 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,147 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// The config package manages the node configuration, which comes from environment variables. +// The sub-package generate specifies these environment variables. +package config + +import ( + "fmt" + "os" +) + +// NodeConfig contains all the Node variables. +// See the corresponding environment variable for the variable documentation. +type NodeConfig struct { + LogLevel LogLevel + LogPrettyEnabled bool + BlockchainID uint64 + BlockchainHttpEndpoint Redacted[string] + BlockchainWsEndpoint Redacted[string] + LegacyBlockchainEnabled bool + EvmReaderDefaultBlock DefaultBlock + EvmReaderRetryPolicyMaxRetries uint64 + EvmReaderRetryPolicyMaxDelay Duration + BlockchainBlockTimeout int + ContractsInputBoxAddress string + ContractsInputBoxDeploymentBlockNumber int64 + SnapshotDir string + PostgresEndpoint Redacted[string] + HttpAddress string + HttpPort int + FeatureClaimSubmissionEnabled bool + FeatureMachineHashCheckEnabled bool + Auth Auth + AdvancerPollingInterval Duration + ValidatorPollingInterval Duration + ClaimerPollingInterval Duration + EspressoBaseUrl string + EspressoStartingBlock uint64 + EspressoNamespace uint64 + EspressoServiceEndpoint string + MainSequencer string +} + +// Auth is used to sign transactions. +type Auth any + +// AuthPrivateKey allows signing through private keys. +type AuthPrivateKey struct { + PrivateKey Redacted[string] +} + +// AuthMnemonic allows signing through mnemonics. +type AuthMnemonic struct { + Mnemonic Redacted[string] + AccountIndex Redacted[int] +} + +// AuthAWS allows signing through AWS services. +type AuthAWS struct { + KeyID Redacted[string] + Region Redacted[string] +} + +// Redacted is a wrapper that redacts a given field from the logs. +type Redacted[T any] struct { + Value T +} + +func (r Redacted[T]) String() string { + return "[REDACTED]" +} + +// FromEnv loads the config from environment variables. +func FromEnv() NodeConfig { + var config NodeConfig + config.LogLevel = GetLogLevel() + config.LogPrettyEnabled = GetLogPrettyEnabled() + config.BlockchainID = GetBlockchainId() + config.BlockchainHttpEndpoint = Redacted[string]{GetBlockchainHttpEndpoint()} + config.BlockchainWsEndpoint = Redacted[string]{GetBlockchainWsEndpoint()} + config.LegacyBlockchainEnabled = GetLegacyBlockchainEnabled() + config.EvmReaderDefaultBlock = GetEvmReaderDefaultBlock() + config.EvmReaderRetryPolicyMaxRetries = GetEvmReaderRetryPolicyMaxRetries() + config.EvmReaderRetryPolicyMaxDelay = GetEvmReaderRetryPolicyMaxDelay() + config.BlockchainBlockTimeout = GetBlockchainBlockTimeout() + config.ContractsInputBoxAddress = GetContractsInputBoxAddress() + config.ContractsInputBoxDeploymentBlockNumber = GetContractsInputBoxDeploymentBlockNumber() + config.SnapshotDir = GetSnapshotDir() + config.PostgresEndpoint = Redacted[string]{GetPostgresEndpoint()} + config.HttpAddress = GetHttpAddress() + config.HttpPort = GetHttpPort() + config.FeatureClaimSubmissionEnabled = GetFeatureClaimSubmissionEnabled() + config.FeatureMachineHashCheckEnabled = GetFeatureMachineHashCheckEnabled() + if config.FeatureClaimSubmissionEnabled { + config.Auth = AuthFromEnv() + } + config.AdvancerPollingInterval = GetAdvancerPollingInterval() + config.ValidatorPollingInterval = GetValidatorPollingInterval() + config.ClaimerPollingInterval = GetClaimerPollingInterval() + config.EspressoBaseUrl = GetBaseUrl() + config.EspressoStartingBlock = GetStartingBlock() + config.EspressoNamespace = GetNamespace() + config.EspressoServiceEndpoint = GetServiceEndpoint() + config.MainSequencer = GetSequencer() + return config +} + +func AuthFromEnv() Auth { + switch GetAuthKind() { + case AuthKindPrivateKeyVar: + return AuthPrivateKey{ + PrivateKey: Redacted[string]{GetAuthPrivateKey()}, + } + case AuthKindPrivateKeyFile: + path := GetAuthPrivateKeyFile() + privateKey, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("failed to read private-key file: %v", err)) + } + return AuthPrivateKey{ + PrivateKey: Redacted[string]{string(privateKey)}, + } + case AuthKindMnemonicVar: + return AuthMnemonic{ + Mnemonic: Redacted[string]{GetAuthMnemonic()}, + AccountIndex: Redacted[int]{GetAuthMnemonicAccountIndex()}, + } + case AuthKindMnemonicFile: + path := GetAuthMnemonicFile() + mnemonic, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("failed to read mnemonic file: %v", err)) + } + return AuthMnemonic{ + Mnemonic: Redacted[string]{string(mnemonic)}, + AccountIndex: Redacted[int]{GetAuthMnemonicAccountIndex()}, + } + case AuthKindAWS: + return AuthAWS{ + KeyID: Redacted[string]{GetAuthAwsKmsKeyId()}, + Region: Redacted[string]{GetAuthAwsKmsRegion()}, + } + default: + panic("invalid auth kind") + } +} diff --git a/internal/config/generate/Config.toml b/internal/config/generate/Config.toml new file mode 100644 index 00000000..66777b2d --- /dev/null +++ b/internal/config/generate/Config.toml @@ -0,0 +1,265 @@ +# (c) Cartesi and individual authors (see AUTHORS) +# SPDX-License-Identifier: Apache-2.0 (see LICENSE) +# +# Logging +# +[logging.CARTESI_LOG_LEVEL] +default = "info" +go-type = "LogLevel" +description = """ +One of "debug", "info", "warn", "error".""" + +[logging.CARTESI_LOG_PRETTY_ENABLED] +default = "false" +go-type = "bool" +description = """ +If set to true, the node will add colors to its log output.""" + +# +# Features +# + +[features.CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED] +default = "true" +go-type = "bool" +description = """ +If set to false, the node will not submit claims (reader mode).""" + +[features.CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED] +default = "true" +go-type = "bool" +description = """ +If set to false, the node will *not* check whether the Cartesi machine hash from +the snapshot matches the hash in the Application contract.""" + +# +# Rollups +# + +[rollups.CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES] +default = "3" +go-type = "uint64" +description = """ +How many times some functions should be retried after an error.""" + +[rollups.CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY] +default = "3" +go-type = "Duration" +description = """ +How many seconds the retry policy will wait between retries.""" + +[rollups.CARTESI_ADVANCER_POLLING_INTERVAL] +default = "7" +go-type = "Duration" +description = """ +How many seconds the node will wait before querying the database for new inputs.""" + +[rollups.CARTESI_VALIDATOR_POLLING_INTERVAL] +default = "7" +go-type = "Duration" +description = """ +How many seconds the node will wait before trying to finish epochs for all applications.""" + +[rollups.CARTESI_CLAIMER_POLLING_INTERVAL] +default = "7" +go-type = "Duration" +description = """ +How many seconds the node will wait before querying the database for new claims.""" + +# +# Blockchain +# + +[blockchain.CARTESI_BLOCKCHAIN_ID] +go-type = "uint64" +description = """ +An unique identifier representing a blockchain network.""" + +[blockchain.CARTESI_BLOCKCHAIN_HTTP_ENDPOINT] +go-type = "string" +description = """ +HTTP endpoint for the blockchain RPC provider.""" + +[blockchain.CARTESI_BLOCKCHAIN_WS_ENDPOINT] +go-type = "string" +description = """ +WebSocket endpoint for the blockchain RPC provider.""" + +[blockchain.CARTESI_LEGACY_BLOCKCHAIN_ENABLED] +default = "false" +go-type = "bool" +description = """ +If set to true the node will send transactions using the legacy gas fee model +(instead of EIP-1559).""" + +[blockchain.CARTESI_EVM_READER_DEFAULT_BLOCK] +default = "finalized" +go-type = "DefaultBlock" +description = """ +The default block to be used by EVM Reader when requesting new blocks. +One of 'latest', 'pending', 'safe', 'finalized'""" + +[blockchain.CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT] +default = "60" +go-type = "int" +description = """ +Block subscription timeout in seconds.""" + +# +# Contracts +# + +[contracts.CARTESI_CONTRACTS_INPUT_BOX_ADDRESS] +go-type = "string" +description = """ +Address of the InputBox contract.""" + +[contracts.CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER] +go-type = "int64" +description = """ +The deployment block for the input box contract. +The node will begin to read blockchain events from this block.""" + +# +# Snapshot +# + +[snapshot.CARTESI_SNAPSHOT_DIR] +default = "/var/lib/cartesi-rollups-node/snapshots" +go-type = "string" +description = """ +Path to the directory with the cartesi-machine snapshot that will be loaded by the node.""" + +# +# Auth +# + +[auth.CARTESI_AUTH_KIND] +default = "mnemonic" +go-type = "AuthKind" +description = """ +One of "private_key", "private_key_file", "mnemonic", "mnemonic_file", "aws".""" + +[auth.CARTESI_AUTH_PRIVATE_KEY] +go-type = "string" +description = """ +The node will use this private key to sign transactions.""" + +[auth.CARTESI_AUTH_PRIVATE_KEY_FILE] +go-type = "string" +description = """ +The node will use the private key contained in this file to sign transactions.""" + +[auth.CARTESI_AUTH_MNEMONIC] +go-type = "string" +description = """ +The node will use the private key generated from this mnemonic to sign transactions.""" + +[auth.CARTESI_AUTH_MNEMONIC_FILE] +go-type = "string" +description = """ +The node will use the private key generated from the mnemonic contained in this file +to sign transactions.""" + +[auth.CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX] +default = "0" +go-type = "int" +description = """ +When using mnemonics to sign transactions, +the node will use this account index to generate the private key.""" + +[auth.CARTESI_AUTH_AWS_KMS_KEY_ID] +go-type = "string" +description = """ +If set, the node will use the AWS KMS service with this key ID to sign transactions. + +Must be set alongside `CARTESI_AUTH_AWS_KMS_REGION`.""" + +[auth.CARTESI_AUTH_AWS_KMS_REGION] +go-type = "string" +description = """ +An AWS KMS Region. + +Must be set alongside `CARTESI_AUTH_AWS_KMS_KEY_ID`.""" + +# +# Postgres +# + +[postgres.CARTESI_POSTGRES_ENDPOINT] +default = "" +go-type = "string" +description = """ +Postgres endpoint in the 'postgres://user:password@hostname:port/database' format. + +If not set, or set to empty string, will defer the behaviour to the PG driver. +See [this](https://www.postgresql.org/docs/current/libpq-envars.html) for more information. + +It is also possible to set the endpoint without a password and load it from Postgres' passfile. +See [this](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-PASSFILE) +for more information.""" + +# +# HTTP +# + +[http.CARTESI_HTTP_ADDRESS] +default = "127.0.0.1" +go-type = "string" +description = """ +HTTP address for the node.""" + +[http.CARTESI_HTTP_PORT] +default = "10000" +go-type = "int" +description = """ +HTTP port for the node. +The node will also use the 20 ports after this one for internal services.""" + +# +# Espresso +# + +[espresso.ESPRESSO_BASE_URL] +default = "" +go-type = "string" +description = """ +Espresso base url.""" + +[espresso.ESPRESSO_STARTING_BLOCK] +default = "0" +go-type = "uint64" +description = """ +Espresso starting block.""" + +[espresso.ESPRESSO_NAMESPACE] +default = "0" +go-type = "uint64" +description = """ +Espresso namespace.""" + +[espresso.ESPRESSO_SERVICE_ENDPOINT] +default = "localhost:8080" +go-type = "string" +description = """ +URL to Espresso nonce and submit service""" + +# +# Sequencer +# + +[sequencer.MAIN_SEQUENCER] +default = "espresso" +go-type = "string" +description = """ +Main sequencer.""" + +# +# Temporary +# + +[temp.CARTESI_MACHINE_SERVER_VERBOSITY] +default = "info" +go-type = "string" +description = """ +TODO.""" diff --git a/internal/config/generate/code.go b/internal/config/generate/code.go new file mode 100644 index 00000000..3aa462a1 --- /dev/null +++ b/internal/config/generate/code.go @@ -0,0 +1,195 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "bytes" + "os" + "strings" + "text/template" + + "go/format" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// generateCodeFile generates a Go file with the getters for the config variables. +func generateCodeFile(path string, env []Env) { + // Load template + funcMap := template.FuncMap{ + "toFunctionName": func(env string) string { + caser := cases.Title(language.English) + words := strings.Split(env, "_") + for i, word := range words { + words[i] = caser.String(word) + } + return strings.Join(words[1:], "") + }, + "toGoFunc": func(goType string) string { + return "to" + strings.ToUpper(goType[:1]) + goType[1:] + }, + } + tmpl := template.Must(template.New("code").Funcs(funcMap).Parse(codeTemplate)) + + // Execute template + var buff bytes.Buffer + err := tmpl.Execute(&buff, env) + if err != nil { + panic(err) + } + + // Format code + code, err := format.Source(buff.Bytes()) + if err != nil { + panic(err) + } + + // Write file + var perm os.FileMode = 0644 + err = os.WriteFile(path, code, perm) + if err != nil { + panic(err) + } +} + +const codeTemplate string = `// Code generated by internal/config/generate. +// DO NOT EDIT. + +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package config + +import ( + "fmt" + "log/slog" + "os" + "strconv" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/model" +) + +type ( + Duration = time.Duration + LogLevel = slog.Level + DefaultBlock = model.DefaultBlock +) + +// ------------------------------------------------------------------------------------------------ +// Auth Kind +// ------------------------------------------------------------------------------------------------ + +type AuthKind uint8 + +const ( + AuthKindPrivateKeyVar AuthKind = iota + AuthKindPrivateKeyFile + AuthKindMnemonicVar + AuthKindMnemonicFile + AuthKindAWS +) + +// ------------------------------------------------------------------------------------------------ +// Parsing functions +// ------------------------------------------------------------------------------------------------ + +func ToInt64FromString(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + +func ToUint64FromString(s string) (uint64, error) { + value, err := strconv.ParseUint(s, 10, 64) + return value, err +} + +func ToStringFromString(s string) (string, error) { + return s, nil +} + +func ToDurationFromSeconds(s string) (time.Duration, error) { + return time.ParseDuration(s + "s") +} + +func ToLogLevelFromString(s string) (LogLevel, error) { + var m = map[string]LogLevel{ + "debug": slog.LevelDebug, + "info": slog.LevelInfo, + "warn": slog.LevelWarn, + "error": slog.LevelError, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue LogLevel + return zeroValue, fmt.Errorf("invalid log level '%s'", s) + } +} + +func ToDefaultBlockFromString(s string) (DefaultBlock,error){ + var m = map[string]DefaultBlock{ + "latest" : model.DefaultBlockStatusLatest, + "pending" : model.DefaultBlockStatusPending, + "safe" : model.DefaultBlockStatusSafe, + "finalized": model.DefaultBlockStatusFinalized, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue DefaultBlock + return zeroValue, fmt.Errorf("invalid default block '%s'", s) + } +} + +func ToAuthKindFromString(s string) (AuthKind, error) { + var m = map[string]AuthKind{ + "private_key": AuthKindPrivateKeyVar, + "private_key_file": AuthKindPrivateKeyFile, + "mnemonic": AuthKindMnemonicVar, + "mnemonic_file": AuthKindMnemonicFile, + "aws": AuthKindAWS, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue AuthKind + return zeroValue, fmt.Errorf("invalid auth kind '%s'", s) + } +} + +// Aliases to be used by the generated functions. +var ( + toBool = strconv.ParseBool + toInt = strconv.Atoi + toInt64 = ToInt64FromString + toUint64 = ToUint64FromString + toString = ToStringFromString + toDuration = ToDurationFromSeconds + toLogLevel = ToLogLevelFromString + toAuthKind = ToAuthKindFromString + toDefaultBlock = ToDefaultBlockFromString +) + +// ------------------------------------------------------------------------------------------------ +// Getters +// ------------------------------------------------------------------------------------------------ + +{{range .}} +func Get{{toFunctionName .Name}}() {{.GoType}} { + s, ok := os.LookupEnv("{{.Name}}") + if !ok { + {{- if .Default}} + s = "{{.Default}}" + {{- else}} + panic("missing env var {{.Name}}") + {{- end}} + } + val, err := {{toGoFunc .GoType}}(s) + if err != nil { + panic(fmt.Sprintf("failed to parse {{.Name}}: %v", err)) + } + return val +} +{{end}}` diff --git a/internal/config/generate/docs.go b/internal/config/generate/docs.go new file mode 100644 index 00000000..be2af96c --- /dev/null +++ b/internal/config/generate/docs.go @@ -0,0 +1,63 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "os" + "text/template" +) + +// generateDocsFile generates a Markdown file with the documentation of the config variables. +func generateDocsFile(path string, env []Env) { + // Open output file + file, err := os.Create(path) + if err != nil { + panic(err) + } + defer file.Close() + + // Load template + funcMap := template.FuncMap{ + "backtick": func(s string) string { + return "`" + s + "`" + }, + "quote": func(s string) string { + return `"` + s + `"` + }, + } + tmpl := template.Must(template.New("docs").Funcs(funcMap).Parse(docsTemplate)) + + // Execute template + err = tmpl.Execute(file, env) + if err != nil { + panic(err) + } +} + +const docsTemplate string = ` + + +# Node Configuration + +The node is configurable through environment variables. +(There is no other way to configure it.) + +This file documents the configuration options. + + +{{- range .}} + +## {{backtick .Name}} + +{{.Description}} + +* **Type:** {{backtick .GoType}} +{{- if .Default}} +* **Default:** {{.Default | quote | backtick}} +{{- end}} +{{- end}} +` diff --git a/internal/config/generate/env.go b/internal/config/generate/env.go new file mode 100644 index 00000000..91590b1a --- /dev/null +++ b/internal/config/generate/env.go @@ -0,0 +1,33 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +// An entry in the toml's top level table representing an environment variable. +type Env struct { + // Name of the environment variable. + Name string + + // The default value for the variable. + // This field is optional. + Default *string `toml:"default"` + + // The Go type for the environment variable. + // This field is required. + GoType string `toml:"go-type"` + + // A brief description of the environment variable. + // This field is required. + Description string `toml:"description"` +} + +// Validates whether the fields of the environment variables were initialized correctly +// and sets defaults for optional fields. +func (e *Env) validate() { + if e.GoType == "" { + panic("missing go-type for " + e.Name) + } + if e.Description == "" { + panic("missing description for " + e.Name) + } +} diff --git a/internal/config/generate/helpers.go b/internal/config/generate/helpers.go new file mode 100644 index 00000000..fa017ca3 --- /dev/null +++ b/internal/config/generate/helpers.go @@ -0,0 +1,59 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "os" + "sort" + + "github.com/BurntSushi/toml" +) + +func readTOML(name string) string { + bytes, err := os.ReadFile(name) + if err != nil { + panic(err) + } + return string(bytes) +} + +type configTOML = map[string](map[string]*Env) + +func decodeTOML(data string) configTOML { + var config configTOML + _, err := toml.Decode(data, &config) + if err != nil { + panic(err) + } + return config +} + +// Creates sorted lists of environment variables from the config +// to make the generated files deterministic. +func sortConfig(config configTOML) []Env { + var topics []string + mapping := make(map[string]([]string)) // topic names to env names + + for name, topic := range config { + var envs []string + for name, env := range topic { + env.Name = name // initializes the environment variable's name + envs = append(envs, name) + } + sort.Strings(envs) + + topics = append(topics, name) + mapping[name] = envs + } + sort.Strings(topics) + + var envs []Env + for _, topic := range topics { + for _, name := range mapping[topic] { + envs = append(envs, *config[topic][name]) + } + } + + return envs +} diff --git a/internal/config/generate/main.go b/internal/config/generate/main.go new file mode 100644 index 00000000..f75cef27 --- /dev/null +++ b/internal/config/generate/main.go @@ -0,0 +1,24 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +//go:generate go run . + +// This script will read the Config.toml file and create: +// - a formatted get.go file, with get functions for each environment variable; +// - a config.md file with documentation for the environment variables. +// +// Each table entry in the toml file translates into an environment variable. +// In Go, this becomes a map[string](map[string]Env), with the keys of the outer map being topic +// names, and the keys of the inner map being variable names. +package main + +func main() { + data := readTOML("Config.toml") + config := decodeTOML(data) + envs := sortConfig(config) + for _, env := range envs { + env.validate() + } + generateDocsFile("../../../docs/config.md", envs) + generateCodeFile("../generated.go", envs) +} diff --git a/internal/config/generated.go b/internal/config/generated.go new file mode 100644 index 00000000..83d3b890 --- /dev/null +++ b/internal/config/generated.go @@ -0,0 +1,541 @@ +// Code generated by internal/config/generate. +// DO NOT EDIT. + +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package config + +import ( + "fmt" + "log/slog" + "os" + "strconv" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/model" +) + +type ( + Duration = time.Duration + LogLevel = slog.Level + DefaultBlock = model.DefaultBlock +) + +// ------------------------------------------------------------------------------------------------ +// Auth Kind +// ------------------------------------------------------------------------------------------------ + +type AuthKind uint8 + +const ( + AuthKindPrivateKeyVar AuthKind = iota + AuthKindPrivateKeyFile + AuthKindMnemonicVar + AuthKindMnemonicFile + AuthKindAWS +) + +// ------------------------------------------------------------------------------------------------ +// Parsing functions +// ------------------------------------------------------------------------------------------------ + +func ToInt64FromString(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + +func ToUint64FromString(s string) (uint64, error) { + value, err := strconv.ParseUint(s, 10, 64) + return value, err +} + +func ToStringFromString(s string) (string, error) { + return s, nil +} + +func ToDurationFromSeconds(s string) (time.Duration, error) { + return time.ParseDuration(s + "s") +} + +func ToLogLevelFromString(s string) (LogLevel, error) { + var m = map[string]LogLevel{ + "debug": slog.LevelDebug, + "info": slog.LevelInfo, + "warn": slog.LevelWarn, + "error": slog.LevelError, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue LogLevel + return zeroValue, fmt.Errorf("invalid log level '%s'", s) + } +} + +func ToDefaultBlockFromString(s string) (DefaultBlock, error) { + var m = map[string]DefaultBlock{ + "latest": model.DefaultBlockStatusLatest, + "pending": model.DefaultBlockStatusPending, + "safe": model.DefaultBlockStatusSafe, + "finalized": model.DefaultBlockStatusFinalized, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue DefaultBlock + return zeroValue, fmt.Errorf("invalid default block '%s'", s) + } +} + +func ToAuthKindFromString(s string) (AuthKind, error) { + var m = map[string]AuthKind{ + "private_key": AuthKindPrivateKeyVar, + "private_key_file": AuthKindPrivateKeyFile, + "mnemonic": AuthKindMnemonicVar, + "mnemonic_file": AuthKindMnemonicFile, + "aws": AuthKindAWS, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue AuthKind + return zeroValue, fmt.Errorf("invalid auth kind '%s'", s) + } +} + +// Aliases to be used by the generated functions. +var ( + toBool = strconv.ParseBool + toInt = strconv.Atoi + toInt64 = ToInt64FromString + toUint64 = ToUint64FromString + toString = ToStringFromString + toDuration = ToDurationFromSeconds + toLogLevel = ToLogLevelFromString + toAuthKind = ToAuthKindFromString + toDefaultBlock = ToDefaultBlockFromString +) + +// ------------------------------------------------------------------------------------------------ +// Getters +// ------------------------------------------------------------------------------------------------ + +func GetAuthAwsKmsKeyId() string { + s, ok := os.LookupEnv("CARTESI_AUTH_AWS_KMS_KEY_ID") + if !ok { + panic("missing env var CARTESI_AUTH_AWS_KMS_KEY_ID") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_AWS_KMS_KEY_ID: %v", err)) + } + return val +} + +func GetAuthAwsKmsRegion() string { + s, ok := os.LookupEnv("CARTESI_AUTH_AWS_KMS_REGION") + if !ok { + panic("missing env var CARTESI_AUTH_AWS_KMS_REGION") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_AWS_KMS_REGION: %v", err)) + } + return val +} + +func GetAuthKind() AuthKind { + s, ok := os.LookupEnv("CARTESI_AUTH_KIND") + if !ok { + s = "mnemonic" + } + val, err := toAuthKind(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_KIND: %v", err)) + } + return val +} + +func GetAuthMnemonic() string { + s, ok := os.LookupEnv("CARTESI_AUTH_MNEMONIC") + if !ok { + panic("missing env var CARTESI_AUTH_MNEMONIC") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_MNEMONIC: %v", err)) + } + return val +} + +func GetAuthMnemonicAccountIndex() int { + s, ok := os.LookupEnv("CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX") + if !ok { + s = "0" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX: %v", err)) + } + return val +} + +func GetAuthMnemonicFile() string { + s, ok := os.LookupEnv("CARTESI_AUTH_MNEMONIC_FILE") + if !ok { + panic("missing env var CARTESI_AUTH_MNEMONIC_FILE") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_MNEMONIC_FILE: %v", err)) + } + return val +} + +func GetAuthPrivateKey() string { + s, ok := os.LookupEnv("CARTESI_AUTH_PRIVATE_KEY") + if !ok { + panic("missing env var CARTESI_AUTH_PRIVATE_KEY") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_PRIVATE_KEY: %v", err)) + } + return val +} + +func GetAuthPrivateKeyFile() string { + s, ok := os.LookupEnv("CARTESI_AUTH_PRIVATE_KEY_FILE") + if !ok { + panic("missing env var CARTESI_AUTH_PRIVATE_KEY_FILE") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_PRIVATE_KEY_FILE: %v", err)) + } + return val +} + +func GetBlockchainBlockTimeout() int { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT") + if !ok { + s = "60" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT: %v", err)) + } + return val +} + +func GetBlockchainHttpEndpoint() string { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_HTTP_ENDPOINT") + if !ok { + panic("missing env var CARTESI_BLOCKCHAIN_HTTP_ENDPOINT") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_HTTP_ENDPOINT: %v", err)) + } + return val +} + +func GetBlockchainId() uint64 { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_ID") + if !ok { + panic("missing env var CARTESI_BLOCKCHAIN_ID") + } + val, err := toUint64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_ID: %v", err)) + } + return val +} + +func GetBlockchainWsEndpoint() string { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_WS_ENDPOINT") + if !ok { + panic("missing env var CARTESI_BLOCKCHAIN_WS_ENDPOINT") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_WS_ENDPOINT: %v", err)) + } + return val +} + +func GetEvmReaderDefaultBlock() DefaultBlock { + s, ok := os.LookupEnv("CARTESI_EVM_READER_DEFAULT_BLOCK") + if !ok { + s = "finalized" + } + val, err := toDefaultBlock(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EVM_READER_DEFAULT_BLOCK: %v", err)) + } + return val +} + +func GetLegacyBlockchainEnabled() bool { + s, ok := os.LookupEnv("CARTESI_LEGACY_BLOCKCHAIN_ENABLED") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_LEGACY_BLOCKCHAIN_ENABLED: %v", err)) + } + return val +} + +func GetContractsInputBoxAddress() string { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_INPUT_BOX_ADDRESS") + if !ok { + panic("missing env var CARTESI_CONTRACTS_INPUT_BOX_ADDRESS") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: %v", err)) + } + return val +} + +func GetContractsInputBoxDeploymentBlockNumber() int64 { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER") + if !ok { + panic("missing env var CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER") + } + val, err := toInt64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER: %v", err)) + } + return val +} + +func GetBaseUrl() string { + s, ok := os.LookupEnv("ESPRESSO_BASE_URL") + if !ok { + s = "" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse ESPRESSO_BASE_URL: %v", err)) + } + return val +} + +func GetNamespace() uint64 { + s, ok := os.LookupEnv("ESPRESSO_NAMESPACE") + if !ok { + s = "0" + } + val, err := toUint64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse ESPRESSO_NAMESPACE: %v", err)) + } + return val +} + +func GetServiceEndpoint() string { + s, ok := os.LookupEnv("ESPRESSO_SERVICE_ENDPOINT") + if !ok { + s = "localhost:8080" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse ESPRESSO_SERVICE_ENDPOINT: %v", err)) + } + return val +} + +func GetStartingBlock() uint64 { + s, ok := os.LookupEnv("ESPRESSO_STARTING_BLOCK") + if !ok { + s = "0" + } + val, err := toUint64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse ESPRESSO_STARTING_BLOCK: %v", err)) + } + return val +} + +func GetFeatureClaimSubmissionEnabled() bool { + s, ok := os.LookupEnv("CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED") + if !ok { + s = "true" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED: %v", err)) + } + return val +} + +func GetFeatureMachineHashCheckEnabled() bool { + s, ok := os.LookupEnv("CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED") + if !ok { + s = "true" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED: %v", err)) + } + return val +} + +func GetHttpAddress() string { + s, ok := os.LookupEnv("CARTESI_HTTP_ADDRESS") + if !ok { + s = "127.0.0.1" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_HTTP_ADDRESS: %v", err)) + } + return val +} + +func GetHttpPort() int { + s, ok := os.LookupEnv("CARTESI_HTTP_PORT") + if !ok { + s = "10000" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_HTTP_PORT: %v", err)) + } + return val +} + +func GetLogLevel() LogLevel { + s, ok := os.LookupEnv("CARTESI_LOG_LEVEL") + if !ok { + s = "info" + } + val, err := toLogLevel(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_LOG_LEVEL: %v", err)) + } + return val +} + +func GetLogPrettyEnabled() bool { + s, ok := os.LookupEnv("CARTESI_LOG_PRETTY_ENABLED") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_LOG_PRETTY_ENABLED: %v", err)) + } + return val +} + +func GetPostgresEndpoint() string { + s, ok := os.LookupEnv("CARTESI_POSTGRES_ENDPOINT") + if !ok { + s = "" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_POSTGRES_ENDPOINT: %v", err)) + } + return val +} + +func GetAdvancerPollingInterval() Duration { + s, ok := os.LookupEnv("CARTESI_ADVANCER_POLLING_INTERVAL") + if !ok { + s = "7" + } + val, err := toDuration(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_ADVANCER_POLLING_INTERVAL: %v", err)) + } + return val +} + +func GetClaimerPollingInterval() Duration { + s, ok := os.LookupEnv("CARTESI_CLAIMER_POLLING_INTERVAL") + if !ok { + s = "7" + } + val, err := toDuration(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CLAIMER_POLLING_INTERVAL: %v", err)) + } + return val +} + +func GetEvmReaderRetryPolicyMaxDelay() Duration { + s, ok := os.LookupEnv("CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY") + if !ok { + s = "3" + } + val, err := toDuration(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY: %v", err)) + } + return val +} + +func GetEvmReaderRetryPolicyMaxRetries() uint64 { + s, ok := os.LookupEnv("CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES") + if !ok { + s = "3" + } + val, err := toUint64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES: %v", err)) + } + return val +} + +func GetValidatorPollingInterval() Duration { + s, ok := os.LookupEnv("CARTESI_VALIDATOR_POLLING_INTERVAL") + if !ok { + s = "7" + } + val, err := toDuration(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_VALIDATOR_POLLING_INTERVAL: %v", err)) + } + return val +} + +func GetSequencer() string { + s, ok := os.LookupEnv("MAIN_SEQUENCER") + if !ok { + s = "espresso" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse MAIN_SEQUENCER: %v", err)) + } + return val +} + +func GetSnapshotDir() string { + s, ok := os.LookupEnv("CARTESI_SNAPSHOT_DIR") + if !ok { + s = "/var/lib/cartesi-rollups-node/snapshots" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_SNAPSHOT_DIR: %v", err)) + } + return val +} + +func GetMachineServerVerbosity() string { + s, ok := os.LookupEnv("CARTESI_MACHINE_SERVER_VERBOSITY") + if !ok { + s = "info" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_MACHINE_SERVER_VERBOSITY: %v", err)) + } + return val +} diff --git a/internal/espressoreader/espresso_db.go b/internal/espressoreader/espresso_db.go new file mode 100644 index 00000000..410cc296 --- /dev/null +++ b/internal/espressoreader/espresso_db.go @@ -0,0 +1,130 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package espressoreader + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/ZzzzHui/espresso-reader/internal/repository" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v5" +) + +func SetupEspressoDB( + ctx context.Context, + database *repository.Database) error { + query := `CREATE TABLE IF NOT EXISTS "espresso_nonce" +( + "sender_address" BYTEA NOT NULL, + "application_address" BYTEA NOT NULL, + "nonce" BIGINT NOT NULL, + UNIQUE("sender_address", "application_address") +);` + _, err := database.GetDB().Exec(ctx, query) + if err != nil { + slog.Error("failed to create table espresso_nonce") + return err + } + + query = `CREATE TABLE IF NOT EXISTS "espresso_block" +( + "application_address" BYTEA PRIMARY KEY, + "last_processed_espresso_block" NUMERIC(20,0) NOT NULL CHECK ("last_processed_espresso_block" >= 0 AND "last_processed_espresso_block" <= f_maxuint64()) +);` + _, err = database.GetDB().Exec(ctx, query) + if err != nil { + slog.Error("failed to create table espresso_block") + return err + } + + query = `CREATE TABLE IF NOT EXISTS "input_index" +( + "application_address" BYTEA PRIMARY KEY, + "index" BIGINT NOT NULL +);` + _, err = database.GetDB().Exec(ctx, query) + if err != nil { + slog.Error("failed to create table input_index") + return err + } + + query = `ALTER TABLE input + ADD COLUMN IF NOT EXISTS transaction_id BYTEA;` + _, err = database.GetDB().Exec(ctx, query) + if err != nil { + slog.Error("failed to add column transaction_id to table input") + return err + } + + return nil +} + +func GetLastProcessedEspressoBlock( + ctx context.Context, + database *repository.Database, + application_address common.Address) (uint64, error) { + + var lastProcessedEspressoBlock uint64 + + query := ` + SELECT + last_processed_espresso_block + FROM + espresso_block + WHERE + application_address=@application_address` + + args := pgx.NamedArgs{ + "application_address": application_address, + } + + err := database.GetDB().QueryRow(ctx, query, args).Scan( + &lastProcessedEspressoBlock, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetLastProcessedEspressoBlock returned no rows", + "app", lastProcessedEspressoBlock) + return 0, nil + } + return 0, fmt.Errorf("GetLastProcessedEspressoBlock QueryRow failed: %w\n", err) + } + + return lastProcessedEspressoBlock, nil +} + +func UpdateLastProcessedEspressoBlock( + ctx context.Context, + database *repository.Database, + application_address common.Address, + last_processed_espresso_block uint64, +) error { + + query := ` + INSERT INTO espresso_block + (application_address, + last_processed_espresso_block) + VALUES + (@application_address, + @last_processed_espresso_block) + ON CONFLICT (application_address) + DO UPDATE + set last_processed_espresso_block=@last_processed_espresso_block + ` + + args := pgx.NamedArgs{ + "application_address": application_address, + "last_processed_espresso_block": last_processed_espresso_block, + } + _, err := database.GetDB().Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("failed to update last_processed_espresso_block: %w", err) + } + + return nil +} diff --git a/internal/espressoreader/espresso_reader.go b/internal/espressoreader/espresso_reader.go new file mode 100644 index 00000000..cc1bd56b --- /dev/null +++ b/internal/espressoreader/espresso_reader.go @@ -0,0 +1,494 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package espressoreader + +import ( + "context" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log/slog" + "math/big" + "net/http" + "slices" + "strconv" + "strings" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/internal/repository" + + "github.com/EspressoSystems/espresso-sequencer-go/client" + "github.com/ethereum/go-ethereum/common" + "github.com/tidwall/gjson" +) + +type EspressoReader struct { + url string + client client.Client + startingBlock uint64 + namespace uint64 + repository *repository.Database + evmReader *evmreader.EvmReader + chainId uint64 + inputBoxDeploymentBlock uint64 +} + +func NewEspressoReader(url string, startingBlock uint64, namespace uint64, repository *repository.Database, evmReader *evmreader.EvmReader, chainId uint64, inputBoxDeploymentBlock uint64) EspressoReader { + client := client.NewClient(url) + return EspressoReader{url: url, client: *client, startingBlock: startingBlock, namespace: namespace, repository: repository, evmReader: evmReader, chainId: chainId, inputBoxDeploymentBlock: inputBoxDeploymentBlock} +} + +func (e *EspressoReader) Run(ctx context.Context, ready chan<- struct{}) error { + ready <- struct{}{} + + err := SetupEspressoDB(ctx, e.repository) + if err != nil { + slog.Error("failed to setup espresso db") + return err + } + + for { + select { + case <-ctx.Done(): + slog.Info("exiting espresso reader") + return ctx.Err() + default: + // fetch latest espresso block height + latestBlockHeight, err := e.client.FetchLatestBlockHeight(ctx) + if err != nil { + slog.Error("failed fetching latest espresso block height", "error", err) + continue + } + slog.Debug("Espresso:", "latestBlockHeight", latestBlockHeight) + + apps := e.getAppsForEvmReader(ctx) + if len(apps) > 0 { + for _, app := range apps { + lastProcessedEspressoBlock, err := GetLastProcessedEspressoBlock(ctx, e.repository, app.Application.ContractAddress) + if err != nil { + slog.Error("failed reading lastProcessedEspressoBlock", "error", err) + continue + } + lastProcessedL1Block := app.Application.LastProcessedBlock + appAddress := app.Application.ContractAddress + if lastProcessedL1Block < e.inputBoxDeploymentBlock { + lastProcessedL1Block = e.inputBoxDeploymentBlock - 1 + } + // bootstrap if there are more than 100 blocks to catch up + if latestBlockHeight-lastProcessedEspressoBlock > 100 { + if lastProcessedEspressoBlock == 0 { + if e.startingBlock != 0 { + lastProcessedEspressoBlock = e.startingBlock - 1 + } else { + lastProcessedEspressoBlock = latestBlockHeight - 1 + } + } + // bootstrap + slog.Debug("bootstrapping:", "app", appAddress, "from-block", lastProcessedEspressoBlock+1, "to-block", latestBlockHeight) + err = e.bootstrap(ctx, app, lastProcessedEspressoBlock, latestBlockHeight, lastProcessedL1Block) + if err != nil { + slog.Error("failed reading inputs", "error", err) + continue + } + + // update lastProcessedEspressoBlock in db + err = UpdateLastProcessedEspressoBlock(ctx, e.repository, app.Application.ContractAddress, latestBlockHeight) + if err != nil { + slog.Error("failed updating last processed espresso block", "error", err) + } + } else { + // in sync. Process espresso blocks one-by-one + currentBlockHeight := lastProcessedEspressoBlock + 1 + for ; currentBlockHeight <= latestBlockHeight; currentBlockHeight++ { + slog.Debug("Espresso:", "app", appAddress, "currentBlockHeight", currentBlockHeight) + //** read base layer **// + var l1FinalizedTimestamp uint64 + lastProcessedL1Block, l1FinalizedTimestamp = e.readL1(ctx, app, currentBlockHeight, lastProcessedL1Block) + //** read espresso **// + e.readEspresso(ctx, app, currentBlockHeight, lastProcessedL1Block, l1FinalizedTimestamp) + + // update lastProcessedEspressoBlock in db + err = UpdateLastProcessedEspressoBlock(ctx, e.repository, app.Application.ContractAddress, latestBlockHeight) + if err != nil { + slog.Error("failed updating last processed espresso block", "error", err) + } + } + } + + } + } + + // take a break :) + var delay time.Duration = 1000 + time.Sleep(delay * time.Millisecond) + } + } +} + +func (e *EspressoReader) bootstrap(ctx context.Context, app evmreader.TypeExportApplication, lastProcessedEspressoBlock uint64, latestBlockHeight uint64, l1FinalizedHeight uint64) error { + var l1FinalizedTimestamp uint64 + var nsTables []string + batchStartingBlock := lastProcessedEspressoBlock + 1 + batchLimit := uint64(100) + for latestBlockHeight >= batchStartingBlock { + select { + case <-ctx.Done(): + slog.Info("exiting espresso reader") + return ctx.Err() + default: + var batchEndingBlock uint64 + if batchStartingBlock+batchLimit > latestBlockHeight+1 { + batchEndingBlock = latestBlockHeight + 1 + } else { + batchEndingBlock = batchStartingBlock + batchLimit + } + nsTable, err := e.getNSTableByRange(ctx, batchStartingBlock, batchEndingBlock) + if err != nil { + return err + } + nsTableBytes := []byte(nsTable) + err = json.Unmarshal(nsTableBytes, &nsTables) + if err != nil { + slog.Error("failed fetching ns tables", "error", err, "ns table", nsTables) + } else { + for index, nsTable := range nsTables { + nsTableBytes, _ := base64.StdEncoding.DecodeString(nsTable) + ns := e.extractNS(nsTableBytes) + if slices.Contains(ns, uint32(e.namespace)) { + currentEspressoBlock := batchStartingBlock + uint64(index) + slog.Debug("found namespace contained in", "block", currentEspressoBlock) + l1FinalizedHeight, l1FinalizedTimestamp = e.readL1(ctx, app, currentEspressoBlock, l1FinalizedHeight) + e.readEspresso(ctx, app, currentEspressoBlock, l1FinalizedHeight, l1FinalizedTimestamp) + } + } + } + // update loop var + batchStartingBlock += batchLimit + } + } + return nil +} + +func (e *EspressoReader) readL1(ctx context.Context, app evmreader.TypeExportApplication, currentBlockHeight uint64, lastProcessedL1Block uint64) (uint64, uint64) { + l1FinalizedLatestHeight, l1FinalizedTimestamp := e.getL1FinalizedHeight(ctx, currentBlockHeight) + // read L1 if there might be update + if l1FinalizedLatestHeight > lastProcessedL1Block { + slog.Debug("L1 finalized", "app", app.Application.ContractAddress, "from", lastProcessedL1Block, "to", l1FinalizedLatestHeight) + + var apps []evmreader.TypeExportApplication + apps = append(apps, app) // make app into 1-element array + + // start reading from the block after the prev height + e.evmReader.ReadAndStoreInputs(ctx, lastProcessedL1Block+1, l1FinalizedLatestHeight, apps) + // check for claim status and output execution + e.evmReader.CheckForClaimStatus(ctx, apps, l1FinalizedLatestHeight) + e.evmReader.CheckForOutputExecution(ctx, apps, l1FinalizedLatestHeight) + } + return l1FinalizedLatestHeight, l1FinalizedTimestamp +} + +func (e *EspressoReader) readEspresso(ctx context.Context, appEvmType evmreader.TypeExportApplication, currentBlockHeight uint64, l1FinalizedLatestHeight uint64, l1FinalizedTimestamp uint64) { + app := appEvmType.Application.ContractAddress + transactions, err := e.client.FetchTransactionsInBlock(ctx, currentBlockHeight, e.namespace) + if err != nil { + slog.Error("failed fetching espresso tx", "error", err) + return + } + + numTx := len(transactions.Transactions) + + for i := 0; i < numTx; i++ { + transaction := transactions.Transactions[i] + + msgSender, typedData, sigHash, err := ExtractSigAndData(string(transaction)) + if err != nil { + slog.Error("failed to extract espresso tx", "error", err) + continue + } + + nonce := uint64(typedData.Message["nonce"].(float64)) + payload := typedData.Message["data"].(string) + appAddressStr := typedData.Message["app"].(string) + appAddress := common.HexToAddress(appAddressStr) + if appAddress != app { + slog.Debug("skipping tx that doesn't belong to", "app", app) + continue + } + slog.Info("Espresso input", "msgSender", msgSender, "nonce", nonce, "payload", payload, "appAddrss", appAddress, "tx-id", sigHash) + + // validate nonce + nonceInDb, err := e.repository.GetEspressoNonce(ctx, msgSender, appAddress) + if err != nil { + slog.Error("failed to get espresso nonce from db", "error", err) + continue + } + if nonce != nonceInDb { + slog.Error("Espresso nonce is incorrect. May be a duplicate tx", "nonce from espresso", nonce, "nonce in db", nonceInDb) + continue + } + + payloadBytes := []byte(payload) + if strings.HasPrefix(payload, "0x") { + payload = payload[2:] // remove 0x + payloadBytes, err = hex.DecodeString(payload) + if err != nil { + slog.Error("failed to decode hex string", "error", err) + continue + } + } + // abi encode payload + abiObject := e.evmReader.IOAbi + chainId := &big.Int{} + chainId.SetInt64(int64(e.chainId)) + l1FinalizedLatestHeightBig := &big.Int{} + l1FinalizedLatestHeightBig.SetUint64(l1FinalizedLatestHeight) + l1FinalizedTimestampBig := &big.Int{} + l1FinalizedTimestampBig.SetUint64(l1FinalizedTimestamp) + prevRandao, err := readPrevRandao(ctx, l1FinalizedLatestHeight, e.evmReader.GetEthClient()) + if err != nil { + slog.Error("failed to read prevrandao", "error", err) + } + index := &big.Int{} + indexUint64, err := e.repository.GetInputIndex(ctx, appAddress) + if err != nil { + slog.Error("failed to read index", "app", appAddress, "error", err) + } + index.SetUint64(indexUint64) + payloadAbi, err := abiObject.Pack("EvmAdvance", chainId, appAddress, msgSender, l1FinalizedLatestHeightBig, l1FinalizedTimestampBig, prevRandao, index, payloadBytes) + if err != nil { + slog.Error("failed to abi encode", "error", err) + continue + } + + // build epochInputMap + // Initialize epochs inputs map + var epochInputMap = make(map[*model.Epoch][]model.Input) + // get epoch length and last open epoch + epochLength := e.evmReader.GetEpochLengthCache(appAddress) + if epochLength == 0 { + err = e.evmReader.AddAppEpochLengthIntoCache(appEvmType) + epochLength = e.evmReader.GetEpochLengthCache(appAddress) + if err != nil || epochLength == 0 { + slog.Error("could not obtain epoch length") + continue + } + } + currentEpoch, err := e.repository.GetEpoch(ctx, + epochLength, appAddress) + if err != nil { + slog.Error("could not obtain current epoch", "err", err) + continue + } + // if currect epoch is not nil, assume the epoch is open + // espresso inputs do not close epoch + epochIndex := evmreader.CalculateEpochIndex(epochLength, l1FinalizedLatestHeight) + if currentEpoch == nil { + currentEpoch = &model.Epoch{ + Index: epochIndex, + FirstBlock: epochIndex * epochLength, + LastBlock: (epochIndex * epochLength) + epochLength - 1, + Status: model.EpochStatusOpen, + AppAddress: appAddress, + } + } + // build input + sigHashHexBytes, err := hex.DecodeString(sigHash[2:]) + if err != nil { + slog.Error("could not obtain bytes for tx-id", "err", err) + continue + } + input := model.Input{ + Index: indexUint64, + CompletionStatus: model.InputStatusNone, + RawData: payloadAbi, + BlockNumber: l1FinalizedLatestHeight, + AppAddress: appAddress, + TransactionId: sigHashHexBytes, + } + currentInputs, ok := epochInputMap[currentEpoch] + if !ok { + currentInputs = []model.Input{} + } + epochInputMap[currentEpoch] = append(currentInputs, input) + + // Store everything + // future optimization: bundle tx by address to fully utilize `epochInputMap`` + if len(epochInputMap) > 0 { + _, _, err = e.repository.StoreEpochAndInputsTransaction( + ctx, + epochInputMap, + l1FinalizedLatestHeight, + appAddress, + ) + if err != nil { + slog.Error("could not store Espresso input", "err", err) + continue + } + } + + // update nonce + err = e.repository.UpdateEspressoNonce(ctx, msgSender, appAddress) + if err != nil { + slog.Error("!!!could not update Espresso nonce!!!", "err", err) + continue + } + // update input index + err = e.repository.UpdateInputIndex(ctx, appAddress) + if err != nil { + slog.Error("failed to update index", "app", appAddress, "error", err) + } + } +} + +func (e *EspressoReader) readEspressoHeader(espressoBlockHeight uint64) string { + requestURL := fmt.Sprintf("%s/availability/header/%d", e.url, espressoBlockHeight) + res, err := http.Get(requestURL) + if err != nil { + slog.Error("error making http request", "err", err) + } + resBody, err := io.ReadAll(res.Body) + if err != nil { + slog.Error("could not read response body", "err", err) + } + + return string(resBody) +} + +func (e *EspressoReader) getL1FinalizedHeight(ctx context.Context, espressoBlockHeight uint64) (uint64, uint64) { + for { + select { + case <-ctx.Done(): + slog.Info("exiting espresso reader") + return 0, 0 + default: + espressoHeader := e.readEspressoHeader(espressoBlockHeight) + if len(espressoHeader) == 0 { + slog.Error("error fetching espresso header", "at height", espressoBlockHeight, "header", espressoHeader) + slog.Error("retrying fetching header") + continue + } + + l1FinalizedNumber := gjson.Get(espressoHeader, "fields.l1_finalized.number").Uint() + l1FinalizedTimestampStr := gjson.Get(espressoHeader, "fields.l1_finalized.timestamp").Str + if len(l1FinalizedTimestampStr) < 2 { + slog.Debug("Espresso header not ready. Retry fetching", "height", espressoBlockHeight) + var delay time.Duration = 3000 + time.Sleep(delay * time.Millisecond) + continue + } + l1FinalizedTimestampInt, err := strconv.ParseInt(l1FinalizedTimestampStr[2:], 16, 64) + if err != nil { + slog.Error("hex to int conversion failed", "err", err) + slog.Error("retrying") + continue + } + l1FinalizedTimestamp := uint64(l1FinalizedTimestampInt) + return l1FinalizedNumber, l1FinalizedTimestamp + } + } +} + +func (e *EspressoReader) readEspressoHeadersByRange(ctx context.Context, from uint64, until uint64) string { + for { + select { + case <-ctx.Done(): + slog.Info("exiting espresso reader") + return "" + default: + requestURL := fmt.Sprintf("%s/availability/header/%d/%d", e.url, from, until) + res, err := http.Get(requestURL) + if err != nil { + slog.Error("error making http request", "err", err) + slog.Error("retrying") + continue + } + resBody, err := io.ReadAll(res.Body) + if err != nil { + slog.Error("could not read response body", "err", err) + slog.Error("retrying") + continue + } + + return string(resBody) + } + } +} + +func (e *EspressoReader) getNSTableByRange(ctx context.Context, from uint64, until uint64) (string, error) { + var nsTables string + for len(nsTables) == 0 { + select { + case <-ctx.Done(): + slog.Info("exiting espresso reader") + return "", ctx.Err() + default: + espressoHeaders := e.readEspressoHeadersByRange(ctx, from, until) + nsTables = gjson.Get(espressoHeaders, "#.fields.ns_table.bytes").Raw + if len(nsTables) == 0 { + slog.Debug("ns table is empty in current block range. Retry fetching") + var delay time.Duration = 2000 + time.Sleep(delay * time.Millisecond) + } + } + } + + return nsTables, nil +} + +func (e *EspressoReader) extractNS(nsTable []byte) []uint32 { + var nsArray []uint32 + numNS := binary.LittleEndian.Uint32(nsTable[0:]) + for i := range numNS { + nextNS := binary.LittleEndian.Uint32(nsTable[(4 + 8*i):]) + nsArray = append(nsArray, nextNS) + } + return nsArray +} + +//////// evm reader related //////// + +func (e *EspressoReader) getAppsForEvmReader(ctx context.Context) []evmreader.TypeExportApplication { + // Get All Applications + runningApps, err := e.repository.GetAllRunningApplications(ctx) + if err != nil { + slog.Error("Error retrieving running applications", + "error", + err, + ) + } + + // Build Contracts + var apps []evmreader.TypeExportApplication + for _, app := range runningApps { + applicationContract, consensusContract, err := e.evmReader.GetAppContracts(app) + if err != nil { + slog.Error("Error retrieving application contracts", "app", app, "error", err) + continue + } + apps = append(apps, evmreader.TypeExportApplication{Application: app, + ApplicationContract: applicationContract, + ConsensusContract: consensusContract}) + } + + if len(apps) == 0 { + slog.Info("No correctly configured applications running") + } + + return apps +} + +func readPrevRandao(ctx context.Context, l1FinalizedLatestHeight uint64, client *evmreader.EthClient) (*big.Int, error) { + header, err := (*client).HeaderByNumber(ctx, big.NewInt(int64(l1FinalizedLatestHeight))) + if err != nil { + return &big.Int{}, fmt.Errorf("espresso read block header error: %w", err) + } + prevRandao := header.MixDigest.Big() + slog.Debug("readPrevRandao", "prevRandao", prevRandao, "blockNumber", l1FinalizedLatestHeight) + return prevRandao, nil +} diff --git a/internal/espressoreader/extract.go b/internal/espressoreader/extract.go new file mode 100644 index 00000000..d4aa79b6 --- /dev/null +++ b/internal/espressoreader/extract.go @@ -0,0 +1,59 @@ +package espressoreader + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +type SigAndData struct { + TypedData apitypes.TypedData `json:"typedData"` + Account string `json:"account"` + Signature string `json:"signature"` +} + +func ExtractSigAndData(raw string) (common.Address, apitypes.TypedData, string, error) { + var sigAndData SigAndData + decodedRaw, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return common.HexToAddress("0x"), apitypes.TypedData{}, "", fmt.Errorf("decode base64: %w", err) + } + + if err := json.Unmarshal(decodedRaw, &sigAndData); err != nil { + return common.HexToAddress("0x"), apitypes.TypedData{}, "", fmt.Errorf("unmarshal sigAndData: %w", err) + } + + signature, err := hexutil.Decode(sigAndData.Signature) + if err != nil { + return common.HexToAddress("0x"), apitypes.TypedData{}, "", fmt.Errorf("decode signature: %w", err) + } + sigHash := crypto.Keccak256Hash(signature).String() + + typedData := sigAndData.TypedData + dataHash, _, err := apitypes.TypedDataAndHash(typedData) + if err != nil { + return common.HexToAddress("0x"), apitypes.TypedData{}, "", fmt.Errorf("typed data hash: %w", err) + } + + // update the recovery id + // https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442 + signature[64] -= 27 + + // get the pubkey used to sign this signature + sigPubkey, err := crypto.Ecrecover(dataHash, signature) + if err != nil { + return common.HexToAddress("0x"), apitypes.TypedData{}, "", fmt.Errorf("ecrecover: %w", err) + } + pubkey, err := crypto.UnmarshalPubkey(sigPubkey) + if err != nil { + return common.HexToAddress("0x"), apitypes.TypedData{}, "", fmt.Errorf("unmarshal: %w", err) + } + address := crypto.PubkeyToAddress(*pubkey) + + return address, typedData, sigHash, nil +} diff --git a/internal/espressoreader/service/espressoreader_service.go b/internal/espressoreader/service/espressoreader_service.go new file mode 100644 index 00000000..7afa564b --- /dev/null +++ b/internal/espressoreader/service/espressoreader_service.go @@ -0,0 +1,275 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package service + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/espressoreader" + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/evmreader/retrypolicy" + "github.com/ZzzzHui/espresso-reader/internal/repository" + + "github.com/EspressoSystems/espresso-sequencer-go/client" + "github.com/EspressoSystems/espresso-sequencer-go/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// app address => sender address => nonce +var nonceCache map[common.Address]map[common.Address]uint64 + +// Service to manage InputReader lifecycle +type EspressoReaderService struct { + blockchainHttpEndpoint string + blockchainWsEndpoint string + database *repository.Database + EspressoBaseUrl string + EspressoStartingBlock uint64 + EspressoNamespace uint64 + maxRetries uint64 + maxDelay time.Duration + chainId uint64 + inputBoxDeploymentBlock uint64 + espressoServiceEndpoint string +} + +func NewEspressoReaderService( + blockchainHttpEndpoint string, + blockchainWsEndpoint string, + database *repository.Database, + EspressoBaseUrl string, + EspressoStartingBlock uint64, + EspressoNamespace uint64, + maxRetries uint64, + maxDelay time.Duration, + chainId uint64, + inputBoxDeploymentBlock uint64, + espressoServiceEndpoint string, +) *EspressoReaderService { + return &EspressoReaderService{ + blockchainHttpEndpoint: blockchainHttpEndpoint, + blockchainWsEndpoint: blockchainWsEndpoint, + database: database, + EspressoBaseUrl: EspressoBaseUrl, + EspressoStartingBlock: EspressoStartingBlock, + EspressoNamespace: EspressoNamespace, + maxRetries: maxRetries, + maxDelay: maxDelay, + chainId: chainId, + inputBoxDeploymentBlock: inputBoxDeploymentBlock, + espressoServiceEndpoint: espressoServiceEndpoint, + } +} + +func (s *EspressoReaderService) Start( + ctx context.Context, + ready chan<- struct{}, +) error { + + evmReader := s.setupEvmReader(ctx, s.database) + + espressoReader := espressoreader.NewEspressoReader(s.EspressoBaseUrl, s.EspressoStartingBlock, s.EspressoNamespace, s.database, evmReader, s.chainId, s.inputBoxDeploymentBlock) + + go s.setupNonceHttpServer() + + return espressoReader.Run(ctx, ready) +} + +func (s *EspressoReaderService) String() string { + return "espressoreader" +} + +func (s *EspressoReaderService) setupEvmReader(ctx context.Context, database *repository.Database) *evmreader.EvmReader { + client, err := ethclient.DialContext(ctx, s.blockchainHttpEndpoint) + if err != nil { + slog.Error("eth client http", "error", err) + } + defer client.Close() + + wsClient, err := ethclient.DialContext(ctx, s.blockchainWsEndpoint) + if err != nil { + slog.Error("eth client ws", "error", err) + } + defer wsClient.Close() + + config, err := database.GetNodeConfig(ctx) + if err != nil { + slog.Error("db config", "error", err) + } + + inputSource, err := evmreader.NewInputSourceAdapter(config.InputBoxAddress, client) + if err != nil { + slog.Error("input source", "error", err) + } + + contractFactory := retrypolicy.NewEvmReaderContractFactory(client, s.maxRetries, s.maxDelay) + + evmReader := evmreader.NewEvmReader( + retrypolicy.NewEhtClientWithRetryPolicy(client, s.maxRetries, s.maxDelay), + retrypolicy.NewEthWsClientWithRetryPolicy(wsClient, s.maxRetries, s.maxDelay), + retrypolicy.NewInputSourceWithRetryPolicy(inputSource, s.maxRetries, s.maxDelay), + database, + config.InputBoxDeploymentBlock, + config.DefaultBlock, + contractFactory, + true, + ) + + return &evmReader +} + +func (s *EspressoReaderService) setupNonceHttpServer() { + nonceCache = make(map[common.Address]map[common.Address]uint64) + + http.HandleFunc("/nonce", s.requestNonce) + http.HandleFunc("/submit", s.submit) + + http.ListenAndServe(s.espressoServiceEndpoint, nil) +} + +type NonceRequest struct { + // AppContract App contract address + AppContract string `json:"app_contract"` + + // MsgSender Message sender address + MsgSender string `json:"msg_sender"` +} + +type NonceResponse struct { + Nonce uint64 `json:"nonce"` +} + +func (s *EspressoReaderService) requestNonce(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers") + if r.Method != http.MethodPost { + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + slog.Error("could not read body", "err", err) + } + nonceRequest := &NonceRequest{} + if err := json.Unmarshal(body, nonceRequest); err != nil { + fmt.Println(err) + } + + senderAddress := common.HexToAddress(nonceRequest.MsgSender) + applicationAddress := common.HexToAddress(nonceRequest.AppContract) + + var nonce uint64 + if nonceCache[applicationAddress] == nil { + nonceCache[applicationAddress] = make(map[common.Address]uint64) + } + if nonceCache[applicationAddress][senderAddress] == 0 { + ctx := r.Context() + nonce = s.queryNonceFromDb(ctx, senderAddress, applicationAddress) + nonceCache[applicationAddress][senderAddress] = nonce + } else { + nonce = nonceCache[applicationAddress][senderAddress] + } + + slog.Debug("got nonce request", "senderAddress", senderAddress, "applicationAddress", applicationAddress) + + nonceResponse := NonceResponse{Nonce: nonce} + if err != nil { + slog.Error("error json marshal nonce response", "err", err) + } + + err = json.NewEncoder(w).Encode(nonceResponse) + if err != nil { + slog.Info("Internal server error", + "service", "espresso nonce querier", + "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (s *EspressoReaderService) queryNonceFromDb( + ctx context.Context, + senderAddress common.Address, + applicationAddress common.Address) uint64 { + nonce, err := s.database.GetEspressoNonce(ctx, senderAddress, applicationAddress) + if err != nil { + slog.Error("failed to get espresso nonce", "error", err) + } + + return nonce +} + +type SubmitResponse struct { + Id string `json:"id,omitempty"` +} + +func (s *EspressoReaderService) submit(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers") + if r.Method != http.MethodPost { + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + slog.Error("could not read body", "err", err) + } + slog.Debug("got submit request", "request body", string(body)) + + client := client.NewClient(s.EspressoBaseUrl) + ctx := r.Context() + var tx types.Transaction + tx.Namespace = s.EspressoNamespace + tx.Payload = []byte(base64.StdEncoding.EncodeToString(body)) + _, err = client.SubmitTransaction(ctx, tx) + if err != nil { + slog.Error("espresso tx submit error", "err", err) + return + } + + msgSender, typedData, sigHash, err := espressoreader.ExtractSigAndData(string(tx.Payload)) + if err != nil { + slog.Error("transaction not correctly formatted", "error", err) + return + } + submitResponse := SubmitResponse{Id: sigHash} + + err = json.NewEncoder(w).Encode(submitResponse) + if err != nil { + slog.Info("Internal server error", + "service", "espresso submit endpoint", + "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // update nonce cache + appAddressStr := typedData.Message["app"].(string) + appAddress := common.HexToAddress(appAddressStr) + if nonceCache[appAddress] == nil { + slog.Error("Should query nonce before submit") + return + } + nonceInRequest := uint64(typedData.Message["nonce"].(float64)) + if nonceCache[appAddress][msgSender] == 0 { + ctx := r.Context() + nonceInDb := s.queryNonceFromDb(ctx, msgSender, appAddress) + if nonceInRequest != nonceInDb { + slog.Error("Nonce in request is incorrect") + return + } + nonceCache[appAddress][msgSender] = nonceInDb + 1 + } else { + nonceCache[appAddress][msgSender]++ + } +} diff --git a/internal/evmreader/abi.json b/internal/evmreader/abi.json new file mode 100644 index 00000000..41ff52ec --- /dev/null +++ b/internal/evmreader/abi.json @@ -0,0 +1,28 @@ +[{ + "type" : "function", + "name" : "EvmAdvance", + "inputs" : [ + { "type" : "uint256" }, + { "type" : "address" }, + { "type" : "address" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "bytes" } + ] +}, { + "type" : "function", + "name" : "Voucher", + "inputs" : [ + { "type" : "address" }, + { "type" : "uint256" }, + { "type" : "bytes" } + ] +}, { + "type" : "function", + "name" : "Notice", + "inputs" : [ + { "type" : "bytes" } + ] +}] diff --git a/internal/evmreader/application_adapter.go b/internal/evmreader/application_adapter.go new file mode 100644 index 00000000..d43387e9 --- /dev/null +++ b/internal/evmreader/application_adapter.go @@ -0,0 +1,55 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + appcontract "github.com/ZzzzHui/espresso-reader/pkg/contracts/iapplication" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// IConsensus Wrapper +type ApplicationContractAdapter struct { + application *appcontract.IApplication +} + +func NewApplicationContractAdapter( + appAddress common.Address, + client *ethclient.Client, +) (*ApplicationContractAdapter, error) { + applicationContract, err := appcontract.NewIApplication(appAddress, client) + if err != nil { + return nil, err + } + return &ApplicationContractAdapter{ + application: applicationContract, + }, nil +} + +func (a *ApplicationContractAdapter) GetConsensus(opts *bind.CallOpts) (common.Address, error) { + return a.application.GetConsensus(opts) +} + +func (a *ApplicationContractAdapter) RetrieveOutputExecutionEvents( + opts *bind.FilterOpts, +) ([]*appcontract.IApplicationOutputExecuted, error) { + + itr, err := a.application.FilterOutputExecuted(opts) + if err != nil { + return nil, err + } + defer itr.Close() + + var events []*appcontract.IApplicationOutputExecuted + for itr.Next() { + outputExecutedEvent := itr.Event + events = append(events, outputExecutedEvent) + } + if err = itr.Error(); err != nil { + return nil, err + } + return events, nil +} diff --git a/internal/evmreader/claim.go b/internal/evmreader/claim.go new file mode 100644 index 00000000..8475dfc8 --- /dev/null +++ b/internal/evmreader/claim.go @@ -0,0 +1,251 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "cmp" + "context" + "log/slog" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iconsensus" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +func (r *EvmReader) CheckForClaimStatus( + ctx context.Context, + apps []application, + mostRecentBlockNumber uint64, +) { + + slog.Debug("evmreader: Checking for new Claim Acceptance Events") + + // Classify them by lastClaimCheck block + appsIndexedByLastCheck := indexApps(keyByLastClaimCheck, apps) + + for lastClaimCheck, apps := range appsIndexedByLastCheck { + + appAddresses := appsToAddresses(apps) + + // Safeguard: Only check blocks starting from the block where the InputBox + // contract was deployed as Inputs can be added to that same block + if lastClaimCheck < r.inputBoxDeploymentBlock { + lastClaimCheck = r.inputBoxDeploymentBlock - 1 + } + + if mostRecentBlockNumber > lastClaimCheck { + + slog.Debug("evmreader: Checking claim acceptance for applications", + "apps", appAddresses, + "last claim check block", lastClaimCheck, + "most recent block", mostRecentBlockNumber) + + r.readAndUpdateClaims(ctx, apps, lastClaimCheck, mostRecentBlockNumber) + + } else if mostRecentBlockNumber < lastClaimCheck { + slog.Warn( + "evmreader: Not reading claim acceptance: most recent block is lower than the last processed one", //nolint:lll + "apps", appAddresses, + "last claim check block", lastClaimCheck, + "most recent block", mostRecentBlockNumber, + ) + } else { + slog.Warn("evmreader: Not reading claim acceptance: already checked the most recent blocks", + "apps", appAddresses, + "last claim check block", lastClaimCheck, + "most recent block", mostRecentBlockNumber, + ) + } + + } +} + +func (r *EvmReader) readAndUpdateClaims( + ctx context.Context, + apps []application, + lastClaimCheck, mostRecentBlockNumber uint64, +) { + + // DISCLAIMER: The current algorithm will only handle Authority. + // To handle Quorum, node needs to handle acceptance events + // that can happen before claim submission + + // DISCLAIMER 2: The current algorithm does not consider that there might + // be more than one claimAcceptance per block + + // Classify them by the same IConsensusAddress + sameConsensusApps := indexApps(keyByIConsensus, apps) + for iConsensusAddress, apps := range sameConsensusApps { + + appAddresses := appsToAddresses(apps) + + // All apps shares the same IConsensus + // If there is a key on indexApps, there is at least one + // application in the referred application slice + consensusContract := apps[0].ConsensusContract + + // Retrieve Claim Acceptance Events from blockchain + appClaimAcceptanceEventMap, err := r.readClaimsAcceptance( + ctx, consensusContract, appAddresses, lastClaimCheck+1, mostRecentBlockNumber) + if err != nil { + slog.Error("evmreader: Error reading claim acceptance status", + "apps", apps, + "IConsensus", iConsensusAddress, + "start", lastClaimCheck, + "end", mostRecentBlockNumber, + "error", err) + continue + } + + // Check events against Epochs + APP_LOOP: + for app, claimAcceptances := range appClaimAcceptanceEventMap { + + for _, claimAcceptance := range claimAcceptances { + + // Get Previous Epochs with submitted claims, If is there any, + // Application is in an invalid State. + previousEpochs, err := r.repository.GetPreviousEpochsWithOpenClaims( + ctx, app, claimAcceptance.LastProcessedBlockNumber.Uint64()) + if err != nil { + slog.Error("evmreader: Error retrieving previous submitted claims", + "app", app, + "block", claimAcceptance.LastProcessedBlockNumber.Uint64(), + "error", err) + continue APP_LOOP + } + if len(previousEpochs) > 0 { + slog.Error("evmreader: Application got 'not accepted' claims. It is in an invalid state", + "claim last block", claimAcceptance.LastProcessedBlockNumber, + "app", app) + continue APP_LOOP + } + + // Get the Epoch for the current Claim Acceptance Event + epoch, err := r.repository.GetEpoch( + ctx, CalculateEpochIndex( + r.epochLengthCache[app], + claimAcceptance.LastProcessedBlockNumber.Uint64()), + app) + if err != nil { + slog.Error("evmreader: Error retrieving Epoch", + "app", app, + "block", claimAcceptance.LastProcessedBlockNumber.Uint64(), + "error", err) + continue APP_LOOP + } + + // Check Epoch + if epoch == nil { + slog.Error( + "evmreader: Found claim acceptance event for an unknown epoch. Application is in an invalid state", //nolint:lll + "app", app, + "claim last block", claimAcceptance.LastProcessedBlockNumber, + "hash", claimAcceptance.Claim) + continue APP_LOOP + } + if epoch.ClaimHash == nil { + slog.Warn( + "evmreader: Found claim acceptance event, but claim hasn't been calculated yet", + "app", app, + "lastBlock", claimAcceptance.LastProcessedBlockNumber, + ) + continue APP_LOOP + } + if claimAcceptance.Claim != *epoch.ClaimHash || + claimAcceptance.LastProcessedBlockNumber.Uint64() != epoch.LastBlock { + slog.Error("evmreader: Accepted Claim does not match actual Claim. Application is in an invalid state", //nolint:lll + "app", app, + "lastBlock", epoch.LastBlock, + "hash", epoch.ClaimHash) + + continue APP_LOOP + } + if epoch.Status == EpochStatusClaimAccepted { + slog.Debug("evmreader: Claim already accepted. Skipping", + "app", app, + "block", claimAcceptance.LastProcessedBlockNumber.Uint64(), + "claimStatus", epoch.Status, + "hash", epoch.ClaimHash) + continue + } + if epoch.Status != EpochStatusClaimSubmitted { + // this happens when running on latest. EvmReader can see the event before + // the claim is marked as submitted by the claimer. + slog.Debug("evmreader: Claim status is not submitted. Skipping for now", + "app", app, + "block", claimAcceptance.LastProcessedBlockNumber.Uint64(), + "claimStatus", epoch.Status, + "hash", epoch.ClaimHash) + continue APP_LOOP + } + + // Update Epoch claim status + slog.Info("evmreader: Claim Accepted", + "app", app, + "lastBlock", epoch.LastBlock, + "hash", epoch.ClaimHash, + "epoch_id", epoch.Id, + "last_claim_check_block", claimAcceptance.Raw.BlockNumber) + + epoch.Status = EpochStatusClaimAccepted + // Store epoch + err = r.repository.UpdateEpochs( + ctx, app, []*Epoch{epoch}, claimAcceptance.Raw.BlockNumber) + if err != nil { + slog.Error("evmreader: Error storing claims", "app", app, "error", err) + continue + } + } + + } + } +} + +func (r *EvmReader) readClaimsAcceptance( + ctx context.Context, + consensusContract ConsensusContract, + appAddresses []common.Address, + startBlock, endBlock uint64, +) (map[common.Address][]*iconsensus.IConsensusClaimAcceptance, error) { + appClaimAcceptanceMap := make(map[common.Address][]*iconsensus.IConsensusClaimAcceptance) + for _, address := range appAddresses { + appClaimAcceptanceMap[address] = []*iconsensus.IConsensusClaimAcceptance{} + } + opts := &bind.FilterOpts{ + Context: ctx, + Start: startBlock, + End: &endBlock, + } + claimAcceptanceEvents, err := consensusContract.RetrieveClaimAcceptanceEvents( + opts, appAddresses) + if err != nil { + return nil, err + } + for _, event := range claimAcceptanceEvents { + appClaimAcceptanceMap[event.AppContract] = insertSorted( + sortByLastBlockNumber, appClaimAcceptanceMap[event.AppContract], event) + } + return appClaimAcceptanceMap, nil +} + +// keyByLastClaimCheck is a LastClaimCheck key extractor function intended +// to be used with `indexApps` function, see indexApps() +func keyByLastClaimCheck(app application) uint64 { + return app.LastClaimCheckBlock +} + +// keyByIConsensus is a IConsensus address key extractor function intended +// to be used with `indexApps` function, see indexApps() +func keyByIConsensus(app application) Address { + return app.IConsensusAddress +} + +// sortByLastBlockNumber is a ClaimAcceptance's by last block number sorting function. +// Intended to be used with insertSorted function, see insertSorted() +func sortByLastBlockNumber(a, b *iconsensus.IConsensusClaimAcceptance) int { + return cmp.Compare(a.LastProcessedBlockNumber.Uint64(), b.LastProcessedBlockNumber.Uint64()) +} diff --git a/internal/evmreader/claim_test.go b/internal/evmreader/claim_test.go new file mode 100644 index 00000000..171b3287 --- /dev/null +++ b/internal/evmreader/claim_test.go @@ -0,0 +1,731 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "context" + "fmt" + "math/big" + "time" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iconsensus" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" +) + +func (s *EvmReaderSuite) TestNoClaimsAcceptance() { + + wsClient := FakeWSEhtClient{} + + //New EVM Reader + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x10, + }}, nil).Once() + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x11, + }}, nil).Once() + + s.repository.Unset("UpdateEpochs") + s.repository.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + obj := arguments.Get(1) + claims, ok := obj.([]*Epoch) + s.Require().True(ok) + s.Require().Equal(0, len(claims)) + + obj = arguments.Get(2) + lastClaimCheck, ok := obj.(uint64) + s.Require().True(ok) + s.Require().Equal(uint64(17), lastClaimCheck) + + }).Return(nil) + s.repository.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + obj := arguments.Get(1) + claims, ok := obj.([]*Epoch) + s.Require().True(ok) + s.Require().Equal(0, len(claims)) + + obj = arguments.Get(2) + lastClaimCheck, ok := obj.(uint64) + s.Require().True(ok) + s.Require().Equal(uint64(18), lastClaimCheck) + + }).Return(nil) + + //No Inputs + s.inputBox.Unset("RetrieveInputs") + s.inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header1, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header2, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + wsClient.fireNewHead(&header1) + time.Sleep(1 * time.Second) + + s.repository.AssertNumberOfCalls( + s.T(), + "UpdateEpochs", + 0, + ) + +} + +func (s *EvmReaderSuite) TestReadClaimAcceptance() { + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + consensusContract := &MockIConsensusContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewIConsensus") + contractFactory.On("NewIConsensus", + mock.Anything, + ).Return(consensusContract, nil) + + //New EVM Reader + wsClient := FakeWSEhtClient{} + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Claims Acceptance Events + + claimEvent0 := &iconsensus.IConsensusClaimAcceptance{ + AppContract: appAddress, + LastProcessedBlockNumber: big.NewInt(3), + Claim: common.HexToHash("0xdeadbeef"), + } + + claimEvents := []*iconsensus.IConsensusClaimAcceptance{claimEvent0} + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return(claimEvents, nil).Once() + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return([]*iconsensus.IConsensusClaimAcceptance{}, nil) + + // Epoch Length + consensusContract.On("GetEpochLength", + mock.Anything, + ).Return(big.NewInt(1), nil).Once() + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x10, + }}, nil).Once() + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x11, + }}, nil).Once() + + claim1Hash := common.HexToHash("0xdeadbeef") + claim0 := &Epoch{ + Index: 3, + FirstBlock: 3, + LastBlock: 3, + AppAddress: appAddress, + Status: EpochStatusClaimSubmitted, + ClaimHash: &claim1Hash, + } + + s.repository.Unset("GetEpoch") + s.repository.On("GetEpoch", + mock.Anything, + mock.Anything, + mock.Anything).Return(claim0, nil) + + s.repository.Unset("GetPreviousEpochsWithOpenClaims") + s.repository.On("GetPreviousEpochsWithOpenClaims", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]*Epoch{}, nil) + + s.repository.Unset("UpdateEpochs") + s.repository.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + obj := arguments.Get(1) + claims, ok := obj.([]*Epoch) + s.Require().True(ok) + s.Require().Equal(1, len(claims)) + claim0 := claims[0] + s.Require().Equal(uint64(3), claim0.LastBlock) + s.Require().Equal(EpochStatusClaimAccepted, claim0.Status) + + }).Return(nil) + + //No Inputs + s.inputBox.Unset("RetrieveInputs") + s.inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(10 * time.Second) + + s.repository.AssertNumberOfCalls( + s.T(), + "UpdateEpochs", + 1, + ) + +} + +func (s *EvmReaderSuite) TestCheckClaimFails() { + s.Run("whenRetrievePreviousEpochsFails", func() { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + consensusContract := &MockIConsensusContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewIConsensus") + contractFactory.On("NewIConsensus", + mock.Anything, + ).Return(consensusContract, nil) + + //New EVM Reader + client := newMockEthClient() + wsClient := FakeWSEhtClient{} + inputBox := newMockInputBox() + repository := newMockRepository() + evmReader := NewEvmReader( + client, + &wsClient, + inputBox, + repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Claims Acceptance Events + + claimEvent0 := &iconsensus.IConsensusClaimAcceptance{ + AppContract: appAddress, + LastProcessedBlockNumber: big.NewInt(3), + Claim: common.HexToHash("0xdeadbeef"), + } + + claimEvents := []*iconsensus.IConsensusClaimAcceptance{claimEvent0} + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return(claimEvents, nil).Once() + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return([]*iconsensus.IConsensusClaimAcceptance{}, nil) + + // Epoch Length + consensusContract.On("GetEpochLength", + mock.Anything, + ).Return(big.NewInt(1), nil).Once() + + // Prepare repository + repository.Unset("GetAllRunningApplications") + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x10, + }}, nil).Once() + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x11, + }}, nil).Once() + + claim1Hash := common.HexToHash("0xdeadbeef") + claim1 := &Epoch{ + Index: 3, + FirstBlock: 3, + LastBlock: 3, + AppAddress: appAddress, + Status: EpochStatusClaimSubmitted, + ClaimHash: &claim1Hash, + } + + repository.Unset("GetEpoch") + repository.On("GetEpoch", + mock.Anything, + mock.Anything, + mock.Anything).Return(claim1, nil) + + repository.Unset("GetPreviousEpochsWithOpenClaims") + repository.On("GetPreviousEpochsWithOpenClaims", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]*Epoch{}, fmt.Errorf("No previous epochs for you")) + + repository.Unset("UpdateEpochs") + repository.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return(nil) + + //No Inputs + inputBox.Unset("RetrieveInputs") + inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + client.Unset("HeaderByNumber") + client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + repository.AssertNumberOfCalls( + s.T(), + "UpdateEpochs", + 0, + ) + + }) + + s.Run("whenGetEpochsFails", func() { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + consensusContract := &MockIConsensusContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewIConsensus") + contractFactory.On("NewIConsensus", + mock.Anything, + ).Return(consensusContract, nil) + + //New EVM Reader + client := newMockEthClient() + wsClient := FakeWSEhtClient{} + inputBox := newMockInputBox() + repository := newMockRepository() + evmReader := NewEvmReader( + client, + &wsClient, + inputBox, + repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Claims Acceptance Events + + claimEvent0 := &iconsensus.IConsensusClaimAcceptance{ + AppContract: appAddress, + LastProcessedBlockNumber: big.NewInt(3), + Claim: common.HexToHash("0xdeadbeef"), + } + + claimEvents := []*iconsensus.IConsensusClaimAcceptance{claimEvent0} + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return(claimEvents, nil).Once() + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return([]*iconsensus.IConsensusClaimAcceptance{}, nil) + + // Epoch Length + consensusContract.On("GetEpochLength", + mock.Anything, + ).Return(big.NewInt(1), nil).Once() + + // Prepare repository + repository.Unset("GetAllRunningApplications") + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x10, + }}, nil).Once() + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x11, + }}, nil).Once() + + claim0Hash := common.HexToHash("0xdeadbeef") + claim0 := &Epoch{ + Index: 1, + FirstBlock: 1, + LastBlock: 1, + AppAddress: appAddress, + Status: EpochStatusClaimSubmitted, + ClaimHash: &claim0Hash, + } + + repository.Unset("GetEpoch") + repository.On("GetEpoch", + mock.Anything, + mock.Anything, + mock.Anything).Return(nil, fmt.Errorf("No epoch for you")) + + repository.Unset("GetPreviousEpochsWithOpenClaims") + repository.On("GetPreviousEpochsWithOpenClaims", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]*Epoch{claim0}, nil) + + repository.Unset("UpdateEpochs") + repository.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return(nil) + + //No Inputs + inputBox.Unset("RetrieveInputs") + inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + client.Unset("HeaderByNumber") + client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + repository.AssertNumberOfCalls( + s.T(), + "UpdateEpochs", + 0, + ) + + }) + + s.Run("whenHasPreviousOpenClaims", func() { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + consensusContract := &MockIConsensusContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewIConsensus") + contractFactory.On("NewIConsensus", + mock.Anything, + ).Return(consensusContract, nil) + + //New EVM Reader + client := newMockEthClient() + wsClient := FakeWSEhtClient{} + inputBox := newMockInputBox() + repository := newMockRepository() + evmReader := NewEvmReader( + client, + &wsClient, + inputBox, + repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Claims Acceptance Events + + claimEvent0 := &iconsensus.IConsensusClaimAcceptance{ + AppContract: appAddress, + LastProcessedBlockNumber: big.NewInt(3), + Claim: common.HexToHash("0xdeadbeef"), + } + + claimEvents := []*iconsensus.IConsensusClaimAcceptance{claimEvent0} + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return(claimEvents, nil).Once() + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return([]*iconsensus.IConsensusClaimAcceptance{}, nil) + + // Epoch Length + consensusContract.On("GetEpochLength", + mock.Anything, + ).Return(big.NewInt(1), nil).Once() + + // Prepare repository + repository.Unset("GetAllRunningApplications") + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x10, + }}, nil).Once() + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastClaimCheckBlock: 0x11, + }}, nil).Once() + + claim0Hash := common.HexToHash("0xdeadbeef") + claim0 := &Epoch{ + Index: 1, + FirstBlock: 1, + LastBlock: 1, + AppAddress: appAddress, + Status: EpochStatusClaimSubmitted, + ClaimHash: &claim0Hash, + } + + repository.Unset("GetPreviousEpochsWithOpenClaims") + repository.On("GetPreviousEpochsWithOpenClaims", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]*Epoch{claim0}, nil) + + repository.Unset("UpdateEpochs") + repository.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return(nil) + + //No Inputs + inputBox.Unset("RetrieveInputs") + inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + client.Unset("HeaderByNumber") + client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + repository.AssertNumberOfCalls( + s.T(), + "UpdateEpochs", + 0, + ) + + }) +} diff --git a/internal/evmreader/consensus_adapter.go b/internal/evmreader/consensus_adapter.go new file mode 100644 index 00000000..c7b15227 --- /dev/null +++ b/internal/evmreader/consensus_adapter.go @@ -0,0 +1,58 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "math/big" + + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iconsensus" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// IConsensus Wrapper +type ConsensusContractAdapter struct { + consensus *iconsensus.IConsensus +} + +func NewConsensusContractAdapter( + iconsensusAddress common.Address, + client *ethclient.Client, +) (*ConsensusContractAdapter, error) { + consensus, err := iconsensus.NewIConsensus(iconsensusAddress, client) + if err != nil { + return nil, err + } + return &ConsensusContractAdapter{ + consensus: consensus, + }, nil +} + +func (c *ConsensusContractAdapter) GetEpochLength(opts *bind.CallOpts) (*big.Int, error) { + return c.consensus.GetEpochLength(opts) +} + +func (c *ConsensusContractAdapter) RetrieveClaimAcceptanceEvents( + opts *bind.FilterOpts, + appAddresses []common.Address, +) ([]*iconsensus.IConsensusClaimAcceptance, error) { + + itr, err := c.consensus.FilterClaimAcceptance(opts, appAddresses) + if err != nil { + return nil, err + } + defer itr.Close() + + var events []*iconsensus.IConsensusClaimAcceptance + for itr.Next() { + claimAcceptanceEvent := itr.Event + events = append(events, claimAcceptanceEvent) + } + if err = itr.Error(); err != nil { + return nil, err + } + return events, nil +} diff --git a/internal/evmreader/evmreader.go b/internal/evmreader/evmreader.go new file mode 100644 index 00000000..f7eefe90 --- /dev/null +++ b/internal/evmreader/evmreader.go @@ -0,0 +1,358 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "context" + "errors" + "fmt" + "log/slog" + "math/big" + "os" + "strings" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + appcontract "github.com/ZzzzHui/espresso-reader/pkg/contracts/iapplication" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iconsensus" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +// Interface for Input reading +type InputSource interface { + // Wrapper for FilterInputAdded(), which is automatically generated + // by go-ethereum and cannot be used for testing + RetrieveInputs(opts *bind.FilterOpts, appAddresses []Address, index []*big.Int, + ) ([]iinputbox.IInputBoxInputAdded, error) +} + +// Interface for the node repository +type EvmReaderRepository interface { + StoreEpochAndInputsTransaction( + ctx context.Context, epochInputMap map[*Epoch][]Input, blockNumber uint64, + appAddress Address, + ) (epochIndexIdMap map[uint64]uint64, epochIndexInputIdsMap map[uint64][]uint64, err error) + + GetAllRunningApplications(ctx context.Context) ([]Application, error) + GetNodeConfig(ctx context.Context) (*NodePersistentConfig, error) + GetEpoch(ctx context.Context, indexKey uint64, appAddressKey Address) (*Epoch, error) + GetPreviousEpochsWithOpenClaims( + ctx context.Context, + app Address, + lastBlock uint64, + ) ([]*Epoch, error) + UpdateEpochs(ctx context.Context, + app Address, + claims []*Epoch, + mostRecentBlockNumber uint64, + ) error + GetOutput( + ctx context.Context, appAddressKey Address, indexKey uint64, + ) (*Output, error) + UpdateOutputExecutionTransaction( + ctx context.Context, app Address, executedOutputs []*Output, blockNumber uint64, + ) error + GetInputIndex( + ctx context.Context, + applicationAddress Address, + ) (uint64, error) + UpdateInputIndex( + ctx context.Context, + applicationAddress Address, + ) error +} + +// EthClient mimics part of ethclient.Client functions to narrow down the +// interface needed by the EvmReader. It must be bound to an HTTP endpoint +type EthClient interface { + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) +} + +// EthWsClient mimics part of ethclient.Client functions to narrow down the +// interface needed by the EvmReader. It must be bound to a WS endpoint +type EthWsClient interface { + SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) +} + +type ConsensusContract interface { + GetEpochLength(opts *bind.CallOpts) (*big.Int, error) + RetrieveClaimAcceptanceEvents( + opts *bind.FilterOpts, + appAddresses []Address, + ) ([]*iconsensus.IConsensusClaimAcceptance, error) +} + +type ApplicationContract interface { + GetConsensus(opts *bind.CallOpts) (Address, error) + RetrieveOutputExecutionEvents( + opts *bind.FilterOpts, + ) ([]*appcontract.IApplicationOutputExecuted, error) +} + +type ContractFactory interface { + NewApplication(address Address) (ApplicationContract, error) + NewIConsensus(address Address) (ConsensusContract, error) +} + +type SubscriptionError struct { + Cause error +} + +func (e *SubscriptionError) Error() string { + return fmt.Sprintf("Subscription error : %v", e.Cause) +} + +// Internal struct to hold application and it's contracts together +type application struct { + Application + ApplicationContract + ConsensusContract +} + +// EvmReader reads Input Added, Claim Submitted and +// Output Executed events from the blockchain +type EvmReader struct { + client EthClient + wsClient EthWsClient + inputSource InputSource + repository EvmReaderRepository + contractFactory ContractFactory + inputBoxDeploymentBlock uint64 + defaultBlock DefaultBlock + epochLengthCache map[Address]uint64 + hasEnabledApps bool + shouldModifyIndex bool // modify index in raw data if the main sequencer is espresso + IOAbi abi.ABI +} + +func (r *EvmReader) String() string { + return "evmreader" +} + +// Creates a new EvmReader +func NewEvmReader( + client EthClient, + wsClient EthWsClient, + inputSource InputSource, + repository EvmReaderRepository, + inputBoxDeploymentBlock uint64, + defaultBlock DefaultBlock, + contractFactory ContractFactory, + shouldModifyIndex bool, +) EvmReader { + abiData, err := os.ReadFile("internal/evmreader/abi.json") + if err != nil { + panic(err) + } + ioABI, err := abi.JSON(strings.NewReader(string(abiData))) + if err != nil { + panic(err) + } + evmReader := EvmReader{ + client: client, + wsClient: wsClient, + inputSource: inputSource, + repository: repository, + inputBoxDeploymentBlock: inputBoxDeploymentBlock, + defaultBlock: defaultBlock, + contractFactory: contractFactory, + hasEnabledApps: true, + shouldModifyIndex: shouldModifyIndex, + IOAbi: ioABI, + } + // Initialize epochLength cache + evmReader.epochLengthCache = make(map[Address]uint64) + return evmReader +} + +func (r *EvmReader) Run(ctx context.Context, ready chan<- struct{}) error { + for { + err := r.watchForNewBlocks(ctx, ready) + // If the error is a SubscriptionError, re run watchForNewBlocks + // that it will restart the websocket subscription + if _, ok := err.(*SubscriptionError); !ok { + return err + } + slog.Error(err.Error()) + slog.Info("evmreader: Restarting subscription") + } +} + +// watchForNewBlocks watches for new blocks and reads new inputs based on the +// default block configuration, which have not been processed yet. +func (r *EvmReader) watchForNewBlocks(ctx context.Context, ready chan<- struct{}) error { + headers := make(chan *types.Header) + sub, err := r.wsClient.SubscribeNewHead(ctx, headers) + if err != nil { + return fmt.Errorf("could not start subscription: %v", err) + } + slog.Info("evmreader: Subscribed to new block events") + ready <- struct{}{} + defer sub.Unsubscribe() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-sub.Err(): + return &SubscriptionError{Cause: err} + case header := <-headers: + + // Every time a new block arrives + slog.Debug("evmreader: New block header received", "blockNumber", header.Number, "blockHash", header.Hash()) + + slog.Debug("evmreader: Retrieving enabled applications") + // Get All Applications + runningApps, err := r.repository.GetAllRunningApplications(ctx) + if err != nil { + slog.Error("evmreader: Error retrieving running applications", + "error", + err, + ) + continue + } + + if len(runningApps) == 0 { + if r.hasEnabledApps { + slog.Info("evmreader: No registered applications enabled") + } + r.hasEnabledApps = false + continue + } + if !r.hasEnabledApps { + slog.Info("evmreader: Found enabled applications") + } + r.hasEnabledApps = true + + // Build Contracts + var apps []application + for _, app := range runningApps { + applicationContract, consensusContract, err := r.GetAppContracts(app) + if err != nil { + slog.Error("evmreader: Error retrieving application contracts", "app", app, "error", err) + continue + } + apps = append(apps, application{Application: app, + ApplicationContract: applicationContract, + ConsensusContract: consensusContract}) + } + + if len(apps) == 0 { + slog.Info("evmreader: No correctly configured applications running") + continue + } + + blockNumber := header.Number.Uint64() + if r.defaultBlock != DefaultBlockStatusLatest { + mostRecentHeader, err := r.fetchMostRecentHeader( + ctx, + r.defaultBlock, + ) + if err != nil { + slog.Error("evmreader: Error fetching most recent block", + "default block", r.defaultBlock, + "error", err) + continue + } + blockNumber = mostRecentHeader.Number.Uint64() + + slog.Debug(fmt.Sprintf("evmreader: Using block %d and not %d because of commitment policy: %s", + mostRecentHeader.Number.Uint64(), header.Number.Uint64(), r.defaultBlock)) + } + + r.checkForNewInputs(ctx, apps, blockNumber) + + r.CheckForClaimStatus(ctx, apps, blockNumber) + + r.CheckForOutputExecution(ctx, apps, blockNumber) + + } + } +} + +// fetchMostRecentHeader fetches the most recent header up till the +// given default block +func (r *EvmReader) fetchMostRecentHeader( + ctx context.Context, + defaultBlock DefaultBlock, +) (*types.Header, error) { + + var defaultBlockNumber int64 + switch defaultBlock { + case DefaultBlockStatusPending: + defaultBlockNumber = rpc.PendingBlockNumber.Int64() + case DefaultBlockStatusLatest: + defaultBlockNumber = rpc.LatestBlockNumber.Int64() + case DefaultBlockStatusFinalized: + defaultBlockNumber = rpc.FinalizedBlockNumber.Int64() + case DefaultBlockStatusSafe: + defaultBlockNumber = rpc.SafeBlockNumber.Int64() + default: + return nil, fmt.Errorf("default block '%v' not supported", defaultBlock) + } + + header, err := + r.client.HeaderByNumber( + ctx, + new(big.Int).SetInt64(defaultBlockNumber)) + if err != nil { + return nil, fmt.Errorf("failed to retrieve header. %v", err) + } + + if header == nil { + return nil, fmt.Errorf("returned header is nil") + } + return header, nil +} + +// GetAppContracts retrieves the ApplicationContract and ConsensusContract for a given Application. +// Also validates if IConsensus configuration matches the blockchain registered one +func (r *EvmReader) GetAppContracts(app Application, +) (ApplicationContract, ConsensusContract, error) { + applicationContract, err := r.contractFactory.NewApplication(app.ContractAddress) + if err != nil { + return nil, nil, errors.Join( + fmt.Errorf("error building application contract"), + err, + ) + + } + consensusAddress, err := applicationContract.GetConsensus(nil) + if err != nil { + return nil, nil, errors.Join( + fmt.Errorf("error retrieving application consensus"), + err, + ) + } + + if app.IConsensusAddress != consensusAddress { + return nil, nil, + fmt.Errorf("IConsensus addresses do not match. Deployed: %s. Configured: %s", + consensusAddress, + app.IConsensusAddress) + } + + consensus, err := r.contractFactory.NewIConsensus(consensusAddress) + if err != nil { + return nil, nil, errors.Join( + fmt.Errorf("error building consensus contract"), + err, + ) + + } + return applicationContract, consensus, nil +} + +func (r *EvmReader) GetEpochLengthCache(a Address) uint64 { + return r.epochLengthCache[a] +} + +func (r *EvmReader) GetEthClient() *EthClient { + return &r.client +} diff --git a/internal/evmreader/evmreader_test.go b/internal/evmreader/evmreader_test.go new file mode 100644 index 00000000..7f00f815 --- /dev/null +++ b/internal/evmreader/evmreader_test.go @@ -0,0 +1,765 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "math/big" + "testing" + "time" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + appcontract "github.com/ZzzzHui/espresso-reader/pkg/contracts/iapplication" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iconsensus" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +const ( + suiteTimeout = 120 * time.Second +) + +//go:embed testdata/input_added_event_0.json +var inputAddedEvent0JsonBytes []byte + +//go:embed testdata/input_added_event_1.json +var inputAddedEvent1JsonBytes []byte + +//go:embed testdata/input_added_event_2.json +var inputAddedEvent2JsonBytes []byte + +//go:embed testdata/input_added_event_3.json +var inputAddedEvent3JsonBytes []byte + +//go:embed testdata/header_0.json +var header0JsonBytes []byte + +//go:embed testdata/header_1.json +var header1JsonBytes []byte + +//go:embed testdata/header_2.json +var header2JsonBytes []byte + +var ( + header0 = types.Header{} + header1 = types.Header{} + header2 = types.Header{} + + block0 = types.Block{} + + inputAddedEvent0 = iinputbox.IInputBoxInputAdded{} + inputAddedEvent1 = iinputbox.IInputBoxInputAdded{} + inputAddedEvent2 = iinputbox.IInputBoxInputAdded{} + inputAddedEvent3 = iinputbox.IInputBoxInputAdded{} + + subscription0 = newMockSubscription() +) + +type EvmReaderSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + client *MockEthClient + wsClient *MockEthClient + inputBox *MockInputBox + repository *MockRepository + evmReader *EvmReader + contractFactory *MockEvmReaderContractFactory +} + +func TestEvmReaderSuite(t *testing.T) { + suite.Run(t, new(EvmReaderSuite)) +} + +func (s *EvmReaderSuite) SetupSuite() { + s.ctx, s.cancel = context.WithTimeout(context.Background(), suiteTimeout) + + err := json.Unmarshal(header0JsonBytes, &header0) + s.Require().Nil(err) + err = json.Unmarshal(header1JsonBytes, &header1) + s.Require().Nil(err) + err = json.Unmarshal(header2JsonBytes, &header2) + s.Require().Nil(err) + + block0 = *types.NewBlockWithHeader(&header0) + + err = json.Unmarshal(inputAddedEvent0JsonBytes, &inputAddedEvent0) + s.Require().Nil(err) + err = json.Unmarshal(inputAddedEvent1JsonBytes, &inputAddedEvent1) + s.Require().Nil(err) + err = json.Unmarshal(inputAddedEvent2JsonBytes, &inputAddedEvent2) + s.Require().Nil(err) + err = json.Unmarshal(inputAddedEvent3JsonBytes, &inputAddedEvent3) + s.Require().Nil(err) +} + +func (s *EvmReaderSuite) TearDownSuite() { + s.cancel() +} + +func (s *EvmReaderSuite) SetupTest() { + + s.client = newMockEthClient() + s.wsClient = s.client + s.inputBox = newMockInputBox() + s.repository = newMockRepository() + s.contractFactory = newEmvReaderContractFactory() + inputReader := NewEvmReader( + s.client, + s.wsClient, + s.inputBox, + s.repository, + 0, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + s.evmReader = &inputReader +} + +// Service tests +func (s *EvmReaderSuite) TestItStopsWhenContextIsCanceled() { + ctx, cancel := context.WithCancel(s.ctx) + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + go func() { + errChannel <- s.evmReader.Run(ctx, ready) + }() + cancel() + + err := <-errChannel + s.Require().Equal(context.Canceled, err, "stopped for the wrong reason") +} + +func (s *EvmReaderSuite) TestItEventuallyBecomesReady() { + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + go func() { + errChannel <- s.evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + case err := <-errChannel: + s.FailNow("unexpected failure", err) + } +} + +func (s *EvmReaderSuite) TestItFailsToSubscribeForNewInputsOnStart() { + s.client.Unset("SubscribeNewHead") + emptySubscription := &MockSubscription{} + s.client.On( + "SubscribeNewHead", + mock.Anything, + mock.Anything, + ).Return(emptySubscription, fmt.Errorf("expected failure")) + + s.Require().ErrorContains( + s.evmReader.Run(s.ctx, make(chan struct{}, 1)), + "expected failure") + s.client.AssertNumberOfCalls(s.T(), "SubscribeNewHead", 1) +} + +func (s *EvmReaderSuite) TestItWrongIConsensus() { + + consensusContract := &MockIConsensusContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewIConsensus") + contractFactory.On("NewIConsensus", + mock.Anything, + ).Return(consensusContract, nil) + + wsClient := FakeWSEhtClient{} + + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare consensus + claimEvent0 := &iconsensus.IConsensusClaimAcceptance{ + AppContract: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + LastProcessedBlockNumber: big.NewInt(3), + Claim: common.HexToHash("0xdeadbeef"), + } + + claimEvents := []*iconsensus.IConsensusClaimAcceptance{claimEvent0} + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return(claimEvents, nil).Once() + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xFFFFFFFF"), + LastProcessedBlock: 0x00, + }}, nil).Once() + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(time.Second) + + // Should not advance input processing + s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveInputs", 0) + s.repository.AssertNumberOfCalls( + s.T(), + "StoreEpochAndInputsTransaction", + 0, + ) + + // Should not advance claim acceptance processing + s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveClaimAcceptanceEvents", 0) + s.repository.AssertNumberOfCalls( + s.T(), + "UpdateEpochs", + 0, + ) +} + +// Mock EthClient +type MockEthClient struct { + mock.Mock +} + +func newMockEthClient() *MockEthClient { + client := &MockEthClient{} + + client.On("HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil) + + client.On("SubscribeNewHead", + mock.Anything, + mock.Anything, + ).Return(subscription0, nil) + + return client +} + +func (m *MockEthClient) Unset(methodName string) { + for _, call := range m.ExpectedCalls { + if call.Method == methodName { + call.Unset() + } + } +} + +func (m *MockEthClient) HeaderByNumber( + ctx context.Context, + number *big.Int, +) (*types.Header, error) { + args := m.Called(ctx, number) + return args.Get(0).(*types.Header), args.Error(1) +} + +func (m *MockEthClient) SubscribeNewHead( + ctx context.Context, + ch chan<- *types.Header, +) (ethereum.Subscription, error) { + args := m.Called(ctx, ch) + return args.Get(0).(ethereum.Subscription), args.Error(1) +} + +// Mock ethereum.Subscription +type MockSubscription struct { + mock.Mock +} + +func newMockSubscription() *MockSubscription { + sub := &MockSubscription{} + + sub.On("Unsubscribe").Return() + sub.On("Err").Return(make(<-chan error)) + + return sub +} + +func (m *MockSubscription) Unsubscribe() { +} + +func (m *MockSubscription) Err() <-chan error { + args := m.Called() + return args.Get(0).(<-chan error) +} + +// FakeClient +type FakeWSEhtClient struct { + ch chan<- *types.Header +} + +func (f *FakeWSEhtClient) SubscribeNewHead( + ctx context.Context, + ch chan<- *types.Header, +) (ethereum.Subscription, error) { + f.ch = ch + return newMockSubscription(), nil +} + +func (f *FakeWSEhtClient) fireNewHead(header *types.Header) { + f.ch <- header +} + +// Mock inputbox.InputBox +type MockInputBox struct { + mock.Mock +} + +func newMockInputBox() *MockInputBox { + inputSource := &MockInputBox{} + + events := []iinputbox.IInputBoxInputAdded{inputAddedEvent0} + inputSource.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return(events, nil) + + return inputSource +} + +func (m *MockInputBox) Unset(methodName string) { + for _, call := range m.ExpectedCalls { + if call.Method == methodName { + call.Unset() + } + } +} + +func (m *MockInputBox) RetrieveInputs( + opts *bind.FilterOpts, + appContract []common.Address, + index []*big.Int, +) ([]iinputbox.IInputBoxInputAdded, error) { + args := m.Called(opts, appContract, index) + return args.Get(0).([]iinputbox.IInputBoxInputAdded), args.Error(1) +} + +// Mock InputReaderRepository +type MockRepository struct { + mock.Mock +} + +func newMockRepository() *MockRepository { + repo := &MockRepository{} + + repo.On("StoreEpochAndInputsTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(make(map[uint64]uint64), make(map[uint64][]uint64), nil) + + repo.On("GetEpoch", + mock.Anything, + uint64(0), + mock.Anything).Return( + &Epoch{ + Id: 1, + Index: 0, + FirstBlock: 0, + LastBlock: 9, + Status: EpochStatusOpen, + AppAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + ClaimHash: nil, + TransactionHash: nil, + }, nil) + repo.On("GetEpoch", + mock.Anything, + uint64(1), + mock.Anything).Return( + &Epoch{ + Id: 2, + Index: 1, + FirstBlock: 10, + LastBlock: 19, + Status: EpochStatusOpen, + AppAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + ClaimHash: nil, + TransactionHash: nil, + }, nil) + repo.On("GetEpoch", + mock.Anything, + uint64(2), + mock.Anything).Return( + &Epoch{ + Id: 3, + Index: 2, + FirstBlock: 20, + LastBlock: 29, + Status: EpochStatusOpen, + AppAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + ClaimHash: nil, + TransactionHash: nil, + }, nil) + + repo.On("InsertEpoch", + mock.Anything, + mock.Anything).Return(1, nil) + + repo.On("GetPreviousEpochsWithOpenClaims", + mock.Anything, + mock.Anything, + ).Return([]Epoch{}, nil) + + repo.On("UpdateEpochs", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Return(nil) + + repo.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + outputHash := common.HexToHash("0xAABBCCDDEE") + repo.On("GetOutput", + mock.Anything, + 0, + common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E")).Return( + &Output{ + Id: 1, + Index: 0, + RawData: common.Hex2Bytes("0xdeadbeef"), + Hash: &outputHash, + InputId: 1, + OutputHashesSiblings: nil, + TransactionHash: nil, + }, + ) + + return repo + +} + +func (s *EvmReaderSuite) TestIndexApps() { + + s.Run("Ok", func() { + apps := []application{ + {Application: Application{LastProcessedBlock: 23}}, + {Application: Application{LastProcessedBlock: 22}}, + {Application: Application{LastProcessedBlock: 21}}, + {Application: Application{LastProcessedBlock: 23}}, + } + + keyByProcessedBlock := func(a application) uint64 { + return a.LastProcessedBlock + } + + indexApps := indexApps(keyByProcessedBlock, apps) + + s.Require().Equal(3, len(indexApps)) + apps, ok := indexApps[23] + s.Require().True(ok) + s.Require().Equal(2, len(apps)) + }) + + s.Run("whenIndexAppsArrayEmpty", func() { + apps := []application{} + + keyByProcessedBlock := func(a application) uint64 { + return a.LastProcessedBlock + } + + indexApps := indexApps(keyByProcessedBlock, apps) + + s.Require().Equal(0, len(indexApps)) + }) + + s.Run("whenIndexAppsArray", func() { + apps := []application{} + + keyByProcessedBlock := func(a application) uint64 { + return a.LastProcessedBlock + } + + indexApps := indexApps(keyByProcessedBlock, apps) + + s.Require().Equal(0, len(indexApps)) + }) + + s.Run("whenIndexByEmptyKey", func() { + apps := []application{ + {Application: Application{LastProcessedBlock: 23}}, + {Application: Application{LastProcessedBlock: 22}}, + {Application: Application{LastProcessedBlock: 21}}, + {Application: Application{LastProcessedBlock: 23}}, + } + + keyByIConsensus := func(a application) ConsensusContract { + return a.ConsensusContract + } + + indexApps := indexApps(keyByIConsensus, apps) + + s.Require().Equal(1, len(indexApps)) + apps, ok := indexApps[nil] + s.Require().True(ok) + s.Require().Equal(4, len(apps)) + }) + + s.Run("whenUsesWrongKey", func() { + apps := []application{ + {Application: Application{LastProcessedBlock: 23}}, + {Application: Application{LastProcessedBlock: 22}}, + {Application: Application{LastProcessedBlock: 21}}, + {Application: Application{LastProcessedBlock: 23}}, + } + + keyByProcessedBlock := func(a application) uint64 { + return a.LastProcessedBlock + } + + indexApps := indexApps(keyByProcessedBlock, apps) + + s.Require().Equal(3, len(indexApps)) + apps, ok := indexApps[0] + s.Require().False(ok) + s.Require().Nil(apps) + + }) + +} + +func (m *MockRepository) Unset(methodName string) { + for _, call := range m.ExpectedCalls { + if call.Method == methodName { + call.Unset() + } + } +} + +func (m *MockRepository) StoreEpochAndInputsTransaction( + ctx context.Context, + epochInputMap map[*Epoch][]Input, + blockNumber uint64, + appAddress common.Address, +) (epochIndexIdMap map[uint64]uint64, epochIndexInputIdsMap map[uint64][]uint64, err error) { + args := m.Called(ctx, epochInputMap, blockNumber, appAddress) + return args.Get(0).(map[uint64]uint64), args.Get(1).(map[uint64][]uint64), args.Error(2) +} + +func (m *MockRepository) GetAllRunningApplications( + ctx context.Context, +) ([]Application, error) { + args := m.Called(ctx) + return args.Get(0).([]Application), args.Error(1) +} + +func (m *MockRepository) GetNodeConfig( + ctx context.Context, +) (*NodePersistentConfig, error) { + args := m.Called(ctx) + return args.Get(0).(*NodePersistentConfig), args.Error(1) +} + +func (m *MockRepository) GetEpoch( + ctx context.Context, + index uint64, + appAddress common.Address, +) (*Epoch, error) { + args := m.Called(ctx, index, appAddress) + obj := args.Get(0) + if obj == nil { + return nil, args.Error(1) + } + return obj.(*Epoch), args.Error(1) +} + +func (m *MockRepository) InsertEpoch( + ctx context.Context, + epoch *Epoch, +) (uint64, error) { + args := m.Called(ctx) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *MockRepository) GetPreviousEpochsWithOpenClaims( + ctx context.Context, + app Address, + lastBlock uint64, +) ([]*Epoch, error) { + args := m.Called(ctx, app, lastBlock) + obj := args.Get(0) + if obj == nil { + return nil, args.Error(1) + } + return obj.([]*Epoch), args.Error(1) + +} +func (m *MockRepository) UpdateEpochs(ctx context.Context, + app Address, + epochs []*Epoch, + mostRecentBlockNumber uint64, +) error { + args := m.Called(ctx, epochs, mostRecentBlockNumber) + return args.Error(0) +} + +func (m *MockRepository) GetOutput( + ctx context.Context, appAddressKey Address, indexKey uint64, +) (*Output, error) { + args := m.Called(ctx, indexKey, appAddressKey) + obj := args.Get(0) + if obj == nil { + return nil, args.Error(1) + } + return obj.(*Output), args.Error(1) +} + +func (m *MockRepository) UpdateOutputExecutionTransaction( + ctx context.Context, app Address, executedOutputs []*Output, blockNumber uint64, +) error { + args := m.Called(ctx, app, executedOutputs, blockNumber) + return args.Error(0) +} + +type MockApplicationContract struct { + mock.Mock +} + +func (m *MockApplicationContract) Unset(methodName string) { + for _, call := range m.ExpectedCalls { + if call.Method == methodName { + call.Unset() + } + } +} + +func (m *MockApplicationContract) GetConsensus( + opts *bind.CallOpts, +) (common.Address, error) { + args := m.Called(opts) + return args.Get(0).(common.Address), args.Error(1) +} + +func (m *MockApplicationContract) RetrieveOutputExecutionEvents( + opts *bind.FilterOpts, +) ([]*appcontract.IApplicationOutputExecuted, error) { + args := m.Called(opts) + return args.Get(0).([]*appcontract.IApplicationOutputExecuted), args.Error(1) +} + +type MockIConsensusContract struct { + mock.Mock +} + +func (m *MockIConsensusContract) GetEpochLength( + opts *bind.CallOpts, +) (*big.Int, error) { + args := m.Called(opts) + return args.Get(0).(*big.Int), args.Error(1) +} + +func (m *MockIConsensusContract) RetrieveClaimAcceptanceEvents( + opts *bind.FilterOpts, appAddresses []common.Address, +) ([]*iconsensus.IConsensusClaimAcceptance, error) { + args := m.Called(opts, appAddresses) + return args.Get(0).([]*iconsensus.IConsensusClaimAcceptance), args.Error(1) +} + +type MockEvmReaderContractFactory struct { + mock.Mock +} + +func (m *MockEvmReaderContractFactory) Unset(methodName string) { + for _, call := range m.ExpectedCalls { + if call.Method == methodName { + call.Unset() + } + } +} + +func (m *MockEvmReaderContractFactory) NewApplication( + Address, +) (ApplicationContract, error) { + args := m.Called(context.Background()) + return args.Get(0).(ApplicationContract), args.Error(1) +} + +func (m *MockEvmReaderContractFactory) NewIConsensus( + Address, +) (ConsensusContract, error) { + args := m.Called(context.Background()) + return args.Get(0).(ConsensusContract), args.Error(1) +} + +func newEmvReaderContractFactory() *MockEvmReaderContractFactory { + + applicationContract := &MockApplicationContract{} + + applicationContract.On("GetConsensus", + mock.Anything, + ).Return(common.HexToAddress("0xdeadbeef"), nil) + + applicationContract.On("RetrieveOutputExecutionEvents", + mock.Anything).Return([]*appcontract.IApplicationOutputExecuted{}, nil) + + consensusContract := &MockIConsensusContract{} + + consensusContract.On("GetEpochLength", + mock.Anything).Return(big.NewInt(10), nil) + + consensusContract.On("RetrieveClaimAcceptanceEvents", + mock.Anything, + mock.Anything, + ).Return([]*iconsensus.IConsensusClaimAcceptance{}, nil) + + factory := &MockEvmReaderContractFactory{} + + factory.On("NewApplication", + mock.Anything, + ).Return(applicationContract, nil) + + factory.On("NewIConsensus", + mock.Anything).Return(consensusContract, nil) + + return factory +} diff --git a/internal/evmreader/input.go b/internal/evmreader/input.go new file mode 100644 index 00000000..31f7341b --- /dev/null +++ b/internal/evmreader/input.go @@ -0,0 +1,387 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "context" + "errors" + "fmt" + "log/slog" + "math/big" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +// checkForNewInputs checks if is there new Inputs for all running Applications +func (r *EvmReader) checkForNewInputs( + ctx context.Context, + apps []application, + mostRecentBlockNumber uint64, +) { + + slog.Debug("evmreader: Checking for new inputs") + + groupedApps := indexApps(byLastProcessedBlock, apps) + + for lastProcessedBlock, apps := range groupedApps { + + appAddresses := appsToAddresses(apps) + + // Safeguard: Only check blocks starting from the block where the InputBox + // contract was deployed as Inputs can be added to that same block + if lastProcessedBlock < r.inputBoxDeploymentBlock { + lastProcessedBlock = r.inputBoxDeploymentBlock - 1 + } + + if mostRecentBlockNumber > lastProcessedBlock { + + slog.Debug("evmreader: Checking inputs for applications", + "apps", appAddresses, + "last processed block", lastProcessedBlock, + "most recent block", mostRecentBlockNumber, + ) + + err := r.ReadAndStoreInputs(ctx, + lastProcessedBlock+1, + mostRecentBlockNumber, + apps, + ) + if err != nil { + slog.Error("Error reading inputs", + "apps", appAddresses, + "last processed block", lastProcessedBlock, + "most recent block", mostRecentBlockNumber, + "error", err, + ) + continue + } + } else if mostRecentBlockNumber < lastProcessedBlock { + slog.Warn( + "evmreader: Not reading inputs: most recent block is lower than the last processed one", + "apps", appAddresses, + "last processed block", lastProcessedBlock, + "most recent block", mostRecentBlockNumber, + ) + } else { + slog.Info("evmreader: Not reading inputs: already checked the most recent blocks", + "apps", appAddresses, + "last processed block", lastProcessedBlock, + "most recent block", mostRecentBlockNumber, + ) + } + } +} + +type TypeExportApplication = application + +// ReadAndStoreInputs reads, inputs from the InputSource given specific filter options, indexes +// them into epochs and store the indexed inputs and epochs +func (r *EvmReader) ReadAndStoreInputs( + ctx context.Context, + startBlock uint64, + endBlock uint64, + apps []TypeExportApplication, +) error { + appsToProcess := []common.Address{} + + for _, app := range apps { + + // Get App EpochLength + err := r.AddAppEpochLengthIntoCache(app) + if err != nil { + slog.Error("evmreader: Error adding epoch length into cache", + "app", app.ContractAddress, + "error", err) + continue + } + + appsToProcess = append(appsToProcess, app.ContractAddress) + + } + + if len(appsToProcess) == 0 { + slog.Warn("evmreader: No valid running applications") + return nil + } + + // Retrieve Inputs from blockchain + appInputsMap, err := r.readInputsFromBlockchain(ctx, appsToProcess, startBlock, endBlock) + if err != nil { + return fmt.Errorf("failed to read inputs from block %v to block %v. %w", + startBlock, + endBlock, + err) + } + + // Index Inputs into epochs and handle epoch finalization + for address, inputs := range appInputsMap { + + epochLength := r.epochLengthCache[address] + + // Retrieves last open epoch from DB + currentEpoch, err := r.repository.GetEpoch(ctx, + CalculateEpochIndex(epochLength, startBlock), address) + if err != nil { + slog.Error("evmreader: Error retrieving existing current epoch", + "app", address, + "error", err, + ) + continue + } + + // Check current epoch status + if currentEpoch != nil && currentEpoch.Status != EpochStatusOpen { + slog.Error("evmreader: Current epoch is not open", + "app", address, + "epoch_index", currentEpoch.Index, + "status", currentEpoch.Status, + ) + continue + } + + // Initialize epochs inputs map + var epochInputMap = make(map[*Epoch][]Input) + + // Index Inputs into epochs + for _, input := range inputs { + + inputEpochIndex := CalculateEpochIndex(epochLength, input.BlockNumber) + + // If input belongs into a new epoch, close the previous known one + if currentEpoch != nil && currentEpoch.Index != inputEpochIndex { + currentEpoch.Status = EpochStatusClosed + slog.Info("evmreader: Closing epoch", + "app", currentEpoch.AppAddress, + "epoch_index", currentEpoch.Index, + "start", currentEpoch.FirstBlock, + "end", currentEpoch.LastBlock) + currentEpoch = nil + } + if currentEpoch == nil { + currentEpoch = &Epoch{ + Index: inputEpochIndex, + FirstBlock: inputEpochIndex * epochLength, + LastBlock: (inputEpochIndex * epochLength) + epochLength - 1, + Status: EpochStatusOpen, + AppAddress: address, + } + epochInputMap[currentEpoch] = []Input{} + } + + slog.Info("evmreader: Found new Input", + "app", address, + "index", input.Index, + "block", input.BlockNumber, + "epoch_index", inputEpochIndex) + + currentInputs, ok := epochInputMap[currentEpoch] + if !ok { + currentInputs = []Input{} + } + // overriding input index + combinedIndex, err := r.repository.GetInputIndex(ctx, address) + if err != nil { + slog.Error("evmreader: failed to read index", "app", address, "error", err) + } + if combinedIndex != input.Index && r.shouldModifyIndex == true { + slog.Info("evmreader: Overriding input index", "onchain-index", input.Index, "new-index", combinedIndex) + input.Index = combinedIndex + modifiedRawData, err := r.modifyIndexInRaw(ctx, input.RawData, input.AppAddress, combinedIndex) + if err == nil { + input.RawData = modifiedRawData + } + } + err = r.repository.UpdateInputIndex(ctx, address) + if err != nil { + slog.Error("evmreader: failed to update index", "app", address, "error", err) + } + epochInputMap[currentEpoch] = append(currentInputs, *input) + + } + + // Indexed all inputs. Check if it is time to close this epoch + if currentEpoch != nil && endBlock >= currentEpoch.LastBlock { + currentEpoch.Status = EpochStatusClosed + slog.Info("evmreader: Closing epoch", + "app", currentEpoch.AppAddress, + "epoch_index", currentEpoch.Index, + "start", currentEpoch.FirstBlock, + "end", currentEpoch.LastBlock) + // Add to inputMap so it is stored + _, ok := epochInputMap[currentEpoch] + if !ok { + epochInputMap[currentEpoch] = []Input{} + } + } + + _, _, err = r.repository.StoreEpochAndInputsTransaction( + ctx, + epochInputMap, + endBlock, + address, + ) + if err != nil { + slog.Error("evmreader: Error storing inputs and epochs", + "app", address, + "error", err, + ) + continue + } + + // Store everything + if len(epochInputMap) > 0 { + + slog.Debug("evmreader: Inputs and epochs stored successfully", + "app", address, + "start-block", startBlock, + "end-block", endBlock, + "total epochs", len(epochInputMap), + "total inputs", len(inputs), + ) + } else { + slog.Debug("evmreader: No inputs or epochs to store") + } + + } + + return nil +} + +// AddAppEpochLengthIntoCache checks the epoch length cache and read epoch length from IConsensus +// contract and add it to the cache if needed +func (r *EvmReader) AddAppEpochLengthIntoCache(app application) error { + + epochLength, ok := r.epochLengthCache[app.ContractAddress] + if !ok { + + epochLength, err := getEpochLength(app.ConsensusContract) + if err != nil { + return errors.Join( + fmt.Errorf("error retrieving epoch length from contracts for app %s", + app.ContractAddress), + err) + } + r.epochLengthCache[app.ContractAddress] = epochLength + slog.Info("evmreader: Got epoch length from IConsensus", + "app", app.ContractAddress, + "epoch length", epochLength) + } else { + slog.Debug("evmreader: Got epoch length from cache", + "app", app.ContractAddress, + "epoch length", epochLength) + } + + return nil +} + +// readInputsFromBlockchain read the inputs from the blockchain ordered by Input index +func (r *EvmReader) readInputsFromBlockchain( + ctx context.Context, + appsAddresses []Address, + startBlock, endBlock uint64, +) (map[Address][]*Input, error) { + + // Initialize app input map + var appInputsMap = make(map[Address][]*Input) + for _, appsAddress := range appsAddresses { + appInputsMap[appsAddress] = []*Input{} + } + + opts := bind.FilterOpts{ + Context: ctx, + Start: startBlock, + End: &endBlock, + } + inputsEvents, err := r.inputSource.RetrieveInputs(&opts, appsAddresses, nil) + if err != nil { + return nil, err + } + + // Order inputs as order is not enforced by RetrieveInputs method nor the APIs + for _, event := range inputsEvents { + slog.Debug("evmreader: Received input", + "app", event.AppContract, + "index", event.Index, + "block", event.Raw.BlockNumber) + input := &Input{ + Index: event.Index.Uint64(), + CompletionStatus: InputStatusNone, + RawData: event.Input, + BlockNumber: event.Raw.BlockNumber, + AppAddress: event.AppContract, + TransactionId: event.Index.Bytes(), + } + + // Insert Sorted + appInputsMap[event.AppContract] = insertSorted( + sortByInputIndex, appInputsMap[event.AppContract], input) + } + return appInputsMap, nil +} + +// byLastProcessedBlock key extractor function intended to be used with `indexApps` function +func byLastProcessedBlock(app application) uint64 { + return app.LastProcessedBlock +} + +// getEpochLength reads the application epoch length given it's consensus contract +func getEpochLength(consensus ConsensusContract) (uint64, error) { + + epochLengthRaw, err := consensus.GetEpochLength(nil) + if err != nil { + return 0, errors.Join( + fmt.Errorf("error retrieving application epoch length"), + err, + ) + } + + return epochLengthRaw.Uint64(), nil +} + +func (r *EvmReader) modifyIndexInRaw(ctx context.Context, rawData []byte, appAddress common.Address, currentIndex uint64) ([]byte, error) { + // load contract ABI + abiObject := r.IOAbi + values, err := abiObject.Methods["EvmAdvance"].Inputs.Unpack(rawData[4:]) + if err != nil { + slog.Error("Error unpacking abi", "err", err) + return []byte{}, err + } + + type EvmAdvance struct { + ChainId *big.Int + AppContract common.Address + MsgSender common.Address + BlockNumber *big.Int + BlockTimestamp *big.Int + PrevRandao *big.Int + Index *big.Int + Payload []byte + } + + data := EvmAdvance{ + ChainId: values[0].(*big.Int), + AppContract: values[1].(common.Address), + MsgSender: values[2].(common.Address), + BlockNumber: values[3].(*big.Int), + BlockTimestamp: values[4].(*big.Int), + PrevRandao: values[5].(*big.Int), + Index: values[6].(*big.Int), + Payload: values[7].([]byte), + } + + // modify index + data.Index = big.NewInt(int64(currentIndex)) + + // abi encode again + dataAbi, err := abiObject.Pack("EvmAdvance", data.ChainId, data.AppContract, data.MsgSender, data.BlockNumber, data.BlockTimestamp, data.PrevRandao, data.Index, data.Payload) + if err != nil { + slog.Error("failed to abi encode", "error", err) + return []byte{}, err + } + + return dataAbi, nil +} diff --git a/internal/evmreader/input_test.go b/internal/evmreader/input_test.go new file mode 100644 index 00000000..7facace0 --- /dev/null +++ b/internal/evmreader/input_test.go @@ -0,0 +1,388 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "time" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" +) + +func (s *EvmReaderSuite) TestItReadsInputsFromNewBlocks() { + + wsClient := FakeWSEhtClient{} + + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastProcessedBlock: 0x00, + }}, nil).Once() + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastProcessedBlock: 0x11, + }}, nil).Once() + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header1, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header2, nil).Once() + + // Prepare sequence of inputs + s.inputBox.Unset("RetrieveInputs") + events_0 := []iinputbox.IInputBoxInputAdded{inputAddedEvent0} + mostRecentBlockNumber_0 := uint64(0x11) + retrieveInputsOpts_0 := bind.FilterOpts{ + Context: s.ctx, + Start: 0x10, + End: &mostRecentBlockNumber_0, + } + s.inputBox.On( + "RetrieveInputs", + &retrieveInputsOpts_0, + mock.Anything, + mock.Anything, + ).Return(events_0, nil) + + events_1 := []iinputbox.IInputBoxInputAdded{inputAddedEvent1} + mostRecentBlockNumber_1 := uint64(0x12) + retrieveInputsOpts_1 := bind.FilterOpts{ + Context: s.ctx, + Start: 0x12, + End: &mostRecentBlockNumber_1, + } + s.inputBox.On( + "RetrieveInputs", + &retrieveInputsOpts_1, + mock.Anything, + mock.Anything, + ).Return(events_1, nil) + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + wsClient.fireNewHead(&header1) + time.Sleep(time.Second) + + s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveInputs", 2) + s.repository.AssertNumberOfCalls( + s.T(), + "StoreEpochAndInputsTransaction", + 2, + ) +} + +func (s *EvmReaderSuite) TestItUpdatesLastProcessedBlockWhenThereIsNoInputs() { + + wsClient := FakeWSEhtClient{} + + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastProcessedBlock: 0x00, + }}, nil).Once() + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastProcessedBlock: 0x11, + }}, nil).Once() + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header1, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header2, nil).Once() + + // Prepare sequence of inputs + s.inputBox.Unset("RetrieveInputs") + events_0 := []iinputbox.IInputBoxInputAdded{} + mostRecentBlockNumber_0 := uint64(0x11) + retrieveInputsOpts_0 := bind.FilterOpts{ + Context: s.ctx, + Start: 0x10, + End: &mostRecentBlockNumber_0, + } + s.inputBox.On( + "RetrieveInputs", + &retrieveInputsOpts_0, + mock.Anything, + mock.Anything, + ).Return(events_0, nil) + + events_1 := []iinputbox.IInputBoxInputAdded{} + mostRecentBlockNumber_1 := uint64(0x12) + retrieveInputsOpts_1 := bind.FilterOpts{ + Context: s.ctx, + Start: 0x12, + End: &mostRecentBlockNumber_1, + } + s.inputBox.On( + "RetrieveInputs", + &retrieveInputsOpts_1, + mock.Anything, + mock.Anything, + ).Return(events_1, nil) + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + wsClient.fireNewHead(&header1) + time.Sleep(time.Second) + + s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveInputs", 2) + s.repository.AssertNumberOfCalls( + s.T(), + "StoreEpochAndInputsTransaction", + 2, + ) +} + +func (s *EvmReaderSuite) TestItReadsMultipleInputsFromSingleNewBlock() { + + wsClient := FakeWSEhtClient{} + + inputReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header2, nil).Once() + + // Prepare sequence of inputs + s.inputBox.Unset("RetrieveInputs") + events_2 := []iinputbox.IInputBoxInputAdded{inputAddedEvent2, inputAddedEvent3} + mostRecentBlockNumber_2 := uint64(0x13) + retrieveInputsOpts_2 := bind.FilterOpts{ + Context: s.ctx, + Start: 0x13, + End: &mostRecentBlockNumber_2, + } + s.inputBox.On( + "RetrieveInputs", + &retrieveInputsOpts_2, + mock.Anything, + mock.Anything, + ).Return(events_2, nil) + + // Prepare Repo + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastProcessedBlock: 0x12, + }}, nil).Once() + s.repository.Unset("StoreEpochAndInputsTransaction") + s.repository.On( + "StoreEpochAndInputsTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + var epochInputMap map[*Epoch][]Input + obj := arguments.Get(1) + epochInputMap, ok := obj.(map[*Epoch][]Input) + s.Require().True(ok) + s.Require().Equal(1, len(epochInputMap)) + for _, inputs := range epochInputMap { + s.Require().Equal(2, len(inputs)) + break + } + + }).Return(make(map[uint64]uint64), make(map[uint64][]uint64), nil) + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- inputReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header2) + // Give a time for + time.Sleep(1 * time.Second) + + s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveInputs", 1) + s.repository.AssertNumberOfCalls( + s.T(), + "StoreEpochAndInputsTransaction", + 1, + ) +} + +func (s *EvmReaderSuite) TestItStartsWhenLasProcessedBlockIsTheMostRecentBlock() { + + wsClient := FakeWSEhtClient{} + inputReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Prepare Repo + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastProcessedBlock: 0x13, + }}, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- inputReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header2) + time.Sleep(1 * time.Second) + + s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveInputs", 0) + s.repository.AssertNumberOfCalls( + s.T(), + "StoreEpochAndInputsTransaction", + 0, + ) +} diff --git a/internal/evmreader/inputsource_adapter.go b/internal/evmreader/inputsource_adapter.go new file mode 100644 index 00000000..9bcac134 --- /dev/null +++ b/internal/evmreader/inputsource_adapter.go @@ -0,0 +1,55 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "math/big" + + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// InputBox Wrapper +type InputSourceAdapter struct { + inputbox *iinputbox.IInputBox +} + +func NewInputSourceAdapter( + inputBoxAddress common.Address, + client *ethclient.Client, +) (*InputSourceAdapter, error) { + inputbox, err := iinputbox.NewIInputBox(inputBoxAddress, client) + if err != nil { + return nil, err + } + return &InputSourceAdapter{ + inputbox: inputbox, + }, nil +} + +func (i *InputSourceAdapter) RetrieveInputs( + opts *bind.FilterOpts, + appContract []common.Address, + index []*big.Int, +) ([]iinputbox.IInputBoxInputAdded, error) { + + itr, err := i.inputbox.FilterInputAdded(opts, appContract, index) + if err != nil { + return nil, err + } + defer itr.Close() + + var events []iinputbox.IInputBoxInputAdded + for itr.Next() { + inputAddedEvent := itr.Event + events = append(events, *inputAddedEvent) + } + if err = itr.Error(); err != nil { + return nil, err + } + return events, nil +} diff --git a/internal/evmreader/output.go b/internal/evmreader/output.go new file mode 100644 index 00000000..736d1ee2 --- /dev/null +++ b/internal/evmreader/output.go @@ -0,0 +1,120 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "bytes" + "context" + "log/slog" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" +) + +func (r *EvmReader) CheckForOutputExecution( + ctx context.Context, + apps []application, + mostRecentBlockNumber uint64, +) { + + appAddresses := appsToAddresses(apps) + + slog.Debug("evmreader: Checking for new Output Executed Events", "apps", appAddresses) + + for _, app := range apps { + + LastOutputCheck := app.LastOutputCheckBlock + + // Safeguard: Only check blocks starting from the block where the InputBox + // contract was deployed as Inputs can be added to that same block + if LastOutputCheck < r.inputBoxDeploymentBlock { + LastOutputCheck = r.inputBoxDeploymentBlock + } + + if mostRecentBlockNumber > LastOutputCheck { + + slog.Debug("evmreader: Checking output execution for application", + "app", app.ContractAddress, + "last output check block", LastOutputCheck, + "most recent block", mostRecentBlockNumber) + + r.readAndUpdateOutputs(ctx, app, LastOutputCheck, mostRecentBlockNumber) + + } else if mostRecentBlockNumber < LastOutputCheck { + slog.Warn( + "evmreader: Not reading output execution: most recent block is lower than the last processed one", //nolint:lll + "app", app.ContractAddress, + "last output check block", LastOutputCheck, + "most recent block", mostRecentBlockNumber, + ) + } else { + slog.Warn("evmreader: Not reading output execution: already checked the most recent blocks", + "app", app.ContractAddress, + "last output check block", LastOutputCheck, + "most recent block", mostRecentBlockNumber, + ) + } + } + +} + +func (r *EvmReader) readAndUpdateOutputs( + ctx context.Context, app application, lastOutputCheck, mostRecentBlockNumber uint64) { + + contract := app.ApplicationContract + + opts := &bind.FilterOpts{ + Start: lastOutputCheck + 1, + End: &mostRecentBlockNumber, + } + + outputExecutedEvents, err := contract.RetrieveOutputExecutionEvents(opts) + if err != nil { + slog.Error("evmreader: Error reading output events", "app", app.ContractAddress, "error", err) + return + } + + // Should we check the output hash?? + var executedOutputs []*Output + for _, event := range outputExecutedEvents { + + // Compare output to check it is the correct one + output, err := r.repository.GetOutput(ctx, app.ContractAddress, event.OutputIndex) + if err != nil { + slog.Error("evmreader: Error retrieving output", + "app", app.ContractAddress, "index", event.OutputIndex, "error", err) + return + } + + if output == nil { + slog.Warn("evmreader: Found OutputExecuted event but output does not exist in the database yet", + "app", app.ContractAddress, "index", event.OutputIndex) + return + } + + if !bytes.Equal(output.RawData, event.Output) { + slog.Debug("evmreader: Output mismatch", + "app", app.ContractAddress, "index", event.OutputIndex, + "actual", output.RawData, "event's", event.Output) + + slog.Error("evmreader: Output mismatch. Application is in an invalid state", + "app", app.ContractAddress, + "index", event.OutputIndex) + + return + } + + slog.Info("evmreader: Output executed", "app", app.ContractAddress, "index", event.OutputIndex) + output.TransactionHash = &event.Raw.TxHash + executedOutputs = append(executedOutputs, output) + } + + err = r.repository.UpdateOutputExecutionTransaction( + ctx, app.ContractAddress, executedOutputs, mostRecentBlockNumber) + if err != nil { + slog.Error("evmreader: Error storing output execution statuses", "app", app, "error", err) + } + +} diff --git a/internal/evmreader/output_test.go b/internal/evmreader/output_test.go new file mode 100644 index 00000000..7cb653bf --- /dev/null +++ b/internal/evmreader/output_test.go @@ -0,0 +1,621 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "context" + "errors" + "time" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + appcontract "github.com/ZzzzHui/espresso-reader/pkg/contracts/iapplication" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/mock" +) + +func (s *EvmReaderSuite) TestOutputExecution() { + + wsClient := FakeWSEhtClient{} + + //New EVM Reader + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x10, + DefaultBlockStatusLatest, + s.contractFactory, + false, + ) + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastOutputCheckBlock: 0x10, + }}, nil).Once() + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"), + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastOutputCheckBlock: 0x11, + }}, nil).Once() + + s.repository.Unset("UpdateOutputExecutionTransaction") + s.repository.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + obj := arguments.Get(2) + claims, ok := obj.([]*Output) + s.Require().True(ok) + s.Require().Equal(0, len(claims)) + + obj = arguments.Get(3) + lastOutputCheck, ok := obj.(uint64) + s.Require().True(ok) + s.Require().Equal(uint64(17), lastOutputCheck) + + }).Return(nil) + s.repository.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + obj := arguments.Get(2) + claims, ok := obj.([]*Output) + s.Require().True(ok) + s.Require().Equal(0, len(claims)) + + obj = arguments.Get(3) + lastOutputCheck, ok := obj.(uint64) + s.Require().True(ok) + s.Require().Equal(uint64(18), lastOutputCheck) + + }).Return(nil) + + //No Inputs + s.inputBox.Unset("RetrieveInputs") + s.inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header1, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + wsClient.fireNewHead(&header1) + time.Sleep(1 * time.Second) + + s.repository.AssertNumberOfCalls( + s.T(), + "UpdateOutputExecutionTransaction", + 2, + ) + +} + +func (s *EvmReaderSuite) TestReadOutputExecution() { + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + applicationContract := &MockApplicationContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewApplication") + contractFactory.On("NewApplication", + mock.Anything, + ).Return(applicationContract, nil) + + //New EVM Reader + wsClient := FakeWSEhtClient{} + evmReader := NewEvmReader( + s.client, + &wsClient, + s.inputBox, + s.repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Output Executed Events + outputExecution0 := &appcontract.IApplicationOutputExecuted{ + OutputIndex: 1, + Output: common.Hex2Bytes("AABBCCDDEE"), + Raw: types.Log{ + TxHash: common.HexToHash("0xdeadbeef"), + }, + } + + outputExecutionEvents := []*appcontract.IApplicationOutputExecuted{outputExecution0} + applicationContract.On("RetrieveOutputExecutionEvents", + mock.Anything, + ).Return(outputExecutionEvents, nil).Once() + + applicationContract.On("GetConsensus", + mock.Anything, + ).Return(common.HexToAddress("0xdeadbeef"), nil) + + // Prepare repository + s.repository.Unset("GetAllRunningApplications") + s.repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastOutputCheckBlock: 0x10, + }}, nil).Once() + + output := &Output{ + Index: 1, + RawData: common.Hex2Bytes("AABBCCDDEE"), + } + + s.repository.Unset("GetOutput") + s.repository.On("GetOutput", + mock.Anything, + mock.Anything, + mock.Anything).Return(output, nil) + + s.repository.Unset("UpdateOutputExecutionTransaction") + s.repository.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Run(func(arguments mock.Arguments) { + obj := arguments.Get(2) + outputs, ok := obj.([]*Output) + s.Require().True(ok) + s.Require().Equal(1, len(outputs)) + output := outputs[0] + s.Require().NotNil(output) + s.Require().Equal(uint64(1), output.Index) + s.Require().Equal(common.HexToHash("0xdeadbeef"), *output.TransactionHash) + + }).Return(nil) + + //No Inputs + s.inputBox.Unset("RetrieveInputs") + s.inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + s.client.Unset("HeaderByNumber") + s.client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(s.ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + s.repository.AssertNumberOfCalls( + s.T(), + "UpdateOutputExecutionTransaction", + 1, + ) + +} + +func (s *EvmReaderSuite) TestCheckOutputFails() { + s.Run("whenRetrieveOutputsFails", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + applicationContract := &MockApplicationContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewApplication") + contractFactory.On("NewApplication", + mock.Anything, + ).Return(applicationContract, nil) + + //New EVM Reader + client := newMockEthClient() + wsClient := FakeWSEhtClient{} + inputBox := newMockInputBox() + repository := newMockRepository() + evmReader := NewEvmReader( + client, + &wsClient, + inputBox, + repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + applicationContract.On("RetrieveOutputExecutionEvents", + mock.Anything, + ).Return([]*appcontract.IApplicationOutputExecuted{}, errors.New("No outputs for you")) + + applicationContract.On("GetConsensus", + mock.Anything, + ).Return(common.HexToAddress("0xdeadbeef"), nil) + + // Prepare repository + repository.Unset("GetAllRunningApplications") + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastOutputCheckBlock: 0x10, + }}, nil).Once() + + output := &Output{ + Index: 1, + RawData: common.Hex2Bytes("AABBCCDDEE"), + } + + repository.Unset("GetOutput") + repository.On("GetOutput", + mock.Anything, + mock.Anything, + mock.Anything).Return(output, nil) + + repository.Unset("UpdateOutputExecutionTransaction") + repository.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Return(nil) + + //No Inputs + inputBox.Unset("RetrieveInputs") + inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + client.Unset("HeaderByNumber") + client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + s.repository.AssertNumberOfCalls( + s.T(), + "UpdateOutputExecutionTransaction", + 0, + ) + + }) + + s.Run("whenGetOutputsFails", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + applicationContract := &MockApplicationContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewApplication") + contractFactory.On("NewApplication", + mock.Anything, + ).Return(applicationContract, nil) + + //New EVM Reader + client := newMockEthClient() + wsClient := FakeWSEhtClient{} + inputBox := newMockInputBox() + repository := newMockRepository() + evmReader := NewEvmReader( + client, + &wsClient, + inputBox, + repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Output Executed Events + outputExecution0 := &appcontract.IApplicationOutputExecuted{ + OutputIndex: 1, + Output: common.Hex2Bytes("AABBCCDDEE"), + Raw: types.Log{ + TxHash: common.HexToHash("0xdeadbeef"), + }, + } + + outputExecutionEvents := []*appcontract.IApplicationOutputExecuted{outputExecution0} + applicationContract.On("RetrieveOutputExecutionEvents", + mock.Anything, + ).Return(outputExecutionEvents, nil).Once() + + applicationContract.On("GetConsensus", + mock.Anything, + ).Return(common.HexToAddress("0xdeadbeef"), nil) + + // Prepare repository + repository.Unset("GetAllRunningApplications") + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastOutputCheckBlock: 0x10, + }}, nil).Once() + + repository.Unset("GetOutput") + repository.On("GetOutput", + mock.Anything, + mock.Anything, + mock.Anything).Return(nil, errors.New("no output for you")) + + repository.Unset("UpdateOutputExecutionTransaction") + repository.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Return(nil) + + //No Inputs + inputBox.Unset("RetrieveInputs") + inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + client.Unset("HeaderByNumber") + client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + repository.AssertNumberOfCalls( + s.T(), + "UpdateOutputExecutionTransaction", + 0, + ) + + }) + + s.Run("whenOutputMismatch", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appAddress := common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E") + + // Contract Factory + + applicationContract := &MockApplicationContract{} + + contractFactory := newEmvReaderContractFactory() + + contractFactory.Unset("NewApplication") + contractFactory.On("NewApplication", + mock.Anything, + ).Return(applicationContract, nil) + + //New EVM Reader + client := newMockEthClient() + wsClient := FakeWSEhtClient{} + inputBox := newMockInputBox() + repository := newMockRepository() + evmReader := NewEvmReader( + client, + &wsClient, + inputBox, + repository, + 0x00, + DefaultBlockStatusLatest, + contractFactory, + false, + ) + + // Prepare Output Executed Events + outputExecution0 := &appcontract.IApplicationOutputExecuted{ + OutputIndex: 1, + Output: common.Hex2Bytes("AABBCCDDEE"), + Raw: types.Log{ + TxHash: common.HexToHash("0xdeadbeef"), + }, + } + + outputExecutionEvents := []*appcontract.IApplicationOutputExecuted{outputExecution0} + applicationContract.On("RetrieveOutputExecutionEvents", + mock.Anything, + ).Return(outputExecutionEvents, nil).Once() + + applicationContract.On("GetConsensus", + mock.Anything, + ).Return(common.HexToAddress("0xdeadbeef"), nil) + + // Prepare repository + repository.Unset("GetAllRunningApplications") + repository.On( + "GetAllRunningApplications", + mock.Anything, + ).Return([]Application{{ + ContractAddress: appAddress, + IConsensusAddress: common.HexToAddress("0xdeadbeef"), + LastOutputCheckBlock: 0x10, + }}, nil).Once() + + output := &Output{ + Index: 1, + RawData: common.Hex2Bytes("FFBBCCDDEE"), + } + + repository.Unset("GetOutput") + repository.On("GetOutput", + mock.Anything, + mock.Anything, + mock.Anything).Return(output, nil) + + repository.Unset("UpdateOutputExecutionTransaction") + repository.On("UpdateOutputExecutionTransaction", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Once().Return(nil) + + //No Inputs + inputBox.Unset("RetrieveInputs") + inputBox.On("RetrieveInputs", + mock.Anything, + mock.Anything, + mock.Anything, + ).Return([]iinputbox.IInputBoxInputAdded{}, nil) + + // Prepare Client + client.Unset("HeaderByNumber") + client.On( + "HeaderByNumber", + mock.Anything, + mock.Anything, + ).Return(&header0, nil).Once() + + // Start service + ready := make(chan struct{}, 1) + errChannel := make(chan error, 1) + + go func() { + errChannel <- evmReader.Run(ctx, ready) + }() + + select { + case <-ready: + break + case err := <-errChannel: + s.FailNow("unexpected error signal", err) + } + + wsClient.fireNewHead(&header0) + time.Sleep(1 * time.Second) + + repository.AssertNumberOfCalls( + s.T(), + "UpdateOutputExecutionTransaction", + 0, + ) + + }) +} diff --git a/internal/evmreader/retrypolicy/contractfactory.go b/internal/evmreader/retrypolicy/contractfactory.go new file mode 100644 index 00000000..150b0699 --- /dev/null +++ b/internal/evmreader/retrypolicy/contractfactory.go @@ -0,0 +1,71 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) +package retrypolicy + +import ( + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// Builds contracts delegates that will +// use retry policy on contract methods calls +type EvmReaderContractFactory struct { + maxRetries uint64 + maxDelay time.Duration + ethClient *ethclient.Client + iConsensusCache map[common.Address]evmreader.ConsensusContract +} + +func NewEvmReaderContractFactory( + ethClient *ethclient.Client, + maxRetries uint64, + maxDelay time.Duration, + +) *EvmReaderContractFactory { + return &EvmReaderContractFactory{ + ethClient: ethClient, + maxRetries: maxRetries, + maxDelay: maxDelay, + iConsensusCache: make(map[common.Address]evmreader.ConsensusContract), + } +} + +func (f *EvmReaderContractFactory) NewApplication( + address common.Address, +) (evmreader.ApplicationContract, error) { + + // Building a contract does not fail due to network errors. + // No need to retry this operation + applicationContract, err := evmreader.NewApplicationContractAdapter(address, f.ethClient) + if err != nil { + return nil, err + } + + return NewApplicationWithRetryPolicy(applicationContract, f.maxRetries, f.maxDelay), nil + +} + +func (f *EvmReaderContractFactory) NewIConsensus( + address common.Address, +) (evmreader.ConsensusContract, error) { + + delegator, ok := f.iConsensusCache[address] + if !ok { + // Building a contract does not fail due to network errors. + // No need to retry this operation + consensus, err := evmreader.NewConsensusContractAdapter(address, f.ethClient) + if err != nil { + return nil, err + } + + delegator = NewConsensusWithRetryPolicy(consensus, f.maxRetries, f.maxDelay) + + f.iConsensusCache[address] = delegator + } + return delegator, nil + +} diff --git a/internal/evmreader/retrypolicy/retrypolicy_application_delegator.go b/internal/evmreader/retrypolicy/retrypolicy_application_delegator.go new file mode 100644 index 00000000..c79fddb3 --- /dev/null +++ b/internal/evmreader/retrypolicy/retrypolicy_application_delegator.go @@ -0,0 +1,54 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retrypolicy + +import ( + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/services/retry" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iapplication" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +type ApplicationRetryPolicyDelegator struct { + delegate evmreader.ApplicationContract + maxRetries uint64 + delayBetweenCalls time.Duration +} + +func NewApplicationWithRetryPolicy( + delegate evmreader.ApplicationContract, + maxRetries uint64, + delayBetweenCalls time.Duration, +) *ApplicationRetryPolicyDelegator { + return &ApplicationRetryPolicyDelegator{ + delegate: delegate, + maxRetries: maxRetries, + delayBetweenCalls: delayBetweenCalls, + } +} + +func (d *ApplicationRetryPolicyDelegator) GetConsensus(opts *bind.CallOpts, +) (common.Address, error) { + return retry.CallFunctionWithRetryPolicy(d.delegate.GetConsensus, + opts, + d.maxRetries, + d.delayBetweenCalls, + "Application::GetConsensus", + ) +} + +func (d *ApplicationRetryPolicyDelegator) RetrieveOutputExecutionEvents( + opts *bind.FilterOpts, +) ([]*iapplication.IApplicationOutputExecuted, error) { + return retry.CallFunctionWithRetryPolicy(d.delegate.RetrieveOutputExecutionEvents, + opts, + d.maxRetries, + d.delayBetweenCalls, + "Application::RetrieveOutputExecutionEvents", + ) +} diff --git a/internal/evmreader/retrypolicy/retrypolicy_consensus_delegator.go b/internal/evmreader/retrypolicy/retrypolicy_consensus_delegator.go new file mode 100644 index 00000000..58d31592 --- /dev/null +++ b/internal/evmreader/retrypolicy/retrypolicy_consensus_delegator.go @@ -0,0 +1,73 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retrypolicy + +import ( + "math/big" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/services/retry" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iconsensus" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +// A Consensus Delegator that +// calls GetEpochLength with the retry +// policy defined by util.RetryFunction +type ConsensusRetryPolicyDelegator struct { + delegate evmreader.ConsensusContract + maxRetries uint64 + delayBetweenCalls time.Duration +} + +func NewConsensusWithRetryPolicy( + delegate evmreader.ConsensusContract, + maxRetries uint64, + delayBetweenCalls time.Duration, +) *ConsensusRetryPolicyDelegator { + return &ConsensusRetryPolicyDelegator{ + delegate: delegate, + maxRetries: maxRetries, + delayBetweenCalls: delayBetweenCalls, + } +} + +func (d *ConsensusRetryPolicyDelegator) GetEpochLength( + opts *bind.CallOpts, +) (*big.Int, error) { + + return retry.CallFunctionWithRetryPolicy(d.delegate.GetEpochLength, + opts, + d.maxRetries, + d.delayBetweenCalls, + "Consensus::GetEpochLength", + ) + +} + +type retrieveClaimAcceptedEventsArgs struct { + opts *bind.FilterOpts + appAddresses []common.Address +} + +func (d *ConsensusRetryPolicyDelegator) RetrieveClaimAcceptanceEvents( + opts *bind.FilterOpts, + appAddresses []common.Address, +) ([]*iconsensus.IConsensusClaimAcceptance, error) { + return retry.CallFunctionWithRetryPolicy(d.retrieveClaimAcceptanceEvents, + retrieveClaimAcceptedEventsArgs{ + opts: opts, + appAddresses: appAddresses, + }, d.maxRetries, + d.delayBetweenCalls, + "Consensus::RetrieveClaimAcceptedEvents") +} + +func (d *ConsensusRetryPolicyDelegator) retrieveClaimAcceptanceEvents( + args retrieveClaimAcceptedEventsArgs) ([]*iconsensus.IConsensusClaimAcceptance, error) { + return d.delegate.RetrieveClaimAcceptanceEvents(args.opts, args.appAddresses) +} diff --git a/internal/evmreader/retrypolicy/retrypolicy_ethclient_delegator.go b/internal/evmreader/retrypolicy/retrypolicy_ethclient_delegator.go new file mode 100644 index 00000000..605422c1 --- /dev/null +++ b/internal/evmreader/retrypolicy/retrypolicy_ethclient_delegator.go @@ -0,0 +1,64 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retrypolicy + +import ( + "context" + "math/big" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/services/retry" + + "github.com/ethereum/go-ethereum/core/types" +) + +// A EthClient Delegator that +// calls HeaderByNumber with the retry +// policy defined by util.RetryFunction +type EthClientRetryPolicyDelegator struct { + delegate evmreader.EthClient + maxRetries uint64 + delayBetweenCalls time.Duration +} + +func NewEhtClientWithRetryPolicy( + delegate evmreader.EthClient, + maxRetries uint64, + delayBetweenCalls time.Duration, +) *EthClientRetryPolicyDelegator { + return &EthClientRetryPolicyDelegator{ + delegate: delegate, + maxRetries: maxRetries, + delayBetweenCalls: delayBetweenCalls, + } +} + +type headerByNumberArgs struct { + ctx context.Context + number *big.Int +} + +func (d *EthClientRetryPolicyDelegator) HeaderByNumber( + ctx context.Context, + number *big.Int, +) (*types.Header, error) { + + return retry.CallFunctionWithRetryPolicy(d.headerByNumber, + headerByNumberArgs{ + ctx: ctx, + number: number, + }, + d.maxRetries, + d.delayBetweenCalls, + "EthClient::HeaderByNumber", + ) + +} + +func (d *EthClientRetryPolicyDelegator) headerByNumber( + args headerByNumberArgs, +) (*types.Header, error) { + return d.delegate.HeaderByNumber(args.ctx, args.number) +} diff --git a/internal/evmreader/retrypolicy/retrypolicy_ethwsclient_delegator.go b/internal/evmreader/retrypolicy/retrypolicy_ethwsclient_delegator.go new file mode 100644 index 00000000..b4f1186f --- /dev/null +++ b/internal/evmreader/retrypolicy/retrypolicy_ethwsclient_delegator.go @@ -0,0 +1,61 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retrypolicy + +import ( + "context" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/services/retry" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/core/types" +) + +type EthWsClientRetryPolicyDelegator struct { + delegate evmreader.EthWsClient + maxRetries uint64 + delayBetweenCalls time.Duration +} + +func NewEthWsClientWithRetryPolicy( + delegate evmreader.EthWsClient, + maxRetries uint64, + delayBetweenCalls time.Duration, +) *EthWsClientRetryPolicyDelegator { + return &EthWsClientRetryPolicyDelegator{ + delegate: delegate, + maxRetries: maxRetries, + delayBetweenCalls: delayBetweenCalls, + } +} + +type subscribeNewHeadArgs struct { + ctx context.Context + ch chan<- *types.Header +} + +func (d *EthWsClientRetryPolicyDelegator) SubscribeNewHead( + ctx context.Context, + ch chan<- *types.Header, +) (ethereum.Subscription, error) { + + return retry.CallFunctionWithRetryPolicy( + d.subscribeNewHead, + subscribeNewHeadArgs{ + ctx: ctx, + ch: ch, + }, + d.maxRetries, + d.delayBetweenCalls, + "EthWSClient::SubscribeNewHead", + ) +} + +func (d *EthWsClientRetryPolicyDelegator) subscribeNewHead( + args subscribeNewHeadArgs, +) (ethereum.Subscription, error) { + return d.delegate.SubscribeNewHead(args.ctx, args.ch) +} diff --git a/internal/evmreader/retrypolicy/retrypolicy_inputsource_delegator.go b/internal/evmreader/retrypolicy/retrypolicy_inputsource_delegator.go new file mode 100644 index 00000000..68b1bc83 --- /dev/null +++ b/internal/evmreader/retrypolicy/retrypolicy_inputsource_delegator.go @@ -0,0 +1,67 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retrypolicy + +import ( + "math/big" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/services/retry" + "github.com/ZzzzHui/espresso-reader/pkg/contracts/iinputbox" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +type InputSourceWithRetryPolicyDelegator struct { + delegate evmreader.InputSource + maxRetries uint64 + delay time.Duration +} + +func NewInputSourceWithRetryPolicy( + delegate evmreader.InputSource, + maxRetries uint64, + delay time.Duration, +) *InputSourceWithRetryPolicyDelegator { + return &InputSourceWithRetryPolicyDelegator{ + delegate: delegate, + maxRetries: maxRetries, + delay: delay, + } +} + +type retrieveInputsArgs struct { + opts *bind.FilterOpts + appContract []common.Address + index []*big.Int +} + +func (d *InputSourceWithRetryPolicyDelegator) RetrieveInputs( + opts *bind.FilterOpts, + appContract []common.Address, + index []*big.Int, +) ([]iinputbox.IInputBoxInputAdded, error) { + return retry.CallFunctionWithRetryPolicy(d.retrieveInputs, + retrieveInputsArgs{ + opts: opts, + appContract: appContract, + index: index, + }, + d.maxRetries, + d.delay, + "InputSource::RetrieveInputs", + ) +} + +func (d *InputSourceWithRetryPolicyDelegator) retrieveInputs( + args retrieveInputsArgs, +) ([]iinputbox.IInputBoxInputAdded, error) { + return d.delegate.RetrieveInputs( + args.opts, + args.appContract, + args.index, + ) +} diff --git a/internal/evmreader/service/evmreader_service.go b/internal/evmreader/service/evmreader_service.go new file mode 100644 index 00000000..b06a64fa --- /dev/null +++ b/internal/evmreader/service/evmreader_service.go @@ -0,0 +1,87 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package service + +import ( + "context" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/evmreader" + "github.com/ZzzzHui/espresso-reader/internal/evmreader/retrypolicy" + "github.com/ZzzzHui/espresso-reader/internal/repository" + + "github.com/ethereum/go-ethereum/ethclient" +) + +// Service to manage InputReader lifecycle +type EvmReaderService struct { + blockchainHttpEndpoint string + blockchainWsEndpoint string + database *repository.Database + maxRetries uint64 + maxDelay time.Duration +} + +func NewEvmReaderService( + blockchainHttpEndpoint string, + blockchainWsEndpoint string, + database *repository.Database, + maxRetries uint64, + maxDelay time.Duration, +) *EvmReaderService { + return &EvmReaderService{ + blockchainHttpEndpoint: blockchainHttpEndpoint, + blockchainWsEndpoint: blockchainWsEndpoint, + database: database, + maxRetries: maxRetries, + maxDelay: maxDelay, + } +} + +func (s *EvmReaderService) Start( + ctx context.Context, + ready chan<- struct{}, +) error { + + client, err := ethclient.DialContext(ctx, s.blockchainHttpEndpoint) + if err != nil { + return err + } + defer client.Close() + + wsClient, err := ethclient.DialContext(ctx, s.blockchainWsEndpoint) + if err != nil { + return err + } + defer wsClient.Close() + + config, err := s.database.GetNodeConfig(ctx) + if err != nil { + return err + } + + inputSource, err := evmreader.NewInputSourceAdapter(config.InputBoxAddress, client) + if err != nil { + return err + } + + contractFactory := retrypolicy.NewEvmReaderContractFactory(client, s.maxRetries, s.maxDelay) + + reader := evmreader.NewEvmReader( + retrypolicy.NewEhtClientWithRetryPolicy(client, s.maxRetries, s.maxDelay), + retrypolicy.NewEthWsClientWithRetryPolicy(wsClient, s.maxRetries, s.maxDelay), + retrypolicy.NewInputSourceWithRetryPolicy(inputSource, s.maxRetries, s.maxDelay), + s.database, + config.InputBoxDeploymentBlock, + config.DefaultBlock, + contractFactory, + false, + ) + + return reader.Run(ctx, ready) +} + +func (s *EvmReaderService) String() string { + return "evmreader" +} diff --git a/internal/evmreader/testdata/header_0.json b/internal/evmreader/testdata/header_0.json new file mode 100644 index 00000000..a8095a11 --- /dev/null +++ b/internal/evmreader/testdata/header_0.json @@ -0,0 +1,14 @@ +{ + "number": "0x11", + "gasUsed": "0x11ddc", + "gasLimit": "0x1c9c380", + "extraData": "0x", + "timestamp": "0x6653e99a", + "difficulty": "0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "sha3Uncles":"0x0000000000000000000000000000000000000000000000000000000000000000", + "stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/internal/evmreader/testdata/header_1.json b/internal/evmreader/testdata/header_1.json new file mode 100644 index 00000000..6d003b60 --- /dev/null +++ b/internal/evmreader/testdata/header_1.json @@ -0,0 +1,14 @@ +{ + "number": "0x12", + "gasUsed": "0x11ddc", + "gasLimit": "0x1c9c380", + "extraData": "0x", + "timestamp": "0x6653e99b", + "difficulty": "0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "sha3Uncles":"0x0000000000000000000000000000000000000000000000000000000000000000", + "stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/internal/evmreader/testdata/header_2.json b/internal/evmreader/testdata/header_2.json new file mode 100644 index 00000000..ff16f48a --- /dev/null +++ b/internal/evmreader/testdata/header_2.json @@ -0,0 +1,14 @@ +{ + "number": "0x13", + "gasUsed": "0x11ddc", + "gasLimit": "0x1c9c380", + "extraData": "0x", + "timestamp": "0x6653e99c", + "difficulty": "0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "sha3Uncles":"0x0000000000000000000000000000000000000000000000000000000000000000", + "stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/internal/evmreader/testdata/input_added_event_0.json b/internal/evmreader/testdata/input_added_event_0.json new file mode 100644 index 00000000..3b24005b --- /dev/null +++ b/internal/evmreader/testdata/input_added_event_0.json @@ -0,0 +1,20 @@ +{ + "AppContract": "0x2e663fe9ae92275242406a185aa4fc8174339d3e", + "Index": 0, + "Input": "zH3uHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHppAAAAAAAAAAAAAAAAtyyDLd6hAyYUODHx5fFkaSDJyZAAAAAAAAAAAAAAAADzn9blGq2I9vTOariCcnnP/7kiZgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGZM7voAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATerb7vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Raw": { + "address": "0xa1b8eb1f13d8d5db976a653bbdf8972cfd14691c", + "topics": [ + "0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98", + "0x000000000000000000000000b72c832ddea10326143831f1e5f1646920c9c990", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000124cc7dee1f0000000000000000000000000000000000000000000000000000000000007a69000000000000000000000000b72c832ddea10326143831f1e5f1646920c9c990000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000664ceefa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004deadbeef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x11", + "transactionHash": "0xc405b17d1216fa348ac399df99703ec97cd77b03c9790cca8acddffe7ce4e901", + "transactionIndex": "0x0", + "blockHash": "0x03f50ead05dc4700559e7da32724e6bef31acbfd9b96d823203a8737432b6ad4", + "logIndex": "0x0", + "removed": false + } +} diff --git a/internal/evmreader/testdata/input_added_event_1.json b/internal/evmreader/testdata/input_added_event_1.json new file mode 100644 index 00000000..212d5d9c --- /dev/null +++ b/internal/evmreader/testdata/input_added_event_1.json @@ -0,0 +1,20 @@ +{ + "AppContract": "0x2e663fe9ae92275242406a185aa4fc8174339d3e", + "Index": 1, + "Input": "zH3uHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHppAAAAAAAAAAAAAAAAtyyDLd6hAyYUODHx5fFkaSDJyZAAAAAAAAAAAAAAAABwmXlwxRgS3DoBDH0BtQ4NF9x5yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGZM71kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+rb7vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Raw": { + "address": "0xa1b8eb1f13d8d5db976a653bbdf8972cfd14691c", + "topics": [ + "0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98", + "0x000000000000000000000000b72c832ddea10326143831f1e5f1646920c9c990", + "0x0000000000000000000000000000000000000000000000000000000000000001" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000124cc7dee1f0000000000000000000000000000000000000000000000000000000000007a69000000000000000000000000b72c832ddea10326143831f1e5f1646920c9c99000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000664cef59000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004beadbeef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x12", + "transactionHash": "0x2ae7f7e308ecd1c75c43f073cf15692ac460d7578cc618be242b2b89354a3eb4", + "transactionIndex": "0x0", + "blockHash": "0xcf47fc77c83197f02fb95ea2dd4ac9a681f8cc270151d72765074b876041b584", + "logIndex": "0x0", + "removed": false + } +} diff --git a/internal/evmreader/testdata/input_added_event_2.json b/internal/evmreader/testdata/input_added_event_2.json new file mode 100644 index 00000000..5fa3ba0b --- /dev/null +++ b/internal/evmreader/testdata/input_added_event_2.json @@ -0,0 +1,20 @@ +{ + "AppContract": "0x2e663fe9ae92275242406a185aa4fc8174339d3e", + "Index": 2, + "Input": "zH3uHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHppAAAAAAAAAAAAAAAALmY/6a6SJ1JCQGoYWqT8gXQznT4AAAAAAAAAAAAAAADzn9blGq2I9vTOariCcnnP/7kiZgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGZx6R8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATOrb7vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Raw": { + "address": "0xa1b8eb1f13d8d5db976a653bbdf8972cfd14691c", + "topics": [ + "0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98", + "0x0000000000000000000000002e663fe9ae92275242406a185aa4fc8174339d3e", + "0x0000000000000000000000000000000000000000000000000000000000000002" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000124cc7dee1f0000000000000000000000000000000000000000000000000000000000007a690000000000000000000000002e663fe9ae92275242406a185aa4fc8174339d3e000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000013000000000000000000000000000000000000000000000000000000006671e91f000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004ceadbeef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x13", + "transactionHash": "0xf936021c09a73cbd35ee4bc648e48a3524f2665d93b97c56510348098f32c48c", + "transactionIndex": "0x0", + "blockHash": "0x1bd76f02c7c57256a2025410ab902ad56942faed08df0cf812216e37f7cc9f97", + "logIndex": "0x0", + "removed": false + } +} diff --git a/internal/evmreader/testdata/input_added_event_3.json b/internal/evmreader/testdata/input_added_event_3.json new file mode 100644 index 00000000..fbf2a8a8 --- /dev/null +++ b/internal/evmreader/testdata/input_added_event_3.json @@ -0,0 +1,20 @@ +{ + "AppContract": "0x2e663fe9ae92275242406a185aa4fc8174339d3e", + "Index": 3, + "Input": "zH3uHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHppAAAAAAAAAAAAAAAALmY/6a6SJ1JCQGoYWqT8gXQznT4AAAAAAAAAAAAAAADzn9blGq2I9vTOariCcnnP/7kiZgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGZx6R8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT+rb7vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Raw": { + "address": "0xa1b8eb1f13d8d5db976a653bbdf8972cfd14691c", + "topics": [ + "0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98", + "0x0000000000000000000000002e663fe9ae92275242406a185aa4fc8174339d3e", + "0x0000000000000000000000000000000000000000000000000000000000000003" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000124cc7dee1f0000000000000000000000000000000000000000000000000000000000007a690000000000000000000000002e663fe9ae92275242406a185aa4fc8174339d3e000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000013000000000000000000000000000000000000000000000000000000006671e91f000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004feadbeef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x13", + "transactionHash": "0x9c100b1acf602f19798f699cb7c06e16949f39a6b0c99b8ea462f1f916ff0102", + "transactionIndex": "0x1", + "blockHash": "0x1bd76f02c7c57256a2025410ab902ad56942faed08df0cf812216e37f7cc9f97", + "logIndex": "0x1", + "removed": false + } +} diff --git a/internal/evmreader/util.go b/internal/evmreader/util.go new file mode 100644 index 00000000..0bc670c4 --- /dev/null +++ b/internal/evmreader/util.go @@ -0,0 +1,57 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package evmreader + +import ( + "cmp" + "slices" + + . "github.com/ZzzzHui/espresso-reader/internal/model" +) + +// CalculateEpochIndex calculates the epoch index given the input block number +// and epoch length +func CalculateEpochIndex(epochLength uint64, blockNumber uint64) uint64 { + return blockNumber / epochLength +} + +// appsToAddresses +func appsToAddresses(apps []application) []Address { + var addresses []Address + for _, app := range apps { + addresses = append(addresses, app.ContractAddress) + } + return addresses +} + +// sortByInputIndex is a compare function that orders Inputs +// by index field. It is intended to be used with `insertSorted`, see insertSorted() +func sortByInputIndex(a, b *Input) int { + return cmp.Compare(a.Index, b.Index) +} + +// insertSorted inserts the received input in the slice at the position defined +// by its index property. +func insertSorted[T any](compare func(a, b *T) int, slice []*T, item *T) []*T { + // Insert Sorted + i, _ := slices.BinarySearchFunc( + slice, + item, + compare) + return slices.Insert(slice, i, item) +} + +// Index applications given a key extractor function +func indexApps[K comparable]( + keyExtractor func(application) K, + apps []application, +) map[K][]application { + + result := make(map[K][]application) + for _, item := range apps { + key := keyExtractor(item) + result[key] = append(result[key], item) + } + return result +} diff --git a/internal/model/models.go b/internal/model/models.go new file mode 100644 index 00000000..ad543f44 --- /dev/null +++ b/internal/model/models.go @@ -0,0 +1,146 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package model + +import ( + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +const HashLength = common.HashLength + +type ( + Hash = common.Hash + Address = common.Address + Bytes = hexutil.Bytes + InputCompletionStatus string + ApplicationStatus string + DefaultBlock string + EpochStatus string +) + +const ( + InputStatusNone InputCompletionStatus = "NONE" + InputStatusAccepted InputCompletionStatus = "ACCEPTED" + InputStatusRejected InputCompletionStatus = "REJECTED" + InputStatusException InputCompletionStatus = "EXCEPTION" + InputStatusMachineHalted InputCompletionStatus = "MACHINE_HALTED" + InputStatusOutputsLimitExceeded InputCompletionStatus = "OUTPUTS_LIMIT_EXCEEDED" + InputStatusCycleLimitExceeded InputCompletionStatus = "CYCLE_LIMIT_EXCEEDED" + InputStatusTimeLimitExceeded InputCompletionStatus = "TIME_LIMIT_EXCEEDED" + InputStatusPayloadLengthLimitExceeded InputCompletionStatus = "PAYLOAD_LENGTH_LIMIT_EXCEEDED" +) + +const ( + ApplicationStatusRunning ApplicationStatus = "RUNNING" + ApplicationStatusNotRunning ApplicationStatus = "NOT RUNNING" +) + +const ( + DefaultBlockStatusLatest DefaultBlock = "LATEST" + DefaultBlockStatusFinalized DefaultBlock = "FINALIZED" + DefaultBlockStatusPending DefaultBlock = "PENDING" + DefaultBlockStatusSafe DefaultBlock = "SAFE" +) + +const ( + EpochStatusOpen EpochStatus = "OPEN" + EpochStatusClosed EpochStatus = "CLOSED" + EpochStatusProcessedAllInputs EpochStatus = "PROCESSED_ALL_INPUTS" + EpochStatusClaimComputed EpochStatus = "CLAIM_COMPUTED" + EpochStatusClaimSubmitted EpochStatus = "CLAIM_SUBMITTED" + EpochStatusClaimAccepted EpochStatus = "CLAIM_ACCEPTED" + EpochStatusClaimRejected EpochStatus = "CLAIM_REJECTED" +) + +type NodePersistentConfig struct { + DefaultBlock DefaultBlock + InputBoxDeploymentBlock uint64 + InputBoxAddress Address + ChainId uint64 +} + +type ExecutionParameters struct { + AdvanceIncCycles uint64 + AdvanceMaxCycles uint64 + InspectIncCycles uint64 + InspectMaxCycles uint64 + AdvanceIncDeadline time.Duration + AdvanceMaxDeadline time.Duration + InspectIncDeadline time.Duration + InspectMaxDeadline time.Duration + LoadDeadline time.Duration + StoreDeadline time.Duration + FastDeadline time.Duration + MaxConcurrentInspects int64 +} + +type MachineConfig struct { + AppAddress Address + SnapshotPath string + ProcessedInputs uint64 + ExecutionParameters +} + +type Application struct { + Id uint64 + ContractAddress Address + TemplateHash Hash + TemplateUri string + LastProcessedBlock uint64 + LastClaimCheckBlock uint64 + LastOutputCheckBlock uint64 + Status ApplicationStatus + IConsensusAddress Address +} + +type Epoch struct { + Id uint64 + Index uint64 + FirstBlock uint64 + LastBlock uint64 + ClaimHash *Hash + TransactionHash *Hash + Status EpochStatus + AppAddress Address +} + +type Input struct { + Id uint64 + Index uint64 + CompletionStatus InputCompletionStatus + RawData Bytes + BlockNumber uint64 + MachineHash *Hash + OutputsHash *Hash + AppAddress Address + EpochId uint64 + TransactionId Bytes +} + +type Output struct { + Id uint64 + Index uint64 + RawData Bytes + Hash *Hash + OutputHashesSiblings []Hash + InputId uint64 + TransactionHash *Hash +} + +type Report struct { + Id uint64 + Index uint64 + RawData Bytes + InputId uint64 +} + +type Snapshot struct { + Id uint64 + URI string + InputId uint64 + AppAddress Address +} diff --git a/internal/repository/base.go b/internal/repository/base.go new file mode 100644 index 00000000..5b8e2ef8 --- /dev/null +++ b/internal/repository/base.go @@ -0,0 +1,1333 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package repository + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sync" + "time" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/internal/repository/schema" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Database struct { + db *pgxpool.Pool +} + +var ( + ErrInsertRow = errors.New("unable to insert row") + ErrUpdateRow = errors.New("unable to update row") + ErrCopyFrom = errors.New("unable to COPY FROM") + + ErrBeginTx = errors.New("unable to begin transaction") + ErrCommitTx = errors.New("unable to commit transaction") +) + +func ValidateSchema(endpoint string) error { + + schema, err := schema.New(endpoint) + if err != nil { + return err + } + defer schema.Close() + + _, err = schema.ValidateVersion() + return err +} + +// FIXME: improve this +func ValidateSchemaWithRetry(endpoint string, maxRetries int, delay time.Duration) error { + var err error + for i := 0; i < maxRetries; i++ { + err = ValidateSchema(endpoint) + if err == nil { + return nil + } + time.Sleep(delay) + } + return fmt.Errorf("failed to validate schema after %d attempts: %w", maxRetries, err) +} + +func Connect( + ctx context.Context, + postgresEndpoint string, +) (*Database, error) { + var ( + pgError error + pgInstance *Database + pgOnce sync.Once + ) + + pgError = ValidateSchemaWithRetry(postgresEndpoint, 5, 3*time.Second) // FIXME: get from config + if pgError != nil { + return nil, fmt.Errorf("unable to validate database schema version: %w\n", pgError) + } + + pgOnce.Do(func() { + dbpool, err := pgxpool.New(ctx, postgresEndpoint) + if err != nil { + pgError = fmt.Errorf("unable to create connection pool: %w\n", err) + return + } + + pgInstance = &Database{dbpool} + }) + + return pgInstance, pgError +} + +func (pg *Database) GetDB() *pgxpool.Pool { + return pg.db +} + +func (pg *Database) Close() { + if pg != nil { + pg.db.Close() + } +} + +func (pg *Database) InsertNodeConfig( + ctx context.Context, + config *NodePersistentConfig, +) error { + query := ` + INSERT INTO node_config + (default_block, + input_box_deployment_block, + input_box_address, + chain_id) + SELECT + @defaultBlock, + @deploymentBlock, + @inputBoxAddress, + @chainId + WHERE NOT EXISTS (SELECT * FROM node_config)` + + args := pgx.NamedArgs{ + "defaultBlock": config.DefaultBlock, + "deploymentBlock": config.InputBoxDeploymentBlock, + "inputBoxAddress": config.InputBoxAddress, + "chainId": config.ChainId, + } + + _, err := pg.db.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("%w: %w", ErrInsertRow, err) + } + + return nil +} + +func (pg *Database) InsertApplication( + ctx context.Context, + app *Application, +) (uint64, error) { + query := ` + INSERT INTO application + (contract_address, + template_hash, + template_uri, + last_processed_block, + last_claim_check_block, + last_output_check_block, + status, + iconsensus_address) + VALUES + (@contractAddress, + @templateHash, + @templateUri, + @lastProcessedBlock, + @lastClaimCheckBlock, + @lastOutputCheckBlock, + @status, + @iConsensusAddress) + RETURNING + id + ` + args := pgx.NamedArgs{ + "contractAddress": app.ContractAddress, + "templateHash": app.TemplateHash, + "templateUri": app.TemplateUri, + "lastProcessedBlock": app.LastProcessedBlock, + "lastClaimCheckBlock": app.LastClaimCheckBlock, + "lastOutputCheckBlock": app.LastOutputCheckBlock, + "status": app.Status, + "iConsensusAddress": app.IConsensusAddress, + } + + execParametersQuery := ` + INSERT INTO execution_parameters + (application_id) + VALUES + (@applicationId) + ` + + tx, err := pg.db.Begin(ctx) + if err != nil { + return 0, errors.Join(ErrBeginTx, err) + } + + var id uint64 + err = tx.QueryRow(ctx, query, args).Scan(&id) + if err != nil { + return 0, errors.Join(ErrInsertRow, err, tx.Rollback(ctx)) + } + + args = pgx.NamedArgs{"applicationId": id} + _, err = tx.Exec(ctx, execParametersQuery, args) + if err != nil { + return 0, errors.Join(ErrInsertRow, err, tx.Rollback(ctx)) + } + + err = tx.Commit(ctx) + if err != nil { + return 0, errors.Join(ErrCommitTx, tx.Rollback(ctx)) + } + + return id, nil +} + +func (pg *Database) InsertEpoch( + ctx context.Context, + epoch *Epoch, +) (uint64, error) { + + var id uint64 + + query := ` + INSERT INTO epoch + (index, + first_block, + last_block, + transaction_hash, + claim_hash, + status, + application_address) + VALUES + (@index, + @firstBlock, + @lastBlock, + @transactionHash, + @claimHash, + @status, + @applicationAddress) + RETURNING + id` + + args := pgx.NamedArgs{ + "index": epoch.Index, + "firstBlock": epoch.FirstBlock, + "lastBlock": epoch.LastBlock, + "transactionHash": epoch.TransactionHash, + "claimHash": epoch.ClaimHash, + "status": epoch.Status, + "applicationAddress": epoch.AppAddress, + } + + err := pg.db.QueryRow(ctx, query, args).Scan(&id) + if err != nil { + return 0, fmt.Errorf("%w: %w", ErrInsertRow, err) + } + + return id, nil +} + +func (pg *Database) InsertInput( + ctx context.Context, + input *Input, +) (uint64, error) { + query := ` + INSERT INTO input + (index, + status, + raw_data, + block_number, + machine_hash, + outputs_hash, + application_address, + epoch_id) + VALUES + (@index, + @status, + @rawData, + @blockNumber, + @machineHash, + @outputsHash, + @applicationAddress, + @epochId) + RETURNING + id + ` + + args := pgx.NamedArgs{ + "index": input.Index, + "status": input.CompletionStatus, + "rawData": input.RawData, + "blockNumber": input.BlockNumber, + "machineHash": input.MachineHash, + "outputsHash": input.OutputsHash, + "applicationAddress": input.AppAddress, + "epochId": input.EpochId, + } + + var id uint64 + err := pg.db.QueryRow(ctx, query, args).Scan(&id) + if err != nil { + return 0, errors.Join(ErrInsertRow, err) + } + + return id, nil +} + +func (pg *Database) InsertOutput( + ctx context.Context, + output *Output, +) (uint64, error) { + query := ` + INSERT INTO output + (index, + raw_data, + hash, + output_hashes_siblings, + input_id, + transaction_hash) + VALUES + (@index, + @rawData, + @hash, + @outputHashesSiblings, + @inputId, + @transactionHash) + RETURNING + id + ` + + args := pgx.NamedArgs{ + "inputId": output.InputId, + "index": output.Index, + "rawData": output.RawData, + "hash": output.Hash, + "outputHashesSiblings": output.OutputHashesSiblings, + "transactionHash": output.TransactionHash, + } + + var id uint64 + err := pg.db.QueryRow(ctx, query, args).Scan(&id) + if err != nil { + return 0, errors.Join(ErrInsertRow, err) + } + + return id, nil +} + +func (pg *Database) InsertReport( + ctx context.Context, + report *Report, +) error { + query := ` + INSERT INTO report + (index, + raw_data, + input_id) + VALUES + (@index, + @rawData, + @inputId)` + + args := pgx.NamedArgs{ + "inputId": report.InputId, + "index": report.Index, + "rawData": report.RawData, + } + + _, err := pg.db.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("%w: %w", ErrInsertRow, err) + } + + return nil +} + +func (pg *Database) InsertSnapshot( + ctx context.Context, + snapshot *Snapshot, +) (id uint64, _ error) { + query := ` + INSERT INTO snapshot + (input_id, + application_address, + uri) + VALUES + (@inputId, + @appAddress, + @uri) + RETURNING + id + ` + + args := pgx.NamedArgs{ + "inputId": snapshot.InputId, + "appAddress": snapshot.AppAddress, + "uri": snapshot.URI, + } + + err := pg.db.QueryRow(ctx, query, args).Scan(&id) + if err != nil { + return 0, fmt.Errorf("%w: %w", ErrInsertRow, err) + } + + return id, nil +} + +func (pg *Database) GetNodeConfig( + ctx context.Context, +) (*NodePersistentConfig, error) { + var ( + defaultBlock DefaultBlock + deploymentBlock uint64 + inputBoxAddress Address + chainId uint64 + ) + + query := ` + SELECT + default_block, + input_box_deployment_block, + input_box_address, + chain_id + FROM + node_config` + + err := pg.db.QueryRow(ctx, query).Scan( + &defaultBlock, + &deploymentBlock, + &inputBoxAddress, + &chainId, + ) + if err != nil { + return nil, fmt.Errorf("GetNodeConfig QueryRow failed: %w\n", err) + } + + config := NodePersistentConfig{ + DefaultBlock: defaultBlock, + InputBoxDeploymentBlock: deploymentBlock, + InputBoxAddress: inputBoxAddress, + ChainId: chainId, + } + + return &config, nil +} + +func (pg *Database) GetApplication( + ctx context.Context, + appAddressKey Address, +) (*Application, error) { + var ( + id uint64 + contractAddress Address + templateHash Hash + templateUri string + lastProcessedBlock uint64 + lastClaimCheckBlock uint64 + lastOutputCheckBlock uint64 + status ApplicationStatus + iconsensusAddress Address + ) + + query := ` + SELECT + id, + contract_address, + template_hash, + template_uri, + last_processed_block, + last_claim_check_block, + last_output_check_block, + status, + iconsensus_address + FROM + application + WHERE + contract_address=@contractAddress` + + args := pgx.NamedArgs{ + "contractAddress": appAddressKey, + } + + err := pg.db.QueryRow(ctx, query, args).Scan( + &id, + &contractAddress, + &templateHash, + &templateUri, + &lastProcessedBlock, + &lastClaimCheckBlock, + &lastOutputCheckBlock, + &status, + &iconsensusAddress, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetApplication returned no rows", + "service", "repository", + "app", appAddressKey) + return nil, nil + } + return nil, fmt.Errorf("GetApplication QueryRow failed: %w\n", err) + } + + app := Application{ + Id: id, + ContractAddress: contractAddress, + TemplateHash: templateHash, + TemplateUri: templateUri, + LastProcessedBlock: lastProcessedBlock, + LastClaimCheckBlock: lastClaimCheckBlock, + LastOutputCheckBlock: lastOutputCheckBlock, + Status: status, + IConsensusAddress: iconsensusAddress, + } + + return &app, nil +} + +func (pg *Database) UpdateApplicationStatus( + ctx context.Context, + appAddressKey Address, + newStatus ApplicationStatus, +) error { + query := ` + UPDATE + application + SET + status = @newStatus + WHERE + contract_address = @contractAddress` + + args := pgx.NamedArgs{ + "contractAddress": appAddressKey, + "newStatus": newStatus, + } + + commandTag, err := pg.db.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("UpdateApplicationStatus Exec failed: %w", err) + } + + if commandTag.RowsAffected() == 0 { + slog.Debug("UpdateApplicationStatus affected no rows", + "service", "repository", + "app", appAddressKey) + return fmt.Errorf("no application found with contract address: %s", appAddressKey) + } + + return nil +} + +func (pg *Database) GetEpochs(ctx context.Context, application Address) ([]Epoch, error) { + query := ` + SELECT + id, + application_address, + index, + first_block, + last_block, + claim_hash, + transaction_hash, + status + FROM + epoch + WHERE + application_address=@appAddress + ORDER BY + index ASC` + + args := pgx.NamedArgs{ + "appAddress": application, + } + + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, fmt.Errorf("GetProcessedEpochs failed: %w", err) + } + + var ( + id, index, firstBlock, lastBlock uint64 + appAddress Address + claimHash, transactionHash *Hash + status string + results []Epoch + ) + + scans := []any{ + &id, &appAddress, &index, &firstBlock, &lastBlock, &claimHash, &transactionHash, &status, + } + _, err = pgx.ForEachRow(rows, scans, func() error { + epoch := Epoch{ + Id: id, + Index: index, + AppAddress: appAddress, + FirstBlock: firstBlock, + LastBlock: lastBlock, + ClaimHash: claimHash, + TransactionHash: transactionHash, + Status: EpochStatus(status), + } + results = append(results, epoch) + return nil + }) + if err != nil { + return nil, fmt.Errorf("GetProcessedEpochs failed: %w", err) + } + return results, nil +} + +func (pg *Database) GetEpoch( + ctx context.Context, + indexKey uint64, + appAddressKey Address, +) (*Epoch, error) { + var ( + id uint64 + index uint64 + firstBlock uint64 + lastBlock uint64 + transactionHash *Hash + claimHash *Hash + status EpochStatus + applicationAddress Address + ) + + query := ` + SELECT + id, + index, + first_block, + last_block, + transaction_hash, + claim_hash, + status, + application_address + FROM + epoch + WHERE + index=@index AND application_address=@appAddress` + + args := pgx.NamedArgs{ + "index": indexKey, + "appAddress": appAddressKey, + } + + err := pg.db.QueryRow(ctx, query, args).Scan( + &id, + &index, + &firstBlock, + &lastBlock, + &transactionHash, + &claimHash, + &status, + &applicationAddress, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetEpoch returned no rows", + "service", "repository", + "app", appAddressKey, + "epoch", indexKey) + return nil, nil + } + return nil, fmt.Errorf("GetEpoch QueryRow failed: %w\n", err) + } + + epoch := Epoch{ + Id: id, + Index: index, + FirstBlock: firstBlock, + LastBlock: lastBlock, + TransactionHash: transactionHash, + ClaimHash: claimHash, + Status: status, + AppAddress: applicationAddress, + } + + return &epoch, nil + +} + +func (pg *Database) GetInputs( + ctx context.Context, + app Address, +) ([]*Input, error) { + query := ` + SELECT id, index, status, raw_data, epoch_id + FROM input + WHERE application_address = @applicationAddress + ORDER BY index ASC + ` + args := pgx.NamedArgs{ + "applicationAddress": app, + } + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, fmt.Errorf("advancer repository error (failed querying inputs): %w", err) + } + + res := []*Input{} + var input Input + scans := []any{&input.Id, &input.Index, &input.CompletionStatus, &input.RawData, &input.EpochId} + _, err = pgx.ForEachRow(rows, scans, func() error { + input := input + res = append(res, &input) + return nil + }) + if err != nil { + return nil, fmt.Errorf("advancer repository error (failed reading input rows): %w", err) + } + + return res, nil +} + +func (pg *Database) GetInput( + ctx context.Context, + appAddressKey Address, + indexKey uint64, +) (*Input, error) { + var ( + id uint64 + index uint64 + status InputCompletionStatus + rawData []byte + blockNumber uint64 + machineHash *Hash + outputsHash *Hash + appAddress Address + epochId uint64 + ) + + query := ` + SELECT + id, + index, + raw_data, + status, + block_number, + machine_hash, + outputs_hash, + application_address, + epoch_id + FROM + input + WHERE + index=@index AND application_address=@appAddress` + + args := pgx.NamedArgs{ + "index": indexKey, + "appAddress": appAddressKey, + } + + err := pg.db.QueryRow(ctx, query, args).Scan( + &id, + &index, + &rawData, + &status, + &blockNumber, + &machineHash, + &outputsHash, + &appAddress, + &epochId, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetInput returned no rows", + "service", "repository", + "app", appAddressKey, + "index", indexKey) + return nil, nil + } + return nil, fmt.Errorf("GetInput QueryRow failed: %w\n", err) + } + + input := Input{ + Id: id, + Index: index, + CompletionStatus: status, + RawData: rawData, + BlockNumber: blockNumber, + MachineHash: machineHash, + OutputsHash: outputsHash, + AppAddress: appAddress, + EpochId: epochId, + } + + return &input, nil +} + +func (pg *Database) GetOutputs( + ctx context.Context, + application Address, +) ([]Output, error) { + query := ` + SELECT + o.id, + o.index, + o.raw_data, + o.hash, + o.output_hashes_siblings, + o.input_id + FROM + output o + INNER JOIN + input i + ON + o.input_id=i.id + WHERE + i.application_address=@appAddress + ORDER BY + o.index ASC + ` + + args := pgx.NamedArgs{"appAddress": application} + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, fmt.Errorf("GetOutputs failed: %w", err) + } + + var ( + id, index, inputId uint64 + rawData []byte + hash *Hash + outputHashesSiblings []Hash + outputs []Output + ) + scans := []any{&id, &index, &rawData, &hash, &outputHashesSiblings, &inputId} + _, err = pgx.ForEachRow(rows, scans, func() error { + output := Output{ + Id: id, + Index: index, + RawData: rawData, + Hash: hash, + OutputHashesSiblings: outputHashesSiblings, + InputId: inputId, + } + outputs = append(outputs, output) + return nil + }) + if err != nil { + return nil, fmt.Errorf("GetOutputs failed: %w", err) + } + return outputs, nil +} + +func (pg *Database) GetOutputsByInputIndex( + ctx context.Context, + application Address, + inputIndex uint64, +) ([]Output, error) { + query := ` + SELECT + o.id, + o.index, + o.raw_data, + o.hash, + o.output_hashes_siblings, + o.input_id + FROM + output o + INNER JOIN + input i + ON + o.input_id=i.id + WHERE + i.application_address=@appAddress + AND + i.index=@inputIndex + ORDER BY + o.index ASC + ` + + args := pgx.NamedArgs{ + "appAddress": application, + "inputIndex": inputIndex, + } + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, fmt.Errorf("GetOutputs failed: %w", err) + } + + var ( + id, index, inputId uint64 + rawData []byte + hash *Hash + outputHashesSiblings []Hash + outputs []Output + ) + scans := []any{&id, &index, &rawData, &hash, &outputHashesSiblings, &inputId} + _, err = pgx.ForEachRow(rows, scans, func() error { + output := Output{ + Id: id, + Index: index, + RawData: rawData, + Hash: hash, + OutputHashesSiblings: outputHashesSiblings, + InputId: inputId, + } + outputs = append(outputs, output) + return nil + }) + if err != nil { + return nil, fmt.Errorf("GetOutputs failed: %w", err) + } + return outputs, nil +} + +func (pg *Database) GetOutput( + ctx context.Context, + appAddressKey Address, + indexKey uint64, +) (*Output, error) { + var ( + id uint64 + index uint64 + rawData []byte + hash *Hash + outputHashesSiblings []Hash + inputId uint64 + transactionHash *Hash + ) + + query := ` + SELECT + o.id, + o.index, + o.raw_data, + o.hash, + o.output_hashes_siblings, + o.input_id, + o.transaction_hash + FROM + output o + INNER JOIN + input i + ON + o.input_id=i.id + WHERE + o.index=@index AND i.application_address=@appAddress` + + args := pgx.NamedArgs{ + "index": indexKey, + "appAddress": appAddressKey, + } + + err := pg.db.QueryRow(ctx, query, args).Scan( + &id, + &index, + &rawData, + &hash, + &outputHashesSiblings, + &inputId, + &transactionHash, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetOutput returned no rows", + "service", "repository", + "app", appAddressKey, + "index", indexKey) + return nil, nil + } + return nil, fmt.Errorf("GetOutput QueryRow failed: %w\n", err) + } + + output := Output{ + Id: id, + Index: index, + RawData: rawData, + Hash: hash, + OutputHashesSiblings: outputHashesSiblings, + InputId: inputId, + TransactionHash: transactionHash, + } + + return &output, nil +} + +func (pg *Database) GetReports( + ctx context.Context, + appAddressKey Address, +) ([]Report, error) { + var ( + id uint64 + index uint64 + rawData []byte + inputId uint64 + reports []Report + ) + query := ` + SELECT + r.id, + r.index, + r.raw_data, + r.input_id + FROM + report r + INNER JOIN + input i + ON + r.input_id=i.id + WHERE + i.application_address=@appAddress + ORDER BY + r.index ASC` + + args := pgx.NamedArgs{ + "appAddress": appAddressKey, + } + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, fmt.Errorf("GetReports failed: %w", err) + } + + scans := []any{&id, &index, &rawData, &inputId} + _, err = pgx.ForEachRow(rows, scans, func() error { + report := Report{ + Id: id, + Index: index, + RawData: rawData, + InputId: inputId, + } + + reports = append(reports, report) + return nil + }) + if err != nil { + return nil, fmt.Errorf("GetReports failed: %w", err) + } + + return reports, nil +} + +func (pg *Database) GetReportsByInputIndex( + ctx context.Context, + appAddressKey Address, + inputIndex uint64, +) ([]Report, error) { + var ( + id uint64 + index uint64 + rawData []byte + inputId uint64 + reports []Report + ) + query := ` + SELECT + r.id, + r.index, + r.raw_data, + r.input_id + FROM + report r + INNER JOIN + input i + ON + r.input_id=i.id + WHERE + i.application_address=@appAddress + AND + i.index=@inputIndex + ORDER BY + r.index ASC` + + args := pgx.NamedArgs{ + "appAddress": appAddressKey, + "inputIndex": inputIndex, + } + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, fmt.Errorf("GetReports failed: %w", err) + } + + scans := []any{&id, &index, &rawData, &inputId} + _, err = pgx.ForEachRow(rows, scans, func() error { + report := Report{ + Id: id, + Index: index, + RawData: rawData, + InputId: inputId, + } + + reports = append(reports, report) + return nil + }) + if err != nil { + return nil, fmt.Errorf("GetReports failed: %w", err) + } + + return reports, nil +} + +func (pg *Database) GetReport( + ctx context.Context, + appAddressKey Address, + indexKey uint64, +) (*Report, error) { + var ( + id uint64 + index uint64 + rawData []byte + inputId uint64 + ) + query := ` + SELECT + r.id, + r.index, + r.raw_data, + r.input_id + FROM + report r + INNER JOIN + input i + ON + r.input_id=i.id + WHERE + r.index=@index AND i.application_address=@appAddress` + + args := pgx.NamedArgs{ + "index": indexKey, + "appAddress": appAddressKey, + } + err := pg.db.QueryRow(ctx, query, args).Scan( + &id, + &index, + &rawData, + &inputId, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetReport returned no rows", + "service", "repository", + "app", appAddressKey, + "index", indexKey) + return nil, nil + } + return nil, fmt.Errorf("GetReport QueryRow failed: %w\n", err) + } + + report := Report{ + Id: id, + Index: index, + RawData: rawData, + InputId: inputId, + } + + return &report, nil +} + +func (pg *Database) GetSnapshot( + ctx context.Context, + inputIndexKey uint64, + appAddressKey Address, +) (*Snapshot, error) { + var ( + id uint64 + inputId uint64 + appAddress Address + uri string + ) + query := ` + SELECT + s.id, + s.input_id, + s.application_address, + s.uri + FROM + snapshot s + INNER JOIN + input i + ON + i.id = s.input_id + WHERE + s.application_address=@appAddress AND i.index=@inputIndex + ` + + args := pgx.NamedArgs{ + "inputIndex": inputIndexKey, + "appAddress": appAddressKey, + } + err := pg.db.QueryRow(ctx, query, args).Scan( + &id, + &inputId, + &appAddress, + &uri, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetSnapshot returned no rows", + "service", "repository", + "app", appAddressKey, + "input index", inputIndexKey) + return nil, nil + } + return nil, fmt.Errorf("GetSnapshot QueryRow failed: %w\n", err) + } + + snapshot := Snapshot{ + Id: id, + InputId: inputId, + AppAddress: appAddress, + URI: uri, + } + + return &snapshot, nil + +} + +func (pg *Database) GetEspressoNonce( + ctx context.Context, + senderAddress Address, + applicationAddress Address, +) (uint64, error) { + var ( + nonce uint64 + ) + query := ` + SELECT + nonce + FROM + espresso_nonce + WHERE + sender_address=@senderAddress AND application_address=@applicationAddress + ` + + args := pgx.NamedArgs{ + "senderAddress": senderAddress, + "applicationAddress": applicationAddress, + } + err := pg.db.QueryRow(ctx, query, args).Scan( + &nonce, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetEspressoNonce returned no rows", + "senderAddress", senderAddress, + "applicationAddress", applicationAddress) + return 0, nil + } + return 0, fmt.Errorf("GetEspressoNonce QueryRow failed: %w\n", err) + } + + return nonce, nil +} + +func (pg *Database) UpdateEspressoNonce( + ctx context.Context, + senderAddress Address, + applicationAddress Address, +) error { + nonce, err := pg.GetEspressoNonce(ctx, senderAddress, applicationAddress) + if err != nil { + return err + } + nextNonce := nonce + 1 + + query := ` + INSERT INTO espresso_nonce + (sender_address, + application_address, + nonce) + VALUES + (@senderAddress, + @applicationAddress, + @nextNonce) + ON CONFLICT (sender_address,application_address) + DO UPDATE + set nonce=@nextNonce + ` + + args := pgx.NamedArgs{ + "senderAddress": senderAddress, + "applicationAddress": applicationAddress, + "nextNonce": nextNonce, + } + _, err = pg.db.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("%w: %w", ErrUpdateRow, err) + } + + return nil +} + +func (pg *Database) GetInputIndex( + ctx context.Context, + applicationAddress Address, +) (uint64, error) { + var ( + index uint64 + ) + query := ` + SELECT + index + FROM + input_index + WHERE + application_address=@applicationAddress + ` + + args := pgx.NamedArgs{ + "applicationAddress": applicationAddress, + } + err := pg.db.QueryRow(ctx, query, args).Scan( + &index, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("GetInputIndex returned no rows", + "applicationAddress", applicationAddress) + return 0, nil + } + return 0, fmt.Errorf("GetInputIndex QueryRow failed: %w\n", err) + } + + return index, nil +} + +func (pg *Database) UpdateInputIndex( + ctx context.Context, + applicationAddress Address, +) error { + slog.Debug("Updating input index") + index, err := pg.GetInputIndex(ctx, applicationAddress) + if err != nil { + return err + } + nextIndex := index + 1 + + query := ` + INSERT INTO input_index + (application_address, + index) + VALUES + (@applicationAddress, + @nextIndex) + ON CONFLICT (application_address) + DO UPDATE + set index=@nextIndex + ` + + args := pgx.NamedArgs{ + "applicationAddress": applicationAddress, + "nextIndex": nextIndex, + } + _, err = pg.db.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("%w: %w", ErrUpdateRow, err) + } + + return nil +} diff --git a/internal/repository/base_test.go b/internal/repository/base_test.go new file mode 100644 index 00000000..bdbb45cd --- /dev/null +++ b/internal/repository/base_test.go @@ -0,0 +1,471 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package repository + +import ( + "context" + "math" + "testing" + "time" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/test/tooling/db" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" +) + +const testTimeout = 300 * time.Second + +// This suite sets up a container running a postgres database +type RepositorySuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + database *Database +} + +func (s *RepositorySuite) SetupSuite() { + s.ctx, s.cancel = context.WithTimeout(context.Background(), testTimeout) + + var err error + endpoint, err := db.GetPostgresTestEndpoint() + s.Require().Nil(err) + + err = db.SetupTestPostgres(endpoint) + s.Require().Nil(err) + + s.database, err = Connect(s.ctx, endpoint) + s.Require().Nil(err) + + s.SetupDatabase() +} + +func (s *RepositorySuite) TearDownSuite() { + s.cancel() +} + +func (s *RepositorySuite) SetupDatabase() { + config := NodePersistentConfig{ + DefaultBlock: DefaultBlockStatusFinalized, + InputBoxDeploymentBlock: 1, + InputBoxAddress: common.HexToAddress("deadbeef"), + ChainId: 1, + } + + err := s.database.InsertNodeConfig(s.ctx, &config) + s.Require().Nil(err) + + app := Application{ + ContractAddress: common.HexToAddress("deadbeef"), + IConsensusAddress: common.HexToAddress("ffffff"), + TemplateHash: common.HexToHash("deadbeef"), + TemplateUri: "path/to/template/uri/0", + LastProcessedBlock: 1, + Status: ApplicationStatusRunning, + } + + app2 := Application{ + ContractAddress: common.HexToAddress("feadbeef"), + IConsensusAddress: common.HexToAddress("ffffff"), + TemplateHash: common.HexToHash("deadbeef"), + TemplateUri: "path/to/template/uri/0", + LastProcessedBlock: 1, + Status: ApplicationStatusNotRunning, + } + + _, err = s.database.InsertApplication(s.ctx, &app) + s.Require().Nil(err) + + _, err = s.database.InsertApplication(s.ctx, &app2) + s.Require().Nil(err) + + genericHash := common.HexToHash("deadbeef") + + epoch1 := Epoch{ + Id: 1, + Index: 0, + FirstBlock: 0, + LastBlock: 99, + AppAddress: app.ContractAddress, + ClaimHash: nil, + TransactionHash: nil, + Status: EpochStatusOpen, + } + + _, err = s.database.InsertEpoch(s.ctx, &epoch1) + s.Require().Nil(err) + + epoch2 := Epoch{ + Id: 2, + Index: 1, + FirstBlock: 100, + LastBlock: 199, + AppAddress: app.ContractAddress, + ClaimHash: nil, + TransactionHash: nil, + Status: EpochStatusOpen, + } + + _, err = s.database.InsertEpoch(s.ctx, &epoch2) + s.Require().Nil(err) + + epoch3 := Epoch{ + Id: 3, + Index: 2, + FirstBlock: 200, + LastBlock: 299, + AppAddress: app.ContractAddress, + ClaimHash: nil, + TransactionHash: nil, + Status: EpochStatusClaimSubmitted, + } + + _, err = s.database.InsertEpoch(s.ctx, &epoch3) + s.Require().Nil(err) + + input1 := Input{ + Index: 1, + CompletionStatus: InputStatusAccepted, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 1, + MachineHash: &genericHash, + OutputsHash: &genericHash, + AppAddress: app.ContractAddress, + EpochId: 1, + } + + input1.Id, err = s.database.InsertInput(s.ctx, &input1) + s.Require().Nil(err) + + input2 := Input{ + Index: 2, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 3, + MachineHash: &genericHash, + OutputsHash: &genericHash, + AppAddress: app.ContractAddress, + EpochId: 1, + } + + input2.Id, err = s.database.InsertInput(s.ctx, &input2) + s.Require().Nil(err) + + input3 := Input{ + Index: 3, + CompletionStatus: InputStatusAccepted, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: (math.MaxUint64 / 2) + 1, + MachineHash: &genericHash, + OutputsHash: &genericHash, + AppAddress: app.ContractAddress, + EpochId: 2, + } + + input3.Id, err = s.database.InsertInput(s.ctx, &input3) + s.Require().Nil(err) + + var siblings []Hash + siblings = append(siblings, genericHash) + + output0 := Output{ + Index: 1, + InputId: 1, + RawData: common.Hex2Bytes("deadbeef"), + OutputHashesSiblings: siblings, + } + + output0.Id, err = s.database.InsertOutput(s.ctx, &output0) + s.Require().Nil(err) + + output1 := Output{ + Index: 2, + InputId: 1, + RawData: common.Hex2Bytes("deadbeef"), + OutputHashesSiblings: siblings, + } + + output1.Id, err = s.database.InsertOutput(s.ctx, &output1) + s.Require().Nil(err) + + output2 := Output{ + Index: 3, + InputId: 2, + RawData: common.Hex2Bytes("deadbeef"), + OutputHashesSiblings: siblings, + } + + output2.Id, err = s.database.InsertOutput(s.ctx, &output2) + s.Require().Nil(err) + + output3 := Output{ + Index: 4, + InputId: 3, + RawData: common.Hex2Bytes("deadbeef"), + OutputHashesSiblings: siblings, + } + + output3.Id, err = s.database.InsertOutput(s.ctx, &output3) + s.Require().Nil(err) + + report := Report{ + Index: 1, + InputId: 1, + RawData: common.Hex2Bytes("deadbeef"), + } + + err = s.database.InsertReport(s.ctx, &report) + s.Require().Nil(err) + + snapshot := Snapshot{ + InputId: 1, + AppAddress: app.ContractAddress, + URI: "/some/path", + } + + id, err := s.database.InsertSnapshot(s.ctx, &snapshot) + s.Require().Nil(err) + s.Require().Equal(uint64(1), id) +} + +func (s *RepositorySuite) TestApplicationExists() { + app := Application{ + Id: 1, + ContractAddress: common.HexToAddress("deadbeef"), + IConsensusAddress: common.HexToAddress("ffffff"), + TemplateHash: common.HexToHash("deadbeef"), + TemplateUri: "path/to/template/uri/0", + LastProcessedBlock: 1, + Status: ApplicationStatusRunning, + } + + response, err := s.database.GetApplication(s.ctx, common.HexToAddress("deadbeef")) + s.Require().Equal(&app, response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestApplicationDoesntExist() { + response, err := s.database.GetApplication(s.ctx, common.HexToAddress("deadbeefaaa")) + s.Require().Nil(response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestApplicationFailsDuplicateRow() { + app := Application{ + Id: 1, + ContractAddress: common.HexToAddress("deadbeef"), + IConsensusAddress: common.HexToAddress("ffffff"), + TemplateHash: common.HexToHash("deadbeef"), + TemplateUri: "path/to/template/uri/0", + LastProcessedBlock: 1, + Status: ApplicationStatusRunning, + } + + _, err := s.database.InsertApplication(s.ctx, &app) + s.Require().ErrorContains(err, "duplicate key value") +} + +func (s *RepositorySuite) TestInputExists() { + genericHash := common.HexToHash("deadbeef") + + input := Input{ + Id: 1, + Index: 1, + CompletionStatus: InputStatusAccepted, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 1, + MachineHash: &genericHash, + OutputsHash: &genericHash, + AppAddress: common.HexToAddress("deadbeef"), + EpochId: 1, + } + + response, err := s.database.GetInput(s.ctx, common.HexToAddress("deadbeef"), 1) + s.Require().Equal(&input, response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestInputDoesntExist() { + response, err := s.database.GetInput(s.ctx, common.HexToAddress("deadbeef"), 10) + s.Require().Nil(response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestInputFailsDuplicateRow() { + input := Input{ + Index: 1, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 1, + AppAddress: common.HexToAddress("deadbeef"), + } + + _, err := s.database.InsertInput(s.ctx, &input) + s.Require().ErrorContains(err, "duplicate key value") +} + +func (s *RepositorySuite) TestInputFailsApplicationDoesntExist() { + input := Input{ + Index: 3, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 3, + AppAddress: common.HexToAddress("deadbeefaaa"), + } + + _, err := s.database.InsertInput(s.ctx, &input) + s.Require().ErrorContains(err, "violates foreign key constraint") +} + +func (s *RepositorySuite) TestOutputExists() { + var siblings []Hash + siblings = append(siblings, common.HexToHash("deadbeef")) + + output := Output{ + Id: 1, + Index: 1, + InputId: 1, + RawData: common.Hex2Bytes("deadbeef"), + OutputHashesSiblings: siblings, + } + + response, err := s.database.GetOutput(s.ctx, common.HexToAddress("deadbeef"), 1) + s.Require().Equal(&output, response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestOutputDoesntExist() { + response, err := s.database.GetOutput(s.ctx, common.HexToAddress("deadbeef"), 10) + s.Require().Nil(response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestOutputFailsInputDoesntExist() { + output := Output{ + Index: 10, + InputId: 10, + RawData: common.Hex2Bytes("deadbeef"), + OutputHashesSiblings: nil, + } + + _, err := s.database.InsertOutput(s.ctx, &output) + s.Require().ErrorContains(err, "violates foreign key constraint") +} + +func (s *RepositorySuite) TestReportExists() { + report := Report{ + Id: 1, + Index: 1, + InputId: 1, + RawData: common.Hex2Bytes("deadbeef"), + } + + response, err := s.database.GetReport(s.ctx, common.HexToAddress("deadbeef"), 1) + s.Require().Equal(&report, response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestReportDoesntExist() { + response, err := s.database.GetReport(s.ctx, common.HexToAddress("deadbeef"), 10) + s.Require().Nil(response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestReportFailsInputDoesntExist() { + report := Report{ + Index: 2, + InputId: 10, + RawData: common.Hex2Bytes("deadbeef"), + } + + err := s.database.InsertReport(s.ctx, &report) + s.Require().ErrorContains(err, "violates foreign key constraint") +} + +func (s *RepositorySuite) TestEpochExists() { + + epoch := Epoch{ + Id: 1, + Status: EpochStatusOpen, + Index: 0, + FirstBlock: 0, + LastBlock: 99, + TransactionHash: nil, + ClaimHash: nil, + AppAddress: common.HexToAddress("deadbeef"), + } + + response, err := s.database.GetEpoch(s.ctx, 0, common.HexToAddress("deadbeef")) + s.Require().Equal(epoch, *response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestEpochDoesntExist() { + response, err := s.database.GetEpoch(s.ctx, 3, common.HexToAddress("deadbeef")) + s.Require().Nil(response) + s.Require().Nil(err) +} + +func (s *RepositorySuite) TestEpochFailsDuplicateRow() { + epoch := Epoch{ + Status: EpochStatusOpen, + Index: 0, + FirstBlock: 0, + LastBlock: math.MaxUint64, + TransactionHash: nil, + ClaimHash: nil, + AppAddress: common.HexToAddress("deadbeef"), + } + + _, err := s.database.InsertEpoch(s.ctx, &epoch) + s.Require().ErrorContains(err, "duplicate key value") +} + +func (s *RepositorySuite) TestEpochFailsApplicationDoesntExist() { + hash := common.HexToHash("deadbeef") + epoch := Epoch{ + Status: EpochStatusOpen, + Index: 2, + FirstBlock: 0, + LastBlock: math.MaxUint64, + ClaimHash: &hash, + AppAddress: common.HexToAddress("deadbeefaaa"), + } + + _, err := s.database.InsertEpoch(s.ctx, &epoch) + s.Require().ErrorContains(err, "violates foreign key constraint") +} + +func (s *RepositorySuite) TestGetSnapshot() { + + expectedSnapshot := Snapshot{ + Id: 1, + InputId: 1, + AppAddress: common.HexToAddress("deadbeef"), + URI: "/some/path", + } + + actualSnapshot, err := s.database.GetSnapshot(s.ctx, 1, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().NotNil(actualSnapshot) + s.Require().Equal(&expectedSnapshot, actualSnapshot) +} + +func (s *RepositorySuite) TestInsertSnapshotFailsSameInputId() { + + snapshot := Snapshot{ + InputId: 1, + AppAddress: common.HexToAddress("feadbeef"), + URI: "/some/path", + } + + _, err := s.database.InsertSnapshot(s.ctx, &snapshot) + s.Require().ErrorContains(err, "violates unique constraint") + +} + +func TestRepositorySuite(t *testing.T) { + suite.Run(t, new(RepositorySuite)) +} diff --git a/internal/repository/evmreader.go b/internal/repository/evmreader.go new file mode 100644 index 00000000..73417f92 --- /dev/null +++ b/internal/repository/evmreader.go @@ -0,0 +1,455 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package repository + +import ( + "context" + "errors" + "fmt" + + . "github.com/ZzzzHui/espresso-reader/internal/model" + + "github.com/jackc/pgx/v5" +) + +var ( + errInsertInputs = errors.New("unable to insert inputs") + errUpdateEpochs = errors.New("unable to update epochs status") + errGetEpochWithOpenClaims = errors.New("failed to get epochs with open claims") + errGetAllApplications = errors.New("failed to get Applications") +) + +// This method should be called at the end of EVMReader read input cycle +// In a single transaction it updates or inserts epochs, insert inputs related to each epoch +// and also updates the last processed block +func (pg *Database) StoreEpochAndInputsTransaction( + ctx context.Context, + epochInputsMap map[*Epoch][]Input, + blockNumber uint64, + contractAddress Address, +) (epochIndexIdMap map[uint64]uint64, epochIndexInputIdsMap map[uint64][]uint64, _ error) { + + insertEpochQuery := ` + INSERT INTO epoch + (application_address, + index, + first_block, + last_block, + status) + VALUES + (@appAddress, + @index, + @firstBlock, + @lastBlock, + @status) + ON CONFLICT (index,application_address) + DO UPDATE + set status=@status + RETURNING + id + ` + + insertInputQuery := ` + INSERT INTO input + (index, + status, + raw_data, + block_number, + application_address, + epoch_id, + transaction_id) + VALUES + (@index, + @status, + @rawData, + @blockNumber, + @appAddress, + @epochId, + @transactionId) + RETURNING + id` + + updateLastBlockQuery := ` + UPDATE application + SET + last_processed_block = @blockNumber + WHERE + contract_address=@contractAddress` + + tx, err := pg.db.Begin(ctx) + if err != nil { + return nil, nil, errors.Join(errInsertInputs, err) + } + + // structures to hold the ids + epochIndexIdMap = make(map[uint64]uint64) + epochIndexInputIdsMap = make(map[uint64][]uint64) + + for epoch, inputs := range epochInputsMap { + + // try to insert epoch + // Insert epoch + var epochId uint64 + insertEpochArgs := pgx.NamedArgs{ + "appAddress": epoch.AppAddress, + "index": epoch.Index, + "firstBlock": epoch.FirstBlock, + "lastBlock": epoch.LastBlock, + "status": epoch.Status, + "id": epoch.Id, + } + err := tx.QueryRow(ctx, insertEpochQuery, insertEpochArgs).Scan(&epochId) + + if err != nil { + return nil, nil, errors.Join(errInsertInputs, err, tx.Rollback(ctx)) + } + + epochIndexIdMap[epoch.Index] = epochId + + var inputId uint64 + + // Insert inputs + for _, input := range inputs { + inputArgs := pgx.NamedArgs{ + "index": input.Index, + "status": input.CompletionStatus, + "rawData": input.RawData, + "blockNumber": input.BlockNumber, + "appAddress": input.AppAddress, + "epochId": epochId, + "transactionId": input.TransactionId, + } + err = tx.QueryRow(ctx, insertInputQuery, inputArgs).Scan(&inputId) + if err != nil { + return nil, nil, errors.Join(errInsertInputs, err, tx.Rollback(ctx)) + } + epochIndexInputIdsMap[epoch.Index] = append(epochIndexInputIdsMap[epoch.Index], inputId) + } + } + + // Update last processed block + updateLastBlockArgs := pgx.NamedArgs{ + "blockNumber": blockNumber, + "contractAddress": contractAddress, + } + + _, err = tx.Exec(ctx, updateLastBlockQuery, updateLastBlockArgs) + if err != nil { + return nil, nil, errors.Join(errInsertInputs, err, tx.Rollback(ctx)) + } + + // Commit transaction + err = tx.Commit(ctx) + if err != nil { + return nil, nil, errors.Join(errInsertInputs, err, tx.Rollback(ctx)) + } + + return epochIndexIdMap, epochIndexInputIdsMap, nil +} + +// GetAllRunningApplications returns a slice with the applications being +// actively handled by the node. +func (pg *Database) GetAllRunningApplications( + ctx context.Context, +) ([]Application, error) { + criteria := ApplicationStatusRunning + return pg.getAllApplicationsByStatus(ctx, &criteria) +} + +func (pg *Database) GetAllApplications( + ctx context.Context, +) ([]Application, error) { + return pg.getAllApplicationsByStatus(ctx, nil) +} + +func (pg *Database) getAllApplicationsByStatus( + ctx context.Context, + criteria *ApplicationStatus, +) ([]Application, error) { + var ( + id uint64 + contractAddress Address + templateHash Hash + templateUri string + lastProcessedBlock uint64 + lastClaimCheckBlock uint64 + lastOutputCheckBlock uint64 + status ApplicationStatus + iConsensusAddress Address + results []Application + ) + + query := ` + SELECT + id, + contract_address, + template_hash, + template_uri, + last_processed_block, + last_claim_check_block, + last_output_check_block, + status, + iconsensus_address + FROM + application` + + var args []any + if criteria != nil { + query = query + " WHERE status=$1" + args = append(args, string(*criteria)) + } + + rows, err := pg.db.Query(ctx, query, args...) + if err != nil { + return nil, errors.Join(errGetAllApplications, err) + } + + _, err = pgx.ForEachRow(rows, + []any{&id, &contractAddress, &templateHash, &templateUri, + &lastProcessedBlock, &lastClaimCheckBlock, &lastOutputCheckBlock, + &status, &iConsensusAddress}, + func() error { + app := Application{ + Id: id, + ContractAddress: contractAddress, + TemplateHash: templateHash, + TemplateUri: templateUri, + LastProcessedBlock: lastProcessedBlock, + LastClaimCheckBlock: lastClaimCheckBlock, + LastOutputCheckBlock: lastOutputCheckBlock, + Status: status, + IConsensusAddress: iConsensusAddress, + } + results = append(results, app) + return nil + }) + if err != nil { + return nil, errors.Join(errGetAllApplications, err) + } + + return results, nil +} + +func (pg *Database) GetLastProcessedBlock( + ctx context.Context, + appAddress Address, +) (uint64, error) { + var block uint64 + + query := ` + SELECT + last_processed_block + FROM + application + WHERE + contract_address=@address` + + args := pgx.NamedArgs{ + "address": appAddress, + } + + err := pg.db.QueryRow(ctx, query, args).Scan(&block) + if err != nil { + return 0, fmt.Errorf("GetLastProcessedBlock failed: %w", err) + } + + return block, nil +} + +// GetPreviousEpochsWithOpenClaims retrieves all Epochs that have EpochStatusClaimSubmitted +// status and LastBlock less than 'block' +func (pg *Database) GetPreviousEpochsWithOpenClaims( + ctx context.Context, + app Address, + block uint64, +) ([]*Epoch, error) { + query := ` + SELECT + id, + application_address, + index, + first_block, + last_block, + claim_hash, + transaction_hash, + status + FROM + epoch + WHERE + application_address=@appAddress AND status=@status AND last_block < @block + ORDER BY + index ASC` + + args := pgx.NamedArgs{ + "appAddress": app, + "status": EpochStatusClaimSubmitted, + "block": block, + } + + rows, err := pg.db.Query(ctx, query, args) + if err != nil { + return nil, errors.Join(errGetEpochWithOpenClaims, err) + } + + var ( + id, index, firstBlock, lastBlock uint64 + appAddress Address + claimHash, transactionHash *Hash + status string + results []*Epoch + ) + + scans := []any{ + &id, &appAddress, &index, &firstBlock, &lastBlock, &claimHash, &transactionHash, &status, + } + _, err = pgx.ForEachRow(rows, scans, func() error { + epoch := &Epoch{ + Id: id, + Index: index, + AppAddress: appAddress, + FirstBlock: firstBlock, + LastBlock: lastBlock, + ClaimHash: claimHash, + TransactionHash: transactionHash, + Status: EpochStatus(status), + } + results = append(results, epoch) + return nil + }) + if err != nil { + return nil, errors.Join(errGetEpochWithOpenClaims, err) + } + return results, nil +} + +// UpdateEpochs update given Epochs status +// and given application LastClaimCheckBlockNumber on a single transaction +func (pg *Database) UpdateEpochs( + ctx context.Context, + app Address, + claims []*Epoch, + lastClaimCheckBlock uint64, +) error { + + updateEpochQuery := ` + UPDATE epoch + SET + status = @status + WHERE + id = @id` + + tx, err := pg.db.Begin(ctx) + if err != nil { + return errors.Join(errUpdateEpochs, err) + } + + for _, claim := range claims { + updateClaimArgs := pgx.NamedArgs{ + "status": claim.Status, + "id": claim.Id, + } + + tag, err := tx.Exec(ctx, updateEpochQuery, updateClaimArgs) + if err != nil { + return errors.Join(errUpdateEpochs, err, tx.Rollback(ctx)) + } + if tag.RowsAffected() != 1 { + return errors.Join(errUpdateEpochs, + fmt.Errorf("no row affected when updating claim %d", claim.Index), + tx.Rollback(ctx)) + } + } + + // Update last processed block + updateLastBlockQuery := ` + UPDATE application + SET + last_claim_check_block = @blockNumber + WHERE + contract_address=@contractAddress` + + updateLastBlockArgs := pgx.NamedArgs{ + "blockNumber": lastClaimCheckBlock, + "contractAddress": app, + } + + _, err = tx.Exec(ctx, updateLastBlockQuery, updateLastBlockArgs) + if err != nil { + return errors.Join(errUpdateEpochs, err, tx.Rollback(ctx)) + } + + // Commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Join(errUpdateEpochs, err, tx.Rollback(ctx)) + } + + return nil +} + +func (pg *Database) UpdateOutputExecutionTransaction( + ctx context.Context, + app Address, + executedOutputs []*Output, + blockNumber uint64, +) error { + + var errUpdateOutputs = errors.New("unable to update outputs") + + tx, err := pg.db.Begin(ctx) + if err != nil { + return errors.Join(errUpdateOutputs, err) + } + + updateOutputQuery := ` + UPDATE output + SET + transaction_hash = @hash + WHERE + id = @id + ` + + for _, output := range executedOutputs { + updateOutputArgs := pgx.NamedArgs{ + "hash": output.TransactionHash, + "id": output.Id, + } + + tag, err := tx.Exec(ctx, updateOutputQuery, updateOutputArgs) + if err != nil { + return errors.Join(errUpdateOutputs, err, tx.Rollback(ctx)) + } + if tag.RowsAffected() != 1 { + return errors.Join(errUpdateOutputs, + fmt.Errorf("no rows affected when updating output %d from app %s", + output.Index, app), + tx.Rollback(ctx)) + } + } + + // Update last processed block + updateLastBlockQuery := ` + UPDATE application + SET last_output_check_block = @blockNumber + WHERE + contract_address=@contractAddress` + + updateLastBlockArgs := pgx.NamedArgs{ + "blockNumber": blockNumber, + "contractAddress": app, + } + + _, err = tx.Exec(ctx, updateLastBlockQuery, updateLastBlockArgs) + if err != nil { + return errors.Join(errUpdateOutputs, err, tx.Rollback(ctx)) + } + + // Commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Join(errUpdateOutputs, err, tx.Rollback(ctx)) + } + + return nil +} diff --git a/internal/repository/evmreader_test.go b/internal/repository/evmreader_test.go new file mode 100644 index 00000000..e50c867c --- /dev/null +++ b/internal/repository/evmreader_test.go @@ -0,0 +1,232 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package repository + +import ( + . "github.com/ZzzzHui/espresso-reader/internal/model" + + "github.com/ethereum/go-ethereum/common" +) + +func (s *RepositorySuite) TestInsertInputsAndUpdateLastProcessedBlock() { + + epoch0 := Epoch{ + Index: 0, + FirstBlock: 0, + LastBlock: 9, + AppAddress: common.HexToAddress("deadbeef"), + Status: EpochStatusOpen, + } + + input0 := Input{ + Index: 5, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 5, + AppAddress: common.HexToAddress("deadbeef"), + } + + input1 := Input{ + Index: 6, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 6, + AppAddress: common.HexToAddress("deadbeef"), + } + + epochInputMap := make(map[*Epoch][]Input) + + epochInputMap[&epoch0] = []Input{input0, input1} + + epochIdMap, epochInputIdMap, err := s.database.StoreEpochAndInputsTransaction( + s.ctx, + epochInputMap, + 6, + common.HexToAddress("deadbeef"), + ) + s.Require().Nil(err) + s.Require().Len(epochIdMap, 1) + s.Require().Len(epochInputIdMap[0], 2) + + input0.Id = epochInputIdMap[0][0] + input0.EpochId = epochIdMap[0] + input1.Id = epochInputIdMap[0][1] + input1.EpochId = epochIdMap[0] + + response, err := s.database.GetInput(s.ctx, common.HexToAddress("deadbeef"), 5) + s.Require().Nil(err) + s.Require().Equal(&input0, response) + + var mostRecentCheck uint64 = 6 + response2, err := s.database.GetLastProcessedBlock(s.ctx, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().Equal(mostRecentCheck, response2) +} + +func (s *RepositorySuite) TestInsertInputsAndUpdateMostRecentFinalizedBlockEmptyInputs() { + _, _, err := s.database.StoreEpochAndInputsTransaction( + s.ctx, + nil, + 7, + common.HexToAddress("deadbeef"), + ) + s.Require().Nil(err) + + var block uint64 = 7 + response, err := s.database.GetLastProcessedBlock(s.ctx, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().Equal(block, response) +} + +func (s *RepositorySuite) TestInsertInputsAndUpdateLastProcessedBlockInputAlreadyExists() { + + epoch, err := s.database.GetEpoch(s.ctx, 0, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + + input := Input{ + Index: 5, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 5, + AppAddress: common.HexToAddress("deadbeef"), + EpochId: 1, + } + + epochInputMap := make(map[*Epoch][]Input) + epochInputMap[epoch] = []Input{input} + _, _, err = s.database.StoreEpochAndInputsTransaction( + s.ctx, + epochInputMap, + 8, + common.HexToAddress("deadbeef"), + ) + s.Require().ErrorContains(err, "duplicate key value violates unique constraint") +} + +func (s *RepositorySuite) TestInsertInputsAndUpdateLastProcessedBlockDuplicateInput() { + + epoch, err := s.database.GetEpoch(s.ctx, 0, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + + input0 := Input{ + Index: 7, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 7, + AppAddress: common.HexToAddress("deadbeef"), + EpochId: 1, + } + + input1 := Input{ + Index: 7, + CompletionStatus: InputStatusNone, + RawData: common.Hex2Bytes("deadbeef"), + BlockNumber: 7, + AppAddress: common.HexToAddress("deadbeef"), + EpochId: 1, + } + epochInputMap := make(map[*Epoch][]Input) + epochInputMap[epoch] = []Input{input0, input1} + _, _, err = s.database.StoreEpochAndInputsTransaction( + s.ctx, + epochInputMap, + 8, + common.HexToAddress("deadbeef"), + ) + s.Require().ErrorContains(err, "duplicate key value violates unique constraint") +} + +func (s *RepositorySuite) TestGetAllRunningApplications() { + app := Application{ + Id: 1, + ContractAddress: common.HexToAddress("deadbeef"), + TemplateHash: common.HexToHash("deadbeef"), + TemplateUri: "path/to/template/uri/0", + LastProcessedBlock: 1, + Status: ApplicationStatusRunning, + IConsensusAddress: common.HexToAddress("ffffff"), + } + + response, err := s.database.GetAllRunningApplications(s.ctx) + s.Require().Nil(err) + s.Require().Equal(app, response[0]) +} + +func (s *RepositorySuite) TestGetMostRecentBlock() { + var block uint64 = 1 + + response, err := s.database.GetLastProcessedBlock(s.ctx, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + + s.Require().Equal(block, response) +} + +func (s *RepositorySuite) TestGetPreviousEpochsWithOpenClaims() { + response, err := s.database.GetPreviousEpochsWithOpenClaims( + s.ctx, common.HexToAddress("deadbeef"), 300) + + s.Require().Nil(err) + s.Require().NotNil(response) + s.Require().Equal(1, len(response)) + + epoch, err := s.database.GetEpoch(s.ctx, 2, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + + s.Require().Equal(epoch, response[0]) +} + +func (s *RepositorySuite) TestUpdateEpochs() { + + claim, err := s.database.GetEpoch(s.ctx, 2, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().NotNil(claim) + + s.Require().Equal(EpochStatusClaimSubmitted, claim.Status) + claim.Status = EpochStatusClaimAccepted + + claims := []*Epoch{claim} + + err = s.database.UpdateEpochs( + s.ctx, + common.HexToAddress("deadbeef"), + claims, + 499, + ) + s.Require().Nil(err) + + claim, err = s.database.GetEpoch(s.ctx, 2, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().NotNil(claim) + s.Require().Equal(EpochStatusClaimAccepted, claim.Status) + + application, err := s.database.GetApplication(s.ctx, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().Equal(uint64(499), application.LastClaimCheckBlock) + +} + +func (s *RepositorySuite) TestUpdateOutputExecutionTransaction() { + output, err := s.database.GetOutput(s.ctx, common.HexToAddress("deadbeef"), 1) + s.Require().Nil(err) + s.Require().NotNil(output) + + var executedOutputs []*Output + hash := common.HexToHash("0xAABBCCDD") + output.TransactionHash = &hash + + executedOutputs = append(executedOutputs, output) + + err = s.database.UpdateOutputExecutionTransaction( + s.ctx, common.HexToAddress("deadbeef"), executedOutputs, 854758) + s.Require().Nil(err) + + actualOutput, err := s.database.GetOutput(s.ctx, common.HexToAddress("deadbeef"), 1) + s.Require().Nil(err) + s.Require().Equal(output, actualOutput) + + application, err := s.database.GetApplication(s.ctx, common.HexToAddress("deadbeef")) + s.Require().Nil(err) + s.Require().Equal(uint64(854758), application.LastOutputCheckBlock) + +} diff --git a/internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.down.sql b/internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.down.sql new file mode 100644 index 00000000..52e644c8 --- /dev/null +++ b/internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.down.sql @@ -0,0 +1,18 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +DROP TABLE IF EXISTS "node_config"; +DROP TABLE IF EXISTS "snapshot"; +DROP TABLE IF EXISTS "report"; +DROP TABLE IF EXISTS "output"; +DROP TABLE IF EXISTS "input"; +DROP TABLE IF EXISTS "epoch"; +DROP TABLE IF EXISTS "execution_parameters"; +DROP TABLE IF EXISTS "application"; + +DROP FUNCTION IF EXISTS "f_maxuint64"; + +DROP TYPE IF EXISTS "InputCompletionStatus"; +DROP TYPE IF EXISTS "ApplicationStatus"; +DROP TYPE IF EXISTS "DefaultBlock"; +DROP TYPE IF EXISTS "EpochStatus"; diff --git a/internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.up.sql b/internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.up.sql new file mode 100644 index 00000000..f21bf39b --- /dev/null +++ b/internal/repository/schema/migrations/000001_create_application_input_claim_output_report_nodeconfig.up.sql @@ -0,0 +1,201 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +CREATE TYPE "ApplicationStatus" AS ENUM ('RUNNING', 'NOT RUNNING'); + +CREATE TYPE "InputCompletionStatus" AS ENUM ( + 'NONE', + 'ACCEPTED', + 'REJECTED', + 'EXCEPTION', + 'MACHINE_HALTED', + 'CYCLE_LIMIT_EXCEEDED', + 'TIME_LIMIT_EXCEEDED', + 'PAYLOAD_LENGTH_LIMIT_EXCEEDED'); + +CREATE TYPE "DefaultBlock" AS ENUM ('FINALIZED', 'LATEST', 'PENDING', 'SAFE'); + +CREATE TYPE "EpochStatus" AS ENUM ( + 'OPEN', + 'CLOSED', + 'PROCESSED_ALL_INPUTS', + 'CLAIM_COMPUTED', + 'CLAIM_SUBMITTED', + 'CLAIM_ACCEPTED', + 'CLAIM_REJECTED'); + +CREATE FUNCTION "f_maxuint64"() + RETURNS NUMERIC(20,0) + LANGUAGE sql IMMUTABLE PARALLEL SAFE AS + 'SELECT 18446744073709551615'; + +CREATE TABLE "application" +( + "id" SERIAL, + "contract_address" BYTEA NOT NULL, + "template_hash" BYTEA NOT NULL, + "template_uri" VARCHAR(4096) NOT NULL, + "last_processed_block" NUMERIC(20,0) NOT NULL CHECK ("last_processed_block" >= 0 AND "last_processed_block" <= f_maxuint64()), + "last_processed_espresso_block" NUMERIC(20,0) NOT NULL CHECK ("last_processed_espresso_block" >= 0 AND "last_processed_espresso_block" <= f_maxuint64()), + "status" "ApplicationStatus" NOT NULL, + "iconsensus_address" BYTEA NOT NULL, + "last_claim_check_block" NUMERIC(20,0) NOT NULL CHECK ("last_claim_check_block" >= 0 AND "last_claim_check_block" <= f_maxuint64()), + "last_output_check_block" NUMERIC(20,0) NOT NULL CHECK ("last_output_check_block" >= 0 AND "last_output_check_block" <= f_maxuint64()), + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, -- updated_at is used by high level graphql + CONSTRAINT "application_pkey" PRIMARY KEY ("id"), + UNIQUE("contract_address") +); + + +CREATE TABLE "execution_parameters" ( + "application_id" INT PRIMARY KEY, + "advance_inc_cycles" BIGINT NOT NULL CHECK ("advance_inc_cycles" > 0) DEFAULT 4194304, -- 1 << 22 + "advance_max_cycles" BIGINT NOT NULL CHECK ("advance_max_cycles" > 0) DEFAULT 4611686018427387903, -- uint64 max >> 2 + "inspect_inc_cycles" BIGINT NOT NULL CHECK ("inspect_inc_cycles" > 0) DEFAULT 4194304, -- 1 << 22 + "inspect_max_cycles" BIGINT NOT NULL CHECK ("inspect_max_cycles" > 0) DEFAULT 4611686018427387903, + "advance_inc_deadline" BIGINT NOT NULL CHECK ("advance_inc_deadline" > 0) DEFAULT 10000000000, -- 10s + "advance_max_deadline" BIGINT NOT NULL CHECK ("advance_max_deadline" > 0) DEFAULT 180000000000, -- 180s + "inspect_inc_deadline" BIGINT NOT NULL CHECK ("inspect_inc_deadline" > 0) DEFAULT 10000000000, --10s + "inspect_max_deadline" BIGINT NOT NULL CHECK ("inspect_max_deadline" > 0) DEFAULT 180000000000, -- 180s + "load_deadline" BIGINT NOT NULL CHECK ("load_deadline" > 0) DEFAULT 300000000000, -- 300s + "store_deadline" BIGINT NOT NULL CHECK ("store_deadline" > 0) DEFAULT 180000000000, -- 180s + "fast_deadline" BIGINT NOT NULL CHECK ("fast_deadline" > 0) DEFAULT 5000000000, -- 5s + "max_concurrent_inspects" INT NOT NULL CHECK ("max_concurrent_inspects" > 0) DEFAULT 10, + CONSTRAINT "application_id_fkey" FOREIGN KEY ("application_id") REFERENCES "application"("id") +); + +CREATE TABLE "epoch" +( + "id" BIGSERIAL, + "application_address" BYTEA NOT NULL, + "index" BIGINT NOT NULL, + "first_block" NUMERIC(20,0) NOT NULL CHECK ("first_block" >= 0 AND "first_block" <= f_maxuint64()), + "last_block" NUMERIC(20,0) NOT NULL CHECK ("last_block" >= 0 AND "last_block" <= f_maxuint64()), + "claim_hash" BYTEA, + "transaction_hash" BYTEA, + "status" "EpochStatus" NOT NULL, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, -- updated_at is used by high level graphql + CONSTRAINT "epoch_pkey" PRIMARY KEY ("id"), + CONSTRAINT "epoch_application_address_fkey" FOREIGN KEY ("application_address") REFERENCES "application"("contract_address"), + UNIQUE ("index","application_address") +); + +CREATE INDEX "epoch_idx" ON "epoch"("index"); +CREATE INDEX "epoch_last_block_idx" ON "epoch"("last_block"); + +CREATE TABLE "input" +( + "id" BIGSERIAL, + "index" NUMERIC(20,0) NOT NULL CHECK ("index" >= 0 AND "index" <= f_maxuint64()), + "raw_data" BYTEA NOT NULL, + "block_number" NUMERIC(20,0) NOT NULL CHECK ("block_number" >= 0 AND "block_number" <= f_maxuint64()), + "status" "InputCompletionStatus" NOT NULL, + "machine_hash" BYTEA, + "outputs_hash" BYTEA, + "application_address" BYTEA NOT NULL, + "epoch_id" BIGINT NOT NULL, + "transaction_id" BYTEA, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, -- updated_at is used by high level graphql + CONSTRAINT "input_pkey" PRIMARY KEY ("id"), + CONSTRAINT "input_application_address_fkey" FOREIGN KEY ("application_address") REFERENCES "application"("contract_address"), + CONSTRAINT "input_epoch_fkey" FOREIGN KEY ("epoch_id") REFERENCES "epoch"("id"), + UNIQUE("index", "application_address") +); + +CREATE INDEX "input_idx" ON "input"("block_number"); + +CREATE TABLE "output" +( + "id" BIGSERIAL, + "index" NUMERIC(20,0) NOT NULL CHECK ("index" >= 0 AND "index" <= f_maxuint64()), + "raw_data" BYTEA NOT NULL, + "hash" BYTEA, + "output_hashes_siblings" BYTEA[], + "input_id" BIGINT NOT NULL, + "transaction_hash" BYTEA, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, -- updated_at is used by high level graphql + CONSTRAINT "output_pkey" PRIMARY KEY ("id"), + CONSTRAINT "output_input_id_fkey" FOREIGN KEY ("input_id") REFERENCES "input"("id") +); + +CREATE INDEX "output_idx" ON "output"("index"); + +CREATE TABLE "report" +( + "id" BIGSERIAL, + "index" NUMERIC(20,0) NOT NULL CHECK ("index" >= 0 AND "index" <= f_maxuint64()), + "raw_data" BYTEA NOT NULL, + "input_id" BIGINT NOT NULL, + CONSTRAINT "report_pkey" PRIMARY KEY ("id"), + CONSTRAINT "report_input_id_fkey" FOREIGN KEY ("input_id") REFERENCES "input"("id") +); + +CREATE INDEX "report_idx" ON "report"("index"); + +CREATE TABLE "snapshot" +( + "id" BIGSERIAL, + "input_id" BIGINT NOT NULL, + "application_address" BYTEA NOT NULL, + "uri" VARCHAR(4096) NOT NULL, + CONSTRAINT "snapshot_pkey" PRIMARY KEY ("id"), + CONSTRAINT "snapshot_input_id_fkey" FOREIGN KEY ("input_id") REFERENCES "input"("id"), + CONSTRAINT "snapshot_application_address_fkey" FOREIGN KEY ("application_address") REFERENCES "application"("contract_address"), + UNIQUE("input_id") +); + +CREATE TABLE "node_config" +( + "default_block" "DefaultBlock" NOT NULL, + "input_box_deployment_block" INT NOT NULL, + "input_box_address" BYTEA NOT NULL, + "chain_id" INT NOT NULL +); + +CREATE TABLE "espresso_nonce" +( + "sender_address" BYTEA NOT NULL, + "application_address" BYTEA NOT NULL, + "nonce" BIGINT NOT NULL, + UNIQUE("sender_address", "application_address") +); + +CREATE TABLE "input_index" +( + "application_address" BYTEA PRIMARY KEY, + "index" BIGINT NOT NULL +); + +-- create trigger to update updated_at timestamp + +CREATE FUNCTION trigger_set_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); +RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER set_timestamp +BEFORE +UPDATE ON application +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE +UPDATE ON epoch +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE +UPDATE ON input +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE +UPDATE ON output +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); diff --git a/internal/repository/schema/migrations/000002_create_postgraphile_view.down.sql b/internal/repository/schema/migrations/000002_create_postgraphile_view.down.sql new file mode 100644 index 00000000..4fc005d2 --- /dev/null +++ b/internal/repository/schema/migrations/000002_create_postgraphile_view.down.sql @@ -0,0 +1,4 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +DROP SCHEMA graphql CASCADE; diff --git a/internal/repository/schema/migrations/000002_create_postgraphile_view.up.sql b/internal/repository/schema/migrations/000002_create_postgraphile_view.up.sql new file mode 100644 index 00000000..18475a10 --- /dev/null +++ b/internal/repository/schema/migrations/000002_create_postgraphile_view.up.sql @@ -0,0 +1,92 @@ + +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +CREATE SCHEMA IF NOT EXISTS graphql; + +CREATE OR REPLACE VIEW graphql."applications" AS + SELECT + "contract_address", + "template_hash", + "last_processed_block", + "status" + FROM + "application"; + +CREATE OR REPLACE VIEW graphql."epochs" AS + SELECT + "index", + "application_address", + "first_block", + "last_block", + "transaction_hash", + "claim_hash", + "status" + FROM + "epoch"; + +CREATE OR REPLACE VIEW graphql."inputs" AS + SELECT + i."index", + i."status", + i."block_number", + i."raw_data", + i."machine_hash", + i."outputs_hash", + i."application_address", + i."transaction_id", + e."index" as "epoch_index" + FROM + "input" i + INNER JOIN + "epoch" e on i."epoch_id" = e."id"; + +CREATE OR REPLACE VIEW graphql."outputs" AS + SELECT + o."index", + o."raw_data", + o."output_hashes_siblings", + o."transaction_hash", + i."index" as "input_index" + FROM + "output" o + INNER JOIN + "input" i on o."input_id"=i."id"; + +CREATE OR REPLACE VIEW graphql."reports" AS + SELECT + r."index", + r."raw_data", + i."index" as "input_index" + FROM + "report" r + INNER JOIN + "input" i on r."input_id"=i."id"; + +CREATE OR REPLACE VIEW graphql."espresso_nonces" AS + SELECT + "sender_address", + "application_address", + "nonce" + FROM + "espresso_nonce"; + +CREATE OR REPLACE VIEW graphql."input_indexs" AS + SELECT + "application_address", + "index" + FROM + "input_index"; + +COMMENT ON VIEW graphql."inputs" is + E'@foreignKey (application_address) references applications(contract_address)|@fieldName applicationByApplicationAddress\n@foreignKey (epoch_index) references epochs(index)|@fieldName epochByEpochIndex'; + +COMMENT ON VIEW graphql."outputs" is + E'@foreignKey (input_index) references inputs(index)|@fieldName inputByInputIndex'; + +COMMENT ON VIEW graphql."reports" is + E'@foreignKey (input_index) references inputs(index)|@fieldName inputByInputIndex'; + +COMMENT ON VIEW graphql."epochs" is + E'@foreignKey (application_address) references applications(contract_address)|@fieldName applicationByApplicationAddress'; + \ No newline at end of file diff --git a/internal/repository/schema/schema.go b/internal/repository/schema/schema.go new file mode 100644 index 00000000..ccc6742e --- /dev/null +++ b/internal/repository/schema/schema.go @@ -0,0 +1,85 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package schema + +import ( + "embed" + "errors" + "fmt" + "log/slog" + + "github.com/golang-migrate/migrate/v4" + mig "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed migrations/* +var content embed.FS + +const ExpectedVersion uint = 2 + +type Schema struct { + migrate *mig.Migrate +} + +func New(postgresEndpoint string) (*Schema, error) { + driver, err := iofs.New(content, "migrations") + if err != nil { + return nil, err + } + + migrate, err := mig.NewWithSourceInstance("iofs", driver, postgresEndpoint) + if err != nil { + return nil, err + } + + return &Schema{migrate: migrate}, nil +} + +func (s *Schema) Version() (uint, error) { + version, _, err := s.migrate.Version() + if err != nil && errors.Is(err, migrate.ErrNilVersion) { + return version, fmt.Errorf("No valid database schema found") + } + return version, err +} + +func (s *Schema) Upgrade() error { + if err := s.migrate.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return err + } + return nil +} + +func (s *Schema) Downgrade() error { + if err := s.migrate.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return err + } + return nil +} + +func (s *Schema) Close() { + source, db := s.migrate.Close() + if source != nil { + slog.Error("Error releasing migration sources", "error", source) + } + if db != nil { + slog.Error("Error closing db connection", "error", db) + } +} + +func (s *Schema) ValidateVersion() (uint, error) { + version, err := s.Version() + if err != nil { + return 0, err + } + + if version != ExpectedVersion { + format := "Database schema version mismatch. Expected %d but it is %d" + return 0, fmt.Errorf(format, ExpectedVersion, version) + } + return version, nil +} diff --git a/internal/services/retry/retry.go b/internal/services/retry/retry.go new file mode 100644 index 00000000..530a6cc9 --- /dev/null +++ b/internal/services/retry/retry.go @@ -0,0 +1,49 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retry + +import ( + "log/slog" + "time" +) + +// Implements a simple method call retry policy. +// This retry policy will retry a failed execution for up to 'maxRetries' times +// In between retry calls it will wait a "maxDelay" +func CallFunctionWithRetryPolicy[ + R any, + A any, +]( + fn func(A) (R, error), + args A, + maxRetries uint64, + maxDelay time.Duration, + infoLabel string, +) (R, error) { + + var lastErr error + var lastValue R + + for i := uint64(0); i <= maxRetries; i++ { + + if i != 0 { + slog.Info("Retry Policy: Retrying...", "delay", maxDelay) + time.Sleep(maxDelay) + } + + lastValue, lastErr = fn(args) + if lastErr == nil { + return lastValue, nil + } + slog.Info( + "Retry Policy: Got error calling function", + "label", + infoLabel, + "error", + lastErr.Error()) + + } + return lastValue, lastErr + +} diff --git a/internal/services/retry/retry_test.go b/internal/services/retry/retry_test.go new file mode 100644 index 00000000..77a00c47 --- /dev/null +++ b/internal/services/retry/retry_test.go @@ -0,0 +1,71 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package retry + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type RetrySuite struct { + suite.Suite +} + +func TestRetrySuite(t *testing.T) { + suite.Run(t, new(RetrySuite)) +} + +func (s *RetrySuite) SetupSuite() {} +func (s *RetrySuite) TearDownSuite() {} + +func (s *RetrySuite) TestRetry() { + simpleMock := &SimpleMock{} + + simpleMock.On( + "execute", + mock.Anything). + Once(). + Return(0, fmt.Errorf("An error")) + + simpleMock.On( + "execute", + mock.Anything). + Return(0, nil) + + _, err := CallFunctionWithRetryPolicy(simpleMock.execute, 0, 3, 1*time.Millisecond, "TEST") + s.Require().Nil(err) + + simpleMock.AssertNumberOfCalls(s.T(), "execute", 2) + +} + +func (s *RetrySuite) TestRetryMaxRetries() { + + simpleMock := &SimpleMock{} + simpleMock.On( + "execute", + mock.Anything). + Return(0, fmt.Errorf("An error")) + + _, err := CallFunctionWithRetryPolicy(simpleMock.execute, 0, 3, 1*time.Millisecond, "TEST") + s.Require().NotNil(err) + + simpleMock.AssertNumberOfCalls(s.T(), "execute", 4) + +} + +type SimpleMock struct { + mock.Mock +} + +func (m *SimpleMock) execute( + arg int, +) (int, error) { + args := m.Called(arg) + return args.Get(0).(int), args.Error(1) +} diff --git a/internal/services/startup/startup.go b/internal/services/startup/startup.go new file mode 100644 index 00000000..85059ff3 --- /dev/null +++ b/internal/services/startup/startup.go @@ -0,0 +1,77 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) +package startup + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + + "github.com/ZzzzHui/espresso-reader/internal/config" + "github.com/ZzzzHui/espresso-reader/internal/model" + "github.com/ZzzzHui/espresso-reader/internal/repository" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v5" + "github.com/lmittmann/tint" + "github.com/mattn/go-isatty" +) + +// Configure the node logs +func ConfigLogs(logLevel slog.Level, logPrettyEnabled bool) { + opts := &tint.Options{ + Level: logLevel, + AddSource: logLevel == slog.LevelDebug, + NoColor: !logPrettyEnabled || !isatty.IsTerminal(os.Stdout.Fd()), + TimeFormat: "2006-01-02T15:04:05.000", // RFC3339 with milliseconds and without timezone + } + handler := tint.NewHandler(os.Stdout, opts) + logger := slog.New(handler) + slog.SetDefault(logger) +} + +// Handles Persistent Config +func SetupNodePersistentConfig( + ctx context.Context, + database *repository.Database, + config config.NodeConfig, +) (*model.NodePersistentConfig, error) { + nodePersistentConfig, err := database.GetNodeConfig(ctx) + if err != nil { + if !errors.Is(err, pgx.ErrNoRows) { + return nil, fmt.Errorf( + "Could not retrieve persistent config from Database. %w", + err, + ) + } + } + + if nodePersistentConfig == nil { + nodePersistentConfig = &model.NodePersistentConfig{ + DefaultBlock: config.EvmReaderDefaultBlock, + InputBoxDeploymentBlock: uint64(config.ContractsInputBoxDeploymentBlockNumber), + InputBoxAddress: common.HexToAddress(config.ContractsInputBoxAddress), + ChainId: config.BlockchainID, + } + slog.Info( + "No persistent config found at the database. Setting it up", + "persistent config", + nodePersistentConfig, + ) + + err = database.InsertNodeConfig(ctx, nodePersistentConfig) + if err != nil { + return nil, fmt.Errorf("Couldn't insert database config. Error : %v", err) + } + } else { + slog.Info( + "Node was already configured. Using previous persistent config", + "persistent config", + nodePersistentConfig, + ) + } + + return nodePersistentConfig, nil +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..f97e1eeb --- /dev/null +++ b/main.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "log/slog" + "os" + "time" + + "github.com/ZzzzHui/espresso-reader/internal/config" + "github.com/ZzzzHui/espresso-reader/internal/espressoreader/service" + "github.com/ZzzzHui/espresso-reader/internal/repository" + "github.com/ZzzzHui/espresso-reader/internal/services/startup" + + "github.com/spf13/cobra" +) + +var ( + // Should be overridden during the final release build with ldflags + // to contain the actual version number + buildVersion = "devel" +) + +const ( + CMD_NAME = "espresso-sequencer" +) + +var Cmd = &cobra.Command{ + Use: CMD_NAME, + Short: "Runs Espresso Reader", + Long: `Runs Espresso Reader`, + Run: run, +} + +func run(cmd *cobra.Command, args []string) { + startTime := time.Now() + + ctx := cmd.Context() + + c := config.FromEnv() + + // setup log + startup.ConfigLogs(c.LogLevel, c.LogPrettyEnabled) + + slog.Info("Starting the Cartesi Rollups Node Espresso Reader", "config", c) + + database, err := repository.Connect(ctx, c.PostgresEndpoint.Value) + if err != nil { + slog.Error("EVM Reader couldn't connect to the database", "error", err) + os.Exit(1) + } + defer database.Close() + + _, err = startup.SetupNodePersistentConfig(ctx, database, c) + if err != nil { + slog.Error("EVM Reader couldn't connect to the database", "error", err) + os.Exit(1) + } + + // create Espresso Reader Service + service := service.NewEspressoReaderService( + c.BlockchainHttpEndpoint.Value, + c.BlockchainHttpEndpoint.Value, + database, + c.EspressoBaseUrl, + c.EspressoStartingBlock, + c.EspressoNamespace, + c.EvmReaderRetryPolicyMaxRetries, + c.EvmReaderRetryPolicyMaxDelay, + c.BlockchainID, + uint64(c.ContractsInputBoxDeploymentBlockNumber), + c.EspressoServiceEndpoint, + ) + + // logs startup time + ready := make(chan struct{}, 1) + go func() { + select { + case <-ready: + duration := time.Since(startTime) + slog.Info("EVM Reader is ready", "after", duration) + case <-ctx.Done(): + } + }() + + // start service + if err := service.Start(ctx, ready); err != nil { + slog.Error("Espresso Reader exited with an error", "error", err) + os.Exit(1) + } +} + +func main() { + err := Cmd.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/pkg/contracts/generate/main.go b/pkg/contracts/generate/main.go new file mode 100644 index 00000000..d4d64d51 --- /dev/null +++ b/pkg/contracts/generate/main.go @@ -0,0 +1,149 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// This binary generates the Go bindings for the Cartesi Rollups contracts. +// This binary should be called with `go generate` in the parent dir. +// First, it downloads the Cartesi Rollups npm package containing the contracts. +// Then, it generates the bindings using abi-gen. +// Finally, it stores the bindings in the current directory. +package main + +import ( + "encoding/json" + "errors" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" +) + +const baseContractsPath = "../../rollups-contracts/export/artifacts/contracts/" + +type contractBinding struct { + jsonPath string + typeName string +} + +var bindings = []contractBinding{ + { + jsonPath: baseContractsPath + "consensus/authority/IAuthorityFactory.sol/IAuthorityFactory.json", + typeName: "IAuthorityFactory", + }, + { + jsonPath: baseContractsPath + "consensus/IConsensus.sol/IConsensus.json", + typeName: "IConsensus", + }, + { + jsonPath: baseContractsPath + "dapp/IApplication.sol/IApplication.json", + typeName: "IApplication", + }, + { + jsonPath: baseContractsPath + "dapp/IApplicationFactory.sol/IApplicationFactory.json", + typeName: "IApplicationFactory", + }, + { + jsonPath: baseContractsPath + "dapp/ISelfHostedApplicationFactory.sol/ISelfHostedApplicationFactory.json", + typeName: "ISelfHostedApplicationFactory", + }, + { + jsonPath: baseContractsPath + "inputs/IInputBox.sol/IInputBox.json", + typeName: "IInputBox", + }, + { + jsonPath: baseContractsPath + "common/Inputs.sol/Inputs.json", + typeName: "Inputs", + }, + { + jsonPath: baseContractsPath + "common/Outputs.sol/Outputs.json", + typeName: "Outputs", + }, +} + +func main() { + files := make(map[string]bool) + for _, b := range bindings { + files[b.jsonPath] = true + } + contents := readFilesFromDir(files) + + for _, b := range bindings { + content := contents[b.jsonPath] + if content == nil { + log.Fatal("missing contents for ", b.jsonPath) + } + generateBinding(b, content) + } +} + +// Exit if there is any error. +func checkErr(context string, err any) { + if err != nil { + log.Fatal(context, ": ", err) + } +} + +// Read the required files from the directory. +// Return a map with the file contents. +func readFilesFromDir(files map[string]bool) map[string][]byte { + contents := make(map[string][]byte) + for fileName := range files { + fileFullPath, err := filepath.Abs(fileName) + if err != nil { + log.Fatal(err) + } + data, err := os.ReadFile(fileFullPath) + checkErr("read file", err) + contents[fileName] = data + } + return contents +} + +// Get the .abi key from the json +func getAbi(rawJson []byte) []byte { + var contents struct { + Abi json.RawMessage `json:"abi"` + } + err := json.Unmarshal(rawJson, &contents) + checkErr("decode json", err) + return contents.Abi +} + +// Check whether file exists. +func fileExists(filePath string) bool { + _, err := os.Stat(filePath) + return !errors.Is(err, fs.ErrNotExist) +} + +// Generate the Go bindings for the contracts. +func generateBinding(b contractBinding, content []byte) { + var ( + pkg = strings.ToLower(b.typeName) + sigs []map[string]string + abis = []string{string(getAbi(content))} + bins = []string{""} + types = []string{b.typeName} + libs = make(map[string]string) + aliases = make(map[string]string) + ) + code, err := bind.Bind(types, abis, bins, sigs, pkg, bind.LangGo, libs, aliases) + checkErr("generate binding", err) + + if fileExists(pkg) { + err := os.RemoveAll(pkg) + checkErr("removing dir", err) + } + + const dirMode = 0700 + err = os.Mkdir(pkg, dirMode) + checkErr("creating dir", err) + + const fileMode = 0600 + filePath := pkg + "/" + pkg + ".go" + err = os.WriteFile(filePath, []byte(code), fileMode) + checkErr("write binding file", err) + + log.Print("generated binding for ", filePath) +} diff --git a/pkg/contracts/iapplication/iapplication.go b/pkg/contracts/iapplication/iapplication.go new file mode 100644 index 00000000..a4814a82 --- /dev/null +++ b/pkg/contracts/iapplication/iapplication.go @@ -0,0 +1,722 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package iapplication + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// OutputValidityProof is an auto generated low-level Go binding around an user-defined struct. +type OutputValidityProof struct { + OutputIndex uint64 + OutputHashesSiblings [][32]byte +} + +// IApplicationMetaData contains all meta data concerning the IApplication contract. +var IApplicationMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"}],\"name\":\"ClaimNotAccepted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidOutputHashesSiblingsArrayLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"output\",\"type\":\"bytes\"}],\"name\":\"OutputNotExecutable\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"output\",\"type\":\"bytes\"}],\"name\":\"OutputNotReexecutable\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"contractIConsensus\",\"name\":\"newConsensus\",\"type\":\"address\"}],\"name\":\"NewConsensus\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"outputIndex\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"output\",\"type\":\"bytes\"}],\"name\":\"OutputExecuted\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"output\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"outputIndex\",\"type\":\"uint64\"},{\"internalType\":\"bytes32[]\",\"name\":\"outputHashesSiblings\",\"type\":\"bytes32[]\"}],\"internalType\":\"structOutputValidityProof\",\"name\":\"proof\",\"type\":\"tuple\"}],\"name\":\"executeOutput\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getConsensus\",\"outputs\":[{\"internalType\":\"contractIConsensus\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTemplateHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractIConsensus\",\"name\":\"newConsensus\",\"type\":\"address\"}],\"name\":\"migrateToConsensus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"output\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"outputIndex\",\"type\":\"uint64\"},{\"internalType\":\"bytes32[]\",\"name\":\"outputHashesSiblings\",\"type\":\"bytes32[]\"}],\"internalType\":\"structOutputValidityProof\",\"name\":\"proof\",\"type\":\"tuple\"}],\"name\":\"validateOutput\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"outputHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"outputIndex\",\"type\":\"uint64\"},{\"internalType\":\"bytes32[]\",\"name\":\"outputHashesSiblings\",\"type\":\"bytes32[]\"}],\"internalType\":\"structOutputValidityProof\",\"name\":\"proof\",\"type\":\"tuple\"}],\"name\":\"validateOutputHash\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"outputIndex\",\"type\":\"uint256\"}],\"name\":\"wasOutputExecuted\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// IApplicationABI is the input ABI used to generate the binding from. +// Deprecated: Use IApplicationMetaData.ABI instead. +var IApplicationABI = IApplicationMetaData.ABI + +// IApplication is an auto generated Go binding around an Ethereum contract. +type IApplication struct { + IApplicationCaller // Read-only binding to the contract + IApplicationTransactor // Write-only binding to the contract + IApplicationFilterer // Log filterer for contract events +} + +// IApplicationCaller is an auto generated read-only Go binding around an Ethereum contract. +type IApplicationCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IApplicationTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IApplicationTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IApplicationFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IApplicationFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IApplicationSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IApplicationSession struct { + Contract *IApplication // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IApplicationCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IApplicationCallerSession struct { + Contract *IApplicationCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IApplicationTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IApplicationTransactorSession struct { + Contract *IApplicationTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IApplicationRaw is an auto generated low-level Go binding around an Ethereum contract. +type IApplicationRaw struct { + Contract *IApplication // Generic contract binding to access the raw methods on +} + +// IApplicationCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IApplicationCallerRaw struct { + Contract *IApplicationCaller // Generic read-only contract binding to access the raw methods on +} + +// IApplicationTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IApplicationTransactorRaw struct { + Contract *IApplicationTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIApplication creates a new instance of IApplication, bound to a specific deployed contract. +func NewIApplication(address common.Address, backend bind.ContractBackend) (*IApplication, error) { + contract, err := bindIApplication(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IApplication{IApplicationCaller: IApplicationCaller{contract: contract}, IApplicationTransactor: IApplicationTransactor{contract: contract}, IApplicationFilterer: IApplicationFilterer{contract: contract}}, nil +} + +// NewIApplicationCaller creates a new read-only instance of IApplication, bound to a specific deployed contract. +func NewIApplicationCaller(address common.Address, caller bind.ContractCaller) (*IApplicationCaller, error) { + contract, err := bindIApplication(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IApplicationCaller{contract: contract}, nil +} + +// NewIApplicationTransactor creates a new write-only instance of IApplication, bound to a specific deployed contract. +func NewIApplicationTransactor(address common.Address, transactor bind.ContractTransactor) (*IApplicationTransactor, error) { + contract, err := bindIApplication(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IApplicationTransactor{contract: contract}, nil +} + +// NewIApplicationFilterer creates a new log filterer instance of IApplication, bound to a specific deployed contract. +func NewIApplicationFilterer(address common.Address, filterer bind.ContractFilterer) (*IApplicationFilterer, error) { + contract, err := bindIApplication(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IApplicationFilterer{contract: contract}, nil +} + +// bindIApplication binds a generic wrapper to an already deployed contract. +func bindIApplication(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IApplicationMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IApplication *IApplicationRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IApplication.Contract.IApplicationCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IApplication *IApplicationRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IApplication.Contract.IApplicationTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IApplication *IApplicationRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IApplication.Contract.IApplicationTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IApplication *IApplicationCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IApplication.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IApplication *IApplicationTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IApplication.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IApplication *IApplicationTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IApplication.Contract.contract.Transact(opts, method, params...) +} + +// GetConsensus is a free data retrieval call binding the contract method 0x179e740b. +// +// Solidity: function getConsensus() view returns(address) +func (_IApplication *IApplicationCaller) GetConsensus(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IApplication.contract.Call(opts, &out, "getConsensus") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetConsensus is a free data retrieval call binding the contract method 0x179e740b. +// +// Solidity: function getConsensus() view returns(address) +func (_IApplication *IApplicationSession) GetConsensus() (common.Address, error) { + return _IApplication.Contract.GetConsensus(&_IApplication.CallOpts) +} + +// GetConsensus is a free data retrieval call binding the contract method 0x179e740b. +// +// Solidity: function getConsensus() view returns(address) +func (_IApplication *IApplicationCallerSession) GetConsensus() (common.Address, error) { + return _IApplication.Contract.GetConsensus(&_IApplication.CallOpts) +} + +// GetTemplateHash is a free data retrieval call binding the contract method 0x61b12c66. +// +// Solidity: function getTemplateHash() view returns(bytes32) +func (_IApplication *IApplicationCaller) GetTemplateHash(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _IApplication.contract.Call(opts, &out, "getTemplateHash") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetTemplateHash is a free data retrieval call binding the contract method 0x61b12c66. +// +// Solidity: function getTemplateHash() view returns(bytes32) +func (_IApplication *IApplicationSession) GetTemplateHash() ([32]byte, error) { + return _IApplication.Contract.GetTemplateHash(&_IApplication.CallOpts) +} + +// GetTemplateHash is a free data retrieval call binding the contract method 0x61b12c66. +// +// Solidity: function getTemplateHash() view returns(bytes32) +func (_IApplication *IApplicationCallerSession) GetTemplateHash() ([32]byte, error) { + return _IApplication.Contract.GetTemplateHash(&_IApplication.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IApplication *IApplicationCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IApplication.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IApplication *IApplicationSession) Owner() (common.Address, error) { + return _IApplication.Contract.Owner(&_IApplication.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IApplication *IApplicationCallerSession) Owner() (common.Address, error) { + return _IApplication.Contract.Owner(&_IApplication.CallOpts) +} + +// ValidateOutput is a free data retrieval call binding the contract method 0xe88d39c0. +// +// Solidity: function validateOutput(bytes output, (uint64,bytes32[]) proof) view returns() +func (_IApplication *IApplicationCaller) ValidateOutput(opts *bind.CallOpts, output []byte, proof OutputValidityProof) error { + var out []interface{} + err := _IApplication.contract.Call(opts, &out, "validateOutput", output, proof) + + if err != nil { + return err + } + + return err + +} + +// ValidateOutput is a free data retrieval call binding the contract method 0xe88d39c0. +// +// Solidity: function validateOutput(bytes output, (uint64,bytes32[]) proof) view returns() +func (_IApplication *IApplicationSession) ValidateOutput(output []byte, proof OutputValidityProof) error { + return _IApplication.Contract.ValidateOutput(&_IApplication.CallOpts, output, proof) +} + +// ValidateOutput is a free data retrieval call binding the contract method 0xe88d39c0. +// +// Solidity: function validateOutput(bytes output, (uint64,bytes32[]) proof) view returns() +func (_IApplication *IApplicationCallerSession) ValidateOutput(output []byte, proof OutputValidityProof) error { + return _IApplication.Contract.ValidateOutput(&_IApplication.CallOpts, output, proof) +} + +// ValidateOutputHash is a free data retrieval call binding the contract method 0x08eb89ab. +// +// Solidity: function validateOutputHash(bytes32 outputHash, (uint64,bytes32[]) proof) view returns() +func (_IApplication *IApplicationCaller) ValidateOutputHash(opts *bind.CallOpts, outputHash [32]byte, proof OutputValidityProof) error { + var out []interface{} + err := _IApplication.contract.Call(opts, &out, "validateOutputHash", outputHash, proof) + + if err != nil { + return err + } + + return err + +} + +// ValidateOutputHash is a free data retrieval call binding the contract method 0x08eb89ab. +// +// Solidity: function validateOutputHash(bytes32 outputHash, (uint64,bytes32[]) proof) view returns() +func (_IApplication *IApplicationSession) ValidateOutputHash(outputHash [32]byte, proof OutputValidityProof) error { + return _IApplication.Contract.ValidateOutputHash(&_IApplication.CallOpts, outputHash, proof) +} + +// ValidateOutputHash is a free data retrieval call binding the contract method 0x08eb89ab. +// +// Solidity: function validateOutputHash(bytes32 outputHash, (uint64,bytes32[]) proof) view returns() +func (_IApplication *IApplicationCallerSession) ValidateOutputHash(outputHash [32]byte, proof OutputValidityProof) error { + return _IApplication.Contract.ValidateOutputHash(&_IApplication.CallOpts, outputHash, proof) +} + +// WasOutputExecuted is a free data retrieval call binding the contract method 0x71891db0. +// +// Solidity: function wasOutputExecuted(uint256 outputIndex) view returns(bool) +func (_IApplication *IApplicationCaller) WasOutputExecuted(opts *bind.CallOpts, outputIndex *big.Int) (bool, error) { + var out []interface{} + err := _IApplication.contract.Call(opts, &out, "wasOutputExecuted", outputIndex) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// WasOutputExecuted is a free data retrieval call binding the contract method 0x71891db0. +// +// Solidity: function wasOutputExecuted(uint256 outputIndex) view returns(bool) +func (_IApplication *IApplicationSession) WasOutputExecuted(outputIndex *big.Int) (bool, error) { + return _IApplication.Contract.WasOutputExecuted(&_IApplication.CallOpts, outputIndex) +} + +// WasOutputExecuted is a free data retrieval call binding the contract method 0x71891db0. +// +// Solidity: function wasOutputExecuted(uint256 outputIndex) view returns(bool) +func (_IApplication *IApplicationCallerSession) WasOutputExecuted(outputIndex *big.Int) (bool, error) { + return _IApplication.Contract.WasOutputExecuted(&_IApplication.CallOpts, outputIndex) +} + +// ExecuteOutput is a paid mutator transaction binding the contract method 0x33137b76. +// +// Solidity: function executeOutput(bytes output, (uint64,bytes32[]) proof) returns() +func (_IApplication *IApplicationTransactor) ExecuteOutput(opts *bind.TransactOpts, output []byte, proof OutputValidityProof) (*types.Transaction, error) { + return _IApplication.contract.Transact(opts, "executeOutput", output, proof) +} + +// ExecuteOutput is a paid mutator transaction binding the contract method 0x33137b76. +// +// Solidity: function executeOutput(bytes output, (uint64,bytes32[]) proof) returns() +func (_IApplication *IApplicationSession) ExecuteOutput(output []byte, proof OutputValidityProof) (*types.Transaction, error) { + return _IApplication.Contract.ExecuteOutput(&_IApplication.TransactOpts, output, proof) +} + +// ExecuteOutput is a paid mutator transaction binding the contract method 0x33137b76. +// +// Solidity: function executeOutput(bytes output, (uint64,bytes32[]) proof) returns() +func (_IApplication *IApplicationTransactorSession) ExecuteOutput(output []byte, proof OutputValidityProof) (*types.Transaction, error) { + return _IApplication.Contract.ExecuteOutput(&_IApplication.TransactOpts, output, proof) +} + +// MigrateToConsensus is a paid mutator transaction binding the contract method 0xfc411683. +// +// Solidity: function migrateToConsensus(address newConsensus) returns() +func (_IApplication *IApplicationTransactor) MigrateToConsensus(opts *bind.TransactOpts, newConsensus common.Address) (*types.Transaction, error) { + return _IApplication.contract.Transact(opts, "migrateToConsensus", newConsensus) +} + +// MigrateToConsensus is a paid mutator transaction binding the contract method 0xfc411683. +// +// Solidity: function migrateToConsensus(address newConsensus) returns() +func (_IApplication *IApplicationSession) MigrateToConsensus(newConsensus common.Address) (*types.Transaction, error) { + return _IApplication.Contract.MigrateToConsensus(&_IApplication.TransactOpts, newConsensus) +} + +// MigrateToConsensus is a paid mutator transaction binding the contract method 0xfc411683. +// +// Solidity: function migrateToConsensus(address newConsensus) returns() +func (_IApplication *IApplicationTransactorSession) MigrateToConsensus(newConsensus common.Address) (*types.Transaction, error) { + return _IApplication.Contract.MigrateToConsensus(&_IApplication.TransactOpts, newConsensus) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IApplication *IApplicationTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IApplication.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IApplication *IApplicationSession) RenounceOwnership() (*types.Transaction, error) { + return _IApplication.Contract.RenounceOwnership(&_IApplication.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IApplication *IApplicationTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _IApplication.Contract.RenounceOwnership(&_IApplication.TransactOpts) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IApplication *IApplicationTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _IApplication.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IApplication *IApplicationSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _IApplication.Contract.TransferOwnership(&_IApplication.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IApplication *IApplicationTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _IApplication.Contract.TransferOwnership(&_IApplication.TransactOpts, newOwner) +} + +// IApplicationNewConsensusIterator is returned from FilterNewConsensus and is used to iterate over the raw logs and unpacked data for NewConsensus events raised by the IApplication contract. +type IApplicationNewConsensusIterator struct { + Event *IApplicationNewConsensus // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IApplicationNewConsensusIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IApplicationNewConsensus) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IApplicationNewConsensus) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IApplicationNewConsensusIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IApplicationNewConsensusIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IApplicationNewConsensus represents a NewConsensus event raised by the IApplication contract. +type IApplicationNewConsensus struct { + NewConsensus common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNewConsensus is a free log retrieval operation binding the contract event 0x4991c6f37185659e276ff918a96f3e20e6c5abcd8c9aab450dc19c2f7ad35cb5. +// +// Solidity: event NewConsensus(address newConsensus) +func (_IApplication *IApplicationFilterer) FilterNewConsensus(opts *bind.FilterOpts) (*IApplicationNewConsensusIterator, error) { + + logs, sub, err := _IApplication.contract.FilterLogs(opts, "NewConsensus") + if err != nil { + return nil, err + } + return &IApplicationNewConsensusIterator{contract: _IApplication.contract, event: "NewConsensus", logs: logs, sub: sub}, nil +} + +// WatchNewConsensus is a free log subscription operation binding the contract event 0x4991c6f37185659e276ff918a96f3e20e6c5abcd8c9aab450dc19c2f7ad35cb5. +// +// Solidity: event NewConsensus(address newConsensus) +func (_IApplication *IApplicationFilterer) WatchNewConsensus(opts *bind.WatchOpts, sink chan<- *IApplicationNewConsensus) (event.Subscription, error) { + + logs, sub, err := _IApplication.contract.WatchLogs(opts, "NewConsensus") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IApplicationNewConsensus) + if err := _IApplication.contract.UnpackLog(event, "NewConsensus", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseNewConsensus is a log parse operation binding the contract event 0x4991c6f37185659e276ff918a96f3e20e6c5abcd8c9aab450dc19c2f7ad35cb5. +// +// Solidity: event NewConsensus(address newConsensus) +func (_IApplication *IApplicationFilterer) ParseNewConsensus(log types.Log) (*IApplicationNewConsensus, error) { + event := new(IApplicationNewConsensus) + if err := _IApplication.contract.UnpackLog(event, "NewConsensus", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IApplicationOutputExecutedIterator is returned from FilterOutputExecuted and is used to iterate over the raw logs and unpacked data for OutputExecuted events raised by the IApplication contract. +type IApplicationOutputExecutedIterator struct { + Event *IApplicationOutputExecuted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IApplicationOutputExecutedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IApplicationOutputExecuted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IApplicationOutputExecuted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IApplicationOutputExecutedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IApplicationOutputExecutedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IApplicationOutputExecuted represents a OutputExecuted event raised by the IApplication contract. +type IApplicationOutputExecuted struct { + OutputIndex uint64 + Output []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOutputExecuted is a free log retrieval operation binding the contract event 0xcad1f361c6e84664e892230291c8e8eb9555683e0a6a5ce8ea7b204ac0ac3676. +// +// Solidity: event OutputExecuted(uint64 outputIndex, bytes output) +func (_IApplication *IApplicationFilterer) FilterOutputExecuted(opts *bind.FilterOpts) (*IApplicationOutputExecutedIterator, error) { + + logs, sub, err := _IApplication.contract.FilterLogs(opts, "OutputExecuted") + if err != nil { + return nil, err + } + return &IApplicationOutputExecutedIterator{contract: _IApplication.contract, event: "OutputExecuted", logs: logs, sub: sub}, nil +} + +// WatchOutputExecuted is a free log subscription operation binding the contract event 0xcad1f361c6e84664e892230291c8e8eb9555683e0a6a5ce8ea7b204ac0ac3676. +// +// Solidity: event OutputExecuted(uint64 outputIndex, bytes output) +func (_IApplication *IApplicationFilterer) WatchOutputExecuted(opts *bind.WatchOpts, sink chan<- *IApplicationOutputExecuted) (event.Subscription, error) { + + logs, sub, err := _IApplication.contract.WatchLogs(opts, "OutputExecuted") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IApplicationOutputExecuted) + if err := _IApplication.contract.UnpackLog(event, "OutputExecuted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOutputExecuted is a log parse operation binding the contract event 0xcad1f361c6e84664e892230291c8e8eb9555683e0a6a5ce8ea7b204ac0ac3676. +// +// Solidity: event OutputExecuted(uint64 outputIndex, bytes output) +func (_IApplication *IApplicationFilterer) ParseOutputExecuted(log types.Log) (*IApplicationOutputExecuted, error) { + event := new(IApplicationOutputExecuted) + if err := _IApplication.contract.UnpackLog(event, "OutputExecuted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/contracts/iapplicationfactory/iapplicationfactory.go b/pkg/contracts/iapplicationfactory/iapplicationfactory.go new file mode 100644 index 00000000..5daa5c82 --- /dev/null +++ b/pkg/contracts/iapplicationfactory/iapplicationfactory.go @@ -0,0 +1,401 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package iapplicationfactory + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IApplicationFactoryMetaData contains all meta data concerning the IApplicationFactory contract. +var IApplicationFactoryMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contractIConsensus\",\"name\":\"consensus\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"appOwner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"templateHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"contractIApplication\",\"name\":\"appContract\",\"type\":\"address\"}],\"name\":\"ApplicationCreated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"contractIConsensus\",\"name\":\"consensus\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"appOwner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"templateHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"}],\"name\":\"calculateApplicationAddress\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractIConsensus\",\"name\":\"consensus\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"appOwner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"templateHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"}],\"name\":\"newApplication\",\"outputs\":[{\"internalType\":\"contractIApplication\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractIConsensus\",\"name\":\"consensus\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"appOwner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"templateHash\",\"type\":\"bytes32\"}],\"name\":\"newApplication\",\"outputs\":[{\"internalType\":\"contractIApplication\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// IApplicationFactoryABI is the input ABI used to generate the binding from. +// Deprecated: Use IApplicationFactoryMetaData.ABI instead. +var IApplicationFactoryABI = IApplicationFactoryMetaData.ABI + +// IApplicationFactory is an auto generated Go binding around an Ethereum contract. +type IApplicationFactory struct { + IApplicationFactoryCaller // Read-only binding to the contract + IApplicationFactoryTransactor // Write-only binding to the contract + IApplicationFactoryFilterer // Log filterer for contract events +} + +// IApplicationFactoryCaller is an auto generated read-only Go binding around an Ethereum contract. +type IApplicationFactoryCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IApplicationFactoryTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IApplicationFactoryTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IApplicationFactoryFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IApplicationFactoryFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IApplicationFactorySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IApplicationFactorySession struct { + Contract *IApplicationFactory // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IApplicationFactoryCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IApplicationFactoryCallerSession struct { + Contract *IApplicationFactoryCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IApplicationFactoryTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IApplicationFactoryTransactorSession struct { + Contract *IApplicationFactoryTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IApplicationFactoryRaw is an auto generated low-level Go binding around an Ethereum contract. +type IApplicationFactoryRaw struct { + Contract *IApplicationFactory // Generic contract binding to access the raw methods on +} + +// IApplicationFactoryCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IApplicationFactoryCallerRaw struct { + Contract *IApplicationFactoryCaller // Generic read-only contract binding to access the raw methods on +} + +// IApplicationFactoryTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IApplicationFactoryTransactorRaw struct { + Contract *IApplicationFactoryTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIApplicationFactory creates a new instance of IApplicationFactory, bound to a specific deployed contract. +func NewIApplicationFactory(address common.Address, backend bind.ContractBackend) (*IApplicationFactory, error) { + contract, err := bindIApplicationFactory(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IApplicationFactory{IApplicationFactoryCaller: IApplicationFactoryCaller{contract: contract}, IApplicationFactoryTransactor: IApplicationFactoryTransactor{contract: contract}, IApplicationFactoryFilterer: IApplicationFactoryFilterer{contract: contract}}, nil +} + +// NewIApplicationFactoryCaller creates a new read-only instance of IApplicationFactory, bound to a specific deployed contract. +func NewIApplicationFactoryCaller(address common.Address, caller bind.ContractCaller) (*IApplicationFactoryCaller, error) { + contract, err := bindIApplicationFactory(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IApplicationFactoryCaller{contract: contract}, nil +} + +// NewIApplicationFactoryTransactor creates a new write-only instance of IApplicationFactory, bound to a specific deployed contract. +func NewIApplicationFactoryTransactor(address common.Address, transactor bind.ContractTransactor) (*IApplicationFactoryTransactor, error) { + contract, err := bindIApplicationFactory(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IApplicationFactoryTransactor{contract: contract}, nil +} + +// NewIApplicationFactoryFilterer creates a new log filterer instance of IApplicationFactory, bound to a specific deployed contract. +func NewIApplicationFactoryFilterer(address common.Address, filterer bind.ContractFilterer) (*IApplicationFactoryFilterer, error) { + contract, err := bindIApplicationFactory(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IApplicationFactoryFilterer{contract: contract}, nil +} + +// bindIApplicationFactory binds a generic wrapper to an already deployed contract. +func bindIApplicationFactory(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IApplicationFactoryMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IApplicationFactory *IApplicationFactoryRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IApplicationFactory.Contract.IApplicationFactoryCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IApplicationFactory *IApplicationFactoryRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IApplicationFactory.Contract.IApplicationFactoryTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IApplicationFactory *IApplicationFactoryRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IApplicationFactory.Contract.IApplicationFactoryTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IApplicationFactory *IApplicationFactoryCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IApplicationFactory.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IApplicationFactory *IApplicationFactoryTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IApplicationFactory.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IApplicationFactory *IApplicationFactoryTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IApplicationFactory.Contract.contract.Transact(opts, method, params...) +} + +// CalculateApplicationAddress is a free data retrieval call binding the contract method 0xbd4f1219. +// +// Solidity: function calculateApplicationAddress(address consensus, address appOwner, bytes32 templateHash, bytes32 salt) view returns(address) +func (_IApplicationFactory *IApplicationFactoryCaller) CalculateApplicationAddress(opts *bind.CallOpts, consensus common.Address, appOwner common.Address, templateHash [32]byte, salt [32]byte) (common.Address, error) { + var out []interface{} + err := _IApplicationFactory.contract.Call(opts, &out, "calculateApplicationAddress", consensus, appOwner, templateHash, salt) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// CalculateApplicationAddress is a free data retrieval call binding the contract method 0xbd4f1219. +// +// Solidity: function calculateApplicationAddress(address consensus, address appOwner, bytes32 templateHash, bytes32 salt) view returns(address) +func (_IApplicationFactory *IApplicationFactorySession) CalculateApplicationAddress(consensus common.Address, appOwner common.Address, templateHash [32]byte, salt [32]byte) (common.Address, error) { + return _IApplicationFactory.Contract.CalculateApplicationAddress(&_IApplicationFactory.CallOpts, consensus, appOwner, templateHash, salt) +} + +// CalculateApplicationAddress is a free data retrieval call binding the contract method 0xbd4f1219. +// +// Solidity: function calculateApplicationAddress(address consensus, address appOwner, bytes32 templateHash, bytes32 salt) view returns(address) +func (_IApplicationFactory *IApplicationFactoryCallerSession) CalculateApplicationAddress(consensus common.Address, appOwner common.Address, templateHash [32]byte, salt [32]byte) (common.Address, error) { + return _IApplicationFactory.Contract.CalculateApplicationAddress(&_IApplicationFactory.CallOpts, consensus, appOwner, templateHash, salt) +} + +// NewApplication is a paid mutator transaction binding the contract method 0x0e1a07f5. +// +// Solidity: function newApplication(address consensus, address appOwner, bytes32 templateHash, bytes32 salt) returns(address) +func (_IApplicationFactory *IApplicationFactoryTransactor) NewApplication(opts *bind.TransactOpts, consensus common.Address, appOwner common.Address, templateHash [32]byte, salt [32]byte) (*types.Transaction, error) { + return _IApplicationFactory.contract.Transact(opts, "newApplication", consensus, appOwner, templateHash, salt) +} + +// NewApplication is a paid mutator transaction binding the contract method 0x0e1a07f5. +// +// Solidity: function newApplication(address consensus, address appOwner, bytes32 templateHash, bytes32 salt) returns(address) +func (_IApplicationFactory *IApplicationFactorySession) NewApplication(consensus common.Address, appOwner common.Address, templateHash [32]byte, salt [32]byte) (*types.Transaction, error) { + return _IApplicationFactory.Contract.NewApplication(&_IApplicationFactory.TransactOpts, consensus, appOwner, templateHash, salt) +} + +// NewApplication is a paid mutator transaction binding the contract method 0x0e1a07f5. +// +// Solidity: function newApplication(address consensus, address appOwner, bytes32 templateHash, bytes32 salt) returns(address) +func (_IApplicationFactory *IApplicationFactoryTransactorSession) NewApplication(consensus common.Address, appOwner common.Address, templateHash [32]byte, salt [32]byte) (*types.Transaction, error) { + return _IApplicationFactory.Contract.NewApplication(&_IApplicationFactory.TransactOpts, consensus, appOwner, templateHash, salt) +} + +// NewApplication0 is a paid mutator transaction binding the contract method 0x3648bfb5. +// +// Solidity: function newApplication(address consensus, address appOwner, bytes32 templateHash) returns(address) +func (_IApplicationFactory *IApplicationFactoryTransactor) NewApplication0(opts *bind.TransactOpts, consensus common.Address, appOwner common.Address, templateHash [32]byte) (*types.Transaction, error) { + return _IApplicationFactory.contract.Transact(opts, "newApplication0", consensus, appOwner, templateHash) +} + +// NewApplication0 is a paid mutator transaction binding the contract method 0x3648bfb5. +// +// Solidity: function newApplication(address consensus, address appOwner, bytes32 templateHash) returns(address) +func (_IApplicationFactory *IApplicationFactorySession) NewApplication0(consensus common.Address, appOwner common.Address, templateHash [32]byte) (*types.Transaction, error) { + return _IApplicationFactory.Contract.NewApplication0(&_IApplicationFactory.TransactOpts, consensus, appOwner, templateHash) +} + +// NewApplication0 is a paid mutator transaction binding the contract method 0x3648bfb5. +// +// Solidity: function newApplication(address consensus, address appOwner, bytes32 templateHash) returns(address) +func (_IApplicationFactory *IApplicationFactoryTransactorSession) NewApplication0(consensus common.Address, appOwner common.Address, templateHash [32]byte) (*types.Transaction, error) { + return _IApplicationFactory.Contract.NewApplication0(&_IApplicationFactory.TransactOpts, consensus, appOwner, templateHash) +} + +// IApplicationFactoryApplicationCreatedIterator is returned from FilterApplicationCreated and is used to iterate over the raw logs and unpacked data for ApplicationCreated events raised by the IApplicationFactory contract. +type IApplicationFactoryApplicationCreatedIterator struct { + Event *IApplicationFactoryApplicationCreated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IApplicationFactoryApplicationCreatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IApplicationFactoryApplicationCreated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IApplicationFactoryApplicationCreated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IApplicationFactoryApplicationCreatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IApplicationFactoryApplicationCreatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IApplicationFactoryApplicationCreated represents a ApplicationCreated event raised by the IApplicationFactory contract. +type IApplicationFactoryApplicationCreated struct { + Consensus common.Address + AppOwner common.Address + TemplateHash [32]byte + AppContract common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterApplicationCreated is a free log retrieval operation binding the contract event 0xe73165c2d277daf8713fd08b40845cb6bb7a20b2b543f3d35324a475660fcebd. +// +// Solidity: event ApplicationCreated(address indexed consensus, address appOwner, bytes32 templateHash, address appContract) +func (_IApplicationFactory *IApplicationFactoryFilterer) FilterApplicationCreated(opts *bind.FilterOpts, consensus []common.Address) (*IApplicationFactoryApplicationCreatedIterator, error) { + + var consensusRule []interface{} + for _, consensusItem := range consensus { + consensusRule = append(consensusRule, consensusItem) + } + + logs, sub, err := _IApplicationFactory.contract.FilterLogs(opts, "ApplicationCreated", consensusRule) + if err != nil { + return nil, err + } + return &IApplicationFactoryApplicationCreatedIterator{contract: _IApplicationFactory.contract, event: "ApplicationCreated", logs: logs, sub: sub}, nil +} + +// WatchApplicationCreated is a free log subscription operation binding the contract event 0xe73165c2d277daf8713fd08b40845cb6bb7a20b2b543f3d35324a475660fcebd. +// +// Solidity: event ApplicationCreated(address indexed consensus, address appOwner, bytes32 templateHash, address appContract) +func (_IApplicationFactory *IApplicationFactoryFilterer) WatchApplicationCreated(opts *bind.WatchOpts, sink chan<- *IApplicationFactoryApplicationCreated, consensus []common.Address) (event.Subscription, error) { + + var consensusRule []interface{} + for _, consensusItem := range consensus { + consensusRule = append(consensusRule, consensusItem) + } + + logs, sub, err := _IApplicationFactory.contract.WatchLogs(opts, "ApplicationCreated", consensusRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IApplicationFactoryApplicationCreated) + if err := _IApplicationFactory.contract.UnpackLog(event, "ApplicationCreated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseApplicationCreated is a log parse operation binding the contract event 0xe73165c2d277daf8713fd08b40845cb6bb7a20b2b543f3d35324a475660fcebd. +// +// Solidity: event ApplicationCreated(address indexed consensus, address appOwner, bytes32 templateHash, address appContract) +func (_IApplicationFactory *IApplicationFactoryFilterer) ParseApplicationCreated(log types.Log) (*IApplicationFactoryApplicationCreated, error) { + event := new(IApplicationFactoryApplicationCreated) + if err := _IApplicationFactory.contract.UnpackLog(event, "ApplicationCreated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/contracts/iauthorityfactory/iauthorityfactory.go b/pkg/contracts/iauthorityfactory/iauthorityfactory.go new file mode 100644 index 00000000..babb53dd --- /dev/null +++ b/pkg/contracts/iauthorityfactory/iauthorityfactory.go @@ -0,0 +1,388 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package iauthorityfactory + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IAuthorityFactoryMetaData contains all meta data concerning the IAuthorityFactory contract. +var IAuthorityFactoryMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"contractIAuthority\",\"name\":\"authority\",\"type\":\"address\"}],\"name\":\"AuthorityCreated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"authorityOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"epochLength\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"}],\"name\":\"calculateAuthorityAddress\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"authorityOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"epochLength\",\"type\":\"uint256\"}],\"name\":\"newAuthority\",\"outputs\":[{\"internalType\":\"contractIAuthority\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"authorityOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"epochLength\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"}],\"name\":\"newAuthority\",\"outputs\":[{\"internalType\":\"contractIAuthority\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// IAuthorityFactoryABI is the input ABI used to generate the binding from. +// Deprecated: Use IAuthorityFactoryMetaData.ABI instead. +var IAuthorityFactoryABI = IAuthorityFactoryMetaData.ABI + +// IAuthorityFactory is an auto generated Go binding around an Ethereum contract. +type IAuthorityFactory struct { + IAuthorityFactoryCaller // Read-only binding to the contract + IAuthorityFactoryTransactor // Write-only binding to the contract + IAuthorityFactoryFilterer // Log filterer for contract events +} + +// IAuthorityFactoryCaller is an auto generated read-only Go binding around an Ethereum contract. +type IAuthorityFactoryCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IAuthorityFactoryTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IAuthorityFactoryTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IAuthorityFactoryFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IAuthorityFactoryFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IAuthorityFactorySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IAuthorityFactorySession struct { + Contract *IAuthorityFactory // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IAuthorityFactoryCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IAuthorityFactoryCallerSession struct { + Contract *IAuthorityFactoryCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IAuthorityFactoryTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IAuthorityFactoryTransactorSession struct { + Contract *IAuthorityFactoryTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IAuthorityFactoryRaw is an auto generated low-level Go binding around an Ethereum contract. +type IAuthorityFactoryRaw struct { + Contract *IAuthorityFactory // Generic contract binding to access the raw methods on +} + +// IAuthorityFactoryCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IAuthorityFactoryCallerRaw struct { + Contract *IAuthorityFactoryCaller // Generic read-only contract binding to access the raw methods on +} + +// IAuthorityFactoryTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IAuthorityFactoryTransactorRaw struct { + Contract *IAuthorityFactoryTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIAuthorityFactory creates a new instance of IAuthorityFactory, bound to a specific deployed contract. +func NewIAuthorityFactory(address common.Address, backend bind.ContractBackend) (*IAuthorityFactory, error) { + contract, err := bindIAuthorityFactory(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IAuthorityFactory{IAuthorityFactoryCaller: IAuthorityFactoryCaller{contract: contract}, IAuthorityFactoryTransactor: IAuthorityFactoryTransactor{contract: contract}, IAuthorityFactoryFilterer: IAuthorityFactoryFilterer{contract: contract}}, nil +} + +// NewIAuthorityFactoryCaller creates a new read-only instance of IAuthorityFactory, bound to a specific deployed contract. +func NewIAuthorityFactoryCaller(address common.Address, caller bind.ContractCaller) (*IAuthorityFactoryCaller, error) { + contract, err := bindIAuthorityFactory(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IAuthorityFactoryCaller{contract: contract}, nil +} + +// NewIAuthorityFactoryTransactor creates a new write-only instance of IAuthorityFactory, bound to a specific deployed contract. +func NewIAuthorityFactoryTransactor(address common.Address, transactor bind.ContractTransactor) (*IAuthorityFactoryTransactor, error) { + contract, err := bindIAuthorityFactory(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IAuthorityFactoryTransactor{contract: contract}, nil +} + +// NewIAuthorityFactoryFilterer creates a new log filterer instance of IAuthorityFactory, bound to a specific deployed contract. +func NewIAuthorityFactoryFilterer(address common.Address, filterer bind.ContractFilterer) (*IAuthorityFactoryFilterer, error) { + contract, err := bindIAuthorityFactory(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IAuthorityFactoryFilterer{contract: contract}, nil +} + +// bindIAuthorityFactory binds a generic wrapper to an already deployed contract. +func bindIAuthorityFactory(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IAuthorityFactoryMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IAuthorityFactory *IAuthorityFactoryRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IAuthorityFactory.Contract.IAuthorityFactoryCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IAuthorityFactory *IAuthorityFactoryRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.IAuthorityFactoryTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IAuthorityFactory *IAuthorityFactoryRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.IAuthorityFactoryTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IAuthorityFactory *IAuthorityFactoryCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IAuthorityFactory.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IAuthorityFactory *IAuthorityFactoryTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IAuthorityFactory *IAuthorityFactoryTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.contract.Transact(opts, method, params...) +} + +// CalculateAuthorityAddress is a free data retrieval call binding the contract method 0x1442f7bb. +// +// Solidity: function calculateAuthorityAddress(address authorityOwner, uint256 epochLength, bytes32 salt) view returns(address) +func (_IAuthorityFactory *IAuthorityFactoryCaller) CalculateAuthorityAddress(opts *bind.CallOpts, authorityOwner common.Address, epochLength *big.Int, salt [32]byte) (common.Address, error) { + var out []interface{} + err := _IAuthorityFactory.contract.Call(opts, &out, "calculateAuthorityAddress", authorityOwner, epochLength, salt) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// CalculateAuthorityAddress is a free data retrieval call binding the contract method 0x1442f7bb. +// +// Solidity: function calculateAuthorityAddress(address authorityOwner, uint256 epochLength, bytes32 salt) view returns(address) +func (_IAuthorityFactory *IAuthorityFactorySession) CalculateAuthorityAddress(authorityOwner common.Address, epochLength *big.Int, salt [32]byte) (common.Address, error) { + return _IAuthorityFactory.Contract.CalculateAuthorityAddress(&_IAuthorityFactory.CallOpts, authorityOwner, epochLength, salt) +} + +// CalculateAuthorityAddress is a free data retrieval call binding the contract method 0x1442f7bb. +// +// Solidity: function calculateAuthorityAddress(address authorityOwner, uint256 epochLength, bytes32 salt) view returns(address) +func (_IAuthorityFactory *IAuthorityFactoryCallerSession) CalculateAuthorityAddress(authorityOwner common.Address, epochLength *big.Int, salt [32]byte) (common.Address, error) { + return _IAuthorityFactory.Contract.CalculateAuthorityAddress(&_IAuthorityFactory.CallOpts, authorityOwner, epochLength, salt) +} + +// NewAuthority is a paid mutator transaction binding the contract method 0x93d7217c. +// +// Solidity: function newAuthority(address authorityOwner, uint256 epochLength) returns(address) +func (_IAuthorityFactory *IAuthorityFactoryTransactor) NewAuthority(opts *bind.TransactOpts, authorityOwner common.Address, epochLength *big.Int) (*types.Transaction, error) { + return _IAuthorityFactory.contract.Transact(opts, "newAuthority", authorityOwner, epochLength) +} + +// NewAuthority is a paid mutator transaction binding the contract method 0x93d7217c. +// +// Solidity: function newAuthority(address authorityOwner, uint256 epochLength) returns(address) +func (_IAuthorityFactory *IAuthorityFactorySession) NewAuthority(authorityOwner common.Address, epochLength *big.Int) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.NewAuthority(&_IAuthorityFactory.TransactOpts, authorityOwner, epochLength) +} + +// NewAuthority is a paid mutator transaction binding the contract method 0x93d7217c. +// +// Solidity: function newAuthority(address authorityOwner, uint256 epochLength) returns(address) +func (_IAuthorityFactory *IAuthorityFactoryTransactorSession) NewAuthority(authorityOwner common.Address, epochLength *big.Int) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.NewAuthority(&_IAuthorityFactory.TransactOpts, authorityOwner, epochLength) +} + +// NewAuthority0 is a paid mutator transaction binding the contract method 0xec992668. +// +// Solidity: function newAuthority(address authorityOwner, uint256 epochLength, bytes32 salt) returns(address) +func (_IAuthorityFactory *IAuthorityFactoryTransactor) NewAuthority0(opts *bind.TransactOpts, authorityOwner common.Address, epochLength *big.Int, salt [32]byte) (*types.Transaction, error) { + return _IAuthorityFactory.contract.Transact(opts, "newAuthority0", authorityOwner, epochLength, salt) +} + +// NewAuthority0 is a paid mutator transaction binding the contract method 0xec992668. +// +// Solidity: function newAuthority(address authorityOwner, uint256 epochLength, bytes32 salt) returns(address) +func (_IAuthorityFactory *IAuthorityFactorySession) NewAuthority0(authorityOwner common.Address, epochLength *big.Int, salt [32]byte) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.NewAuthority0(&_IAuthorityFactory.TransactOpts, authorityOwner, epochLength, salt) +} + +// NewAuthority0 is a paid mutator transaction binding the contract method 0xec992668. +// +// Solidity: function newAuthority(address authorityOwner, uint256 epochLength, bytes32 salt) returns(address) +func (_IAuthorityFactory *IAuthorityFactoryTransactorSession) NewAuthority0(authorityOwner common.Address, epochLength *big.Int, salt [32]byte) (*types.Transaction, error) { + return _IAuthorityFactory.Contract.NewAuthority0(&_IAuthorityFactory.TransactOpts, authorityOwner, epochLength, salt) +} + +// IAuthorityFactoryAuthorityCreatedIterator is returned from FilterAuthorityCreated and is used to iterate over the raw logs and unpacked data for AuthorityCreated events raised by the IAuthorityFactory contract. +type IAuthorityFactoryAuthorityCreatedIterator struct { + Event *IAuthorityFactoryAuthorityCreated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IAuthorityFactoryAuthorityCreatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IAuthorityFactoryAuthorityCreated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IAuthorityFactoryAuthorityCreated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IAuthorityFactoryAuthorityCreatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IAuthorityFactoryAuthorityCreatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IAuthorityFactoryAuthorityCreated represents a AuthorityCreated event raised by the IAuthorityFactory contract. +type IAuthorityFactoryAuthorityCreated struct { + Authority common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterAuthorityCreated is a free log retrieval operation binding the contract event 0xdca1fad70bee4ba7a4e17a1c6e99e657d2251af7a279124758bc01588abe2d2f. +// +// Solidity: event AuthorityCreated(address authority) +func (_IAuthorityFactory *IAuthorityFactoryFilterer) FilterAuthorityCreated(opts *bind.FilterOpts) (*IAuthorityFactoryAuthorityCreatedIterator, error) { + + logs, sub, err := _IAuthorityFactory.contract.FilterLogs(opts, "AuthorityCreated") + if err != nil { + return nil, err + } + return &IAuthorityFactoryAuthorityCreatedIterator{contract: _IAuthorityFactory.contract, event: "AuthorityCreated", logs: logs, sub: sub}, nil +} + +// WatchAuthorityCreated is a free log subscription operation binding the contract event 0xdca1fad70bee4ba7a4e17a1c6e99e657d2251af7a279124758bc01588abe2d2f. +// +// Solidity: event AuthorityCreated(address authority) +func (_IAuthorityFactory *IAuthorityFactoryFilterer) WatchAuthorityCreated(opts *bind.WatchOpts, sink chan<- *IAuthorityFactoryAuthorityCreated) (event.Subscription, error) { + + logs, sub, err := _IAuthorityFactory.contract.WatchLogs(opts, "AuthorityCreated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IAuthorityFactoryAuthorityCreated) + if err := _IAuthorityFactory.contract.UnpackLog(event, "AuthorityCreated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseAuthorityCreated is a log parse operation binding the contract event 0xdca1fad70bee4ba7a4e17a1c6e99e657d2251af7a279124758bc01588abe2d2f. +// +// Solidity: event AuthorityCreated(address authority) +func (_IAuthorityFactory *IAuthorityFactoryFilterer) ParseAuthorityCreated(log types.Log) (*IAuthorityFactoryAuthorityCreated, error) { + event := new(IAuthorityFactoryAuthorityCreated) + if err := _IAuthorityFactory.contract.UnpackLog(event, "AuthorityCreated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/contracts/iconsensus/iconsensus.go b/pkg/contracts/iconsensus/iconsensus.go new file mode 100644 index 00000000..7995007c --- /dev/null +++ b/pkg/contracts/iconsensus/iconsensus.go @@ -0,0 +1,565 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package iconsensus + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IConsensusMetaData contains all meta data concerning the IConsensus contract. +var IConsensusMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"}],\"name\":\"ClaimAcceptance\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"}],\"name\":\"ClaimSubmission\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"getEpochLength\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"}],\"name\":\"submitClaim\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"}],\"name\":\"wasClaimAccepted\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// IConsensusABI is the input ABI used to generate the binding from. +// Deprecated: Use IConsensusMetaData.ABI instead. +var IConsensusABI = IConsensusMetaData.ABI + +// IConsensus is an auto generated Go binding around an Ethereum contract. +type IConsensus struct { + IConsensusCaller // Read-only binding to the contract + IConsensusTransactor // Write-only binding to the contract + IConsensusFilterer // Log filterer for contract events +} + +// IConsensusCaller is an auto generated read-only Go binding around an Ethereum contract. +type IConsensusCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IConsensusTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IConsensusTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IConsensusFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IConsensusFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IConsensusSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IConsensusSession struct { + Contract *IConsensus // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IConsensusCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IConsensusCallerSession struct { + Contract *IConsensusCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IConsensusTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IConsensusTransactorSession struct { + Contract *IConsensusTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IConsensusRaw is an auto generated low-level Go binding around an Ethereum contract. +type IConsensusRaw struct { + Contract *IConsensus // Generic contract binding to access the raw methods on +} + +// IConsensusCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IConsensusCallerRaw struct { + Contract *IConsensusCaller // Generic read-only contract binding to access the raw methods on +} + +// IConsensusTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IConsensusTransactorRaw struct { + Contract *IConsensusTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIConsensus creates a new instance of IConsensus, bound to a specific deployed contract. +func NewIConsensus(address common.Address, backend bind.ContractBackend) (*IConsensus, error) { + contract, err := bindIConsensus(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IConsensus{IConsensusCaller: IConsensusCaller{contract: contract}, IConsensusTransactor: IConsensusTransactor{contract: contract}, IConsensusFilterer: IConsensusFilterer{contract: contract}}, nil +} + +// NewIConsensusCaller creates a new read-only instance of IConsensus, bound to a specific deployed contract. +func NewIConsensusCaller(address common.Address, caller bind.ContractCaller) (*IConsensusCaller, error) { + contract, err := bindIConsensus(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IConsensusCaller{contract: contract}, nil +} + +// NewIConsensusTransactor creates a new write-only instance of IConsensus, bound to a specific deployed contract. +func NewIConsensusTransactor(address common.Address, transactor bind.ContractTransactor) (*IConsensusTransactor, error) { + contract, err := bindIConsensus(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IConsensusTransactor{contract: contract}, nil +} + +// NewIConsensusFilterer creates a new log filterer instance of IConsensus, bound to a specific deployed contract. +func NewIConsensusFilterer(address common.Address, filterer bind.ContractFilterer) (*IConsensusFilterer, error) { + contract, err := bindIConsensus(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IConsensusFilterer{contract: contract}, nil +} + +// bindIConsensus binds a generic wrapper to an already deployed contract. +func bindIConsensus(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IConsensusMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IConsensus *IConsensusRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IConsensus.Contract.IConsensusCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IConsensus *IConsensusRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IConsensus.Contract.IConsensusTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IConsensus *IConsensusRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IConsensus.Contract.IConsensusTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IConsensus *IConsensusCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IConsensus.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IConsensus *IConsensusTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IConsensus.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IConsensus *IConsensusTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IConsensus.Contract.contract.Transact(opts, method, params...) +} + +// GetEpochLength is a free data retrieval call binding the contract method 0xcfe8a73b. +// +// Solidity: function getEpochLength() view returns(uint256) +func (_IConsensus *IConsensusCaller) GetEpochLength(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IConsensus.contract.Call(opts, &out, "getEpochLength") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetEpochLength is a free data retrieval call binding the contract method 0xcfe8a73b. +// +// Solidity: function getEpochLength() view returns(uint256) +func (_IConsensus *IConsensusSession) GetEpochLength() (*big.Int, error) { + return _IConsensus.Contract.GetEpochLength(&_IConsensus.CallOpts) +} + +// GetEpochLength is a free data retrieval call binding the contract method 0xcfe8a73b. +// +// Solidity: function getEpochLength() view returns(uint256) +func (_IConsensus *IConsensusCallerSession) GetEpochLength() (*big.Int, error) { + return _IConsensus.Contract.GetEpochLength(&_IConsensus.CallOpts) +} + +// WasClaimAccepted is a free data retrieval call binding the contract method 0x9618f35b. +// +// Solidity: function wasClaimAccepted(address appContract, bytes32 claim) view returns(bool) +func (_IConsensus *IConsensusCaller) WasClaimAccepted(opts *bind.CallOpts, appContract common.Address, claim [32]byte) (bool, error) { + var out []interface{} + err := _IConsensus.contract.Call(opts, &out, "wasClaimAccepted", appContract, claim) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// WasClaimAccepted is a free data retrieval call binding the contract method 0x9618f35b. +// +// Solidity: function wasClaimAccepted(address appContract, bytes32 claim) view returns(bool) +func (_IConsensus *IConsensusSession) WasClaimAccepted(appContract common.Address, claim [32]byte) (bool, error) { + return _IConsensus.Contract.WasClaimAccepted(&_IConsensus.CallOpts, appContract, claim) +} + +// WasClaimAccepted is a free data retrieval call binding the contract method 0x9618f35b. +// +// Solidity: function wasClaimAccepted(address appContract, bytes32 claim) view returns(bool) +func (_IConsensus *IConsensusCallerSession) WasClaimAccepted(appContract common.Address, claim [32]byte) (bool, error) { + return _IConsensus.Contract.WasClaimAccepted(&_IConsensus.CallOpts, appContract, claim) +} + +// SubmitClaim is a paid mutator transaction binding the contract method 0x6470af00. +// +// Solidity: function submitClaim(address appContract, uint256 lastProcessedBlockNumber, bytes32 claim) returns() +func (_IConsensus *IConsensusTransactor) SubmitClaim(opts *bind.TransactOpts, appContract common.Address, lastProcessedBlockNumber *big.Int, claim [32]byte) (*types.Transaction, error) { + return _IConsensus.contract.Transact(opts, "submitClaim", appContract, lastProcessedBlockNumber, claim) +} + +// SubmitClaim is a paid mutator transaction binding the contract method 0x6470af00. +// +// Solidity: function submitClaim(address appContract, uint256 lastProcessedBlockNumber, bytes32 claim) returns() +func (_IConsensus *IConsensusSession) SubmitClaim(appContract common.Address, lastProcessedBlockNumber *big.Int, claim [32]byte) (*types.Transaction, error) { + return _IConsensus.Contract.SubmitClaim(&_IConsensus.TransactOpts, appContract, lastProcessedBlockNumber, claim) +} + +// SubmitClaim is a paid mutator transaction binding the contract method 0x6470af00. +// +// Solidity: function submitClaim(address appContract, uint256 lastProcessedBlockNumber, bytes32 claim) returns() +func (_IConsensus *IConsensusTransactorSession) SubmitClaim(appContract common.Address, lastProcessedBlockNumber *big.Int, claim [32]byte) (*types.Transaction, error) { + return _IConsensus.Contract.SubmitClaim(&_IConsensus.TransactOpts, appContract, lastProcessedBlockNumber, claim) +} + +// IConsensusClaimAcceptanceIterator is returned from FilterClaimAcceptance and is used to iterate over the raw logs and unpacked data for ClaimAcceptance events raised by the IConsensus contract. +type IConsensusClaimAcceptanceIterator struct { + Event *IConsensusClaimAcceptance // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IConsensusClaimAcceptanceIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IConsensusClaimAcceptance) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IConsensusClaimAcceptance) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IConsensusClaimAcceptanceIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IConsensusClaimAcceptanceIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IConsensusClaimAcceptance represents a ClaimAcceptance event raised by the IConsensus contract. +type IConsensusClaimAcceptance struct { + AppContract common.Address + LastProcessedBlockNumber *big.Int + Claim [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterClaimAcceptance is a free log retrieval operation binding the contract event 0xd3e4892959c6ddb27e02bcaaebc0c1898d0f677b7360bf80339f10a8717957d3. +// +// Solidity: event ClaimAcceptance(address indexed appContract, uint256 lastProcessedBlockNumber, bytes32 claim) +func (_IConsensus *IConsensusFilterer) FilterClaimAcceptance(opts *bind.FilterOpts, appContract []common.Address) (*IConsensusClaimAcceptanceIterator, error) { + + var appContractRule []interface{} + for _, appContractItem := range appContract { + appContractRule = append(appContractRule, appContractItem) + } + + logs, sub, err := _IConsensus.contract.FilterLogs(opts, "ClaimAcceptance", appContractRule) + if err != nil { + return nil, err + } + return &IConsensusClaimAcceptanceIterator{contract: _IConsensus.contract, event: "ClaimAcceptance", logs: logs, sub: sub}, nil +} + +// WatchClaimAcceptance is a free log subscription operation binding the contract event 0xd3e4892959c6ddb27e02bcaaebc0c1898d0f677b7360bf80339f10a8717957d3. +// +// Solidity: event ClaimAcceptance(address indexed appContract, uint256 lastProcessedBlockNumber, bytes32 claim) +func (_IConsensus *IConsensusFilterer) WatchClaimAcceptance(opts *bind.WatchOpts, sink chan<- *IConsensusClaimAcceptance, appContract []common.Address) (event.Subscription, error) { + + var appContractRule []interface{} + for _, appContractItem := range appContract { + appContractRule = append(appContractRule, appContractItem) + } + + logs, sub, err := _IConsensus.contract.WatchLogs(opts, "ClaimAcceptance", appContractRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IConsensusClaimAcceptance) + if err := _IConsensus.contract.UnpackLog(event, "ClaimAcceptance", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseClaimAcceptance is a log parse operation binding the contract event 0xd3e4892959c6ddb27e02bcaaebc0c1898d0f677b7360bf80339f10a8717957d3. +// +// Solidity: event ClaimAcceptance(address indexed appContract, uint256 lastProcessedBlockNumber, bytes32 claim) +func (_IConsensus *IConsensusFilterer) ParseClaimAcceptance(log types.Log) (*IConsensusClaimAcceptance, error) { + event := new(IConsensusClaimAcceptance) + if err := _IConsensus.contract.UnpackLog(event, "ClaimAcceptance", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IConsensusClaimSubmissionIterator is returned from FilterClaimSubmission and is used to iterate over the raw logs and unpacked data for ClaimSubmission events raised by the IConsensus contract. +type IConsensusClaimSubmissionIterator struct { + Event *IConsensusClaimSubmission // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IConsensusClaimSubmissionIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IConsensusClaimSubmission) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IConsensusClaimSubmission) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IConsensusClaimSubmissionIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IConsensusClaimSubmissionIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IConsensusClaimSubmission represents a ClaimSubmission event raised by the IConsensus contract. +type IConsensusClaimSubmission struct { + Submitter common.Address + AppContract common.Address + LastProcessedBlockNumber *big.Int + Claim [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterClaimSubmission is a free log retrieval operation binding the contract event 0xf5a28e07a1b89d1ca3f9a2a7ef16bd650503a4791baf2e70dc401c21ee505f0a. +// +// Solidity: event ClaimSubmission(address indexed submitter, address indexed appContract, uint256 lastProcessedBlockNumber, bytes32 claim) +func (_IConsensus *IConsensusFilterer) FilterClaimSubmission(opts *bind.FilterOpts, submitter []common.Address, appContract []common.Address) (*IConsensusClaimSubmissionIterator, error) { + + var submitterRule []interface{} + for _, submitterItem := range submitter { + submitterRule = append(submitterRule, submitterItem) + } + var appContractRule []interface{} + for _, appContractItem := range appContract { + appContractRule = append(appContractRule, appContractItem) + } + + logs, sub, err := _IConsensus.contract.FilterLogs(opts, "ClaimSubmission", submitterRule, appContractRule) + if err != nil { + return nil, err + } + return &IConsensusClaimSubmissionIterator{contract: _IConsensus.contract, event: "ClaimSubmission", logs: logs, sub: sub}, nil +} + +// WatchClaimSubmission is a free log subscription operation binding the contract event 0xf5a28e07a1b89d1ca3f9a2a7ef16bd650503a4791baf2e70dc401c21ee505f0a. +// +// Solidity: event ClaimSubmission(address indexed submitter, address indexed appContract, uint256 lastProcessedBlockNumber, bytes32 claim) +func (_IConsensus *IConsensusFilterer) WatchClaimSubmission(opts *bind.WatchOpts, sink chan<- *IConsensusClaimSubmission, submitter []common.Address, appContract []common.Address) (event.Subscription, error) { + + var submitterRule []interface{} + for _, submitterItem := range submitter { + submitterRule = append(submitterRule, submitterItem) + } + var appContractRule []interface{} + for _, appContractItem := range appContract { + appContractRule = append(appContractRule, appContractItem) + } + + logs, sub, err := _IConsensus.contract.WatchLogs(opts, "ClaimSubmission", submitterRule, appContractRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IConsensusClaimSubmission) + if err := _IConsensus.contract.UnpackLog(event, "ClaimSubmission", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseClaimSubmission is a log parse operation binding the contract event 0xf5a28e07a1b89d1ca3f9a2a7ef16bd650503a4791baf2e70dc401c21ee505f0a. +// +// Solidity: event ClaimSubmission(address indexed submitter, address indexed appContract, uint256 lastProcessedBlockNumber, bytes32 claim) +func (_IConsensus *IConsensusFilterer) ParseClaimSubmission(log types.Log) (*IConsensusClaimSubmission, error) { + event := new(IConsensusClaimSubmission) + if err := _IConsensus.contract.UnpackLog(event, "ClaimSubmission", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/contracts/iinputbox/iinputbox.go b/pkg/contracts/iinputbox/iinputbox.go new file mode 100644 index 00000000..d8e3413d --- /dev/null +++ b/pkg/contracts/iinputbox/iinputbox.go @@ -0,0 +1,418 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package iinputbox + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IInputBoxMetaData contains all meta data concerning the IInputBox contract. +var IInputBoxMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"inputLength\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxInputLength\",\"type\":\"uint256\"}],\"name\":\"InputTooLarge\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"input\",\"type\":\"bytes\"}],\"name\":\"InputAdded\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"payload\",\"type\":\"bytes\"}],\"name\":\"addInput\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"getInputHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"}],\"name\":\"getNumberOfInputs\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// IInputBoxABI is the input ABI used to generate the binding from. +// Deprecated: Use IInputBoxMetaData.ABI instead. +var IInputBoxABI = IInputBoxMetaData.ABI + +// IInputBox is an auto generated Go binding around an Ethereum contract. +type IInputBox struct { + IInputBoxCaller // Read-only binding to the contract + IInputBoxTransactor // Write-only binding to the contract + IInputBoxFilterer // Log filterer for contract events +} + +// IInputBoxCaller is an auto generated read-only Go binding around an Ethereum contract. +type IInputBoxCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IInputBoxTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IInputBoxTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IInputBoxFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IInputBoxFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IInputBoxSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IInputBoxSession struct { + Contract *IInputBox // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IInputBoxCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IInputBoxCallerSession struct { + Contract *IInputBoxCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IInputBoxTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IInputBoxTransactorSession struct { + Contract *IInputBoxTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IInputBoxRaw is an auto generated low-level Go binding around an Ethereum contract. +type IInputBoxRaw struct { + Contract *IInputBox // Generic contract binding to access the raw methods on +} + +// IInputBoxCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IInputBoxCallerRaw struct { + Contract *IInputBoxCaller // Generic read-only contract binding to access the raw methods on +} + +// IInputBoxTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IInputBoxTransactorRaw struct { + Contract *IInputBoxTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIInputBox creates a new instance of IInputBox, bound to a specific deployed contract. +func NewIInputBox(address common.Address, backend bind.ContractBackend) (*IInputBox, error) { + contract, err := bindIInputBox(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IInputBox{IInputBoxCaller: IInputBoxCaller{contract: contract}, IInputBoxTransactor: IInputBoxTransactor{contract: contract}, IInputBoxFilterer: IInputBoxFilterer{contract: contract}}, nil +} + +// NewIInputBoxCaller creates a new read-only instance of IInputBox, bound to a specific deployed contract. +func NewIInputBoxCaller(address common.Address, caller bind.ContractCaller) (*IInputBoxCaller, error) { + contract, err := bindIInputBox(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IInputBoxCaller{contract: contract}, nil +} + +// NewIInputBoxTransactor creates a new write-only instance of IInputBox, bound to a specific deployed contract. +func NewIInputBoxTransactor(address common.Address, transactor bind.ContractTransactor) (*IInputBoxTransactor, error) { + contract, err := bindIInputBox(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IInputBoxTransactor{contract: contract}, nil +} + +// NewIInputBoxFilterer creates a new log filterer instance of IInputBox, bound to a specific deployed contract. +func NewIInputBoxFilterer(address common.Address, filterer bind.ContractFilterer) (*IInputBoxFilterer, error) { + contract, err := bindIInputBox(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IInputBoxFilterer{contract: contract}, nil +} + +// bindIInputBox binds a generic wrapper to an already deployed contract. +func bindIInputBox(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IInputBoxMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IInputBox *IInputBoxRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IInputBox.Contract.IInputBoxCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IInputBox *IInputBoxRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IInputBox.Contract.IInputBoxTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IInputBox *IInputBoxRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IInputBox.Contract.IInputBoxTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IInputBox *IInputBoxCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IInputBox.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IInputBox *IInputBoxTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IInputBox.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IInputBox *IInputBoxTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IInputBox.Contract.contract.Transact(opts, method, params...) +} + +// GetInputHash is a free data retrieval call binding the contract method 0x677087c9. +// +// Solidity: function getInputHash(address appContract, uint256 index) view returns(bytes32) +func (_IInputBox *IInputBoxCaller) GetInputHash(opts *bind.CallOpts, appContract common.Address, index *big.Int) ([32]byte, error) { + var out []interface{} + err := _IInputBox.contract.Call(opts, &out, "getInputHash", appContract, index) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetInputHash is a free data retrieval call binding the contract method 0x677087c9. +// +// Solidity: function getInputHash(address appContract, uint256 index) view returns(bytes32) +func (_IInputBox *IInputBoxSession) GetInputHash(appContract common.Address, index *big.Int) ([32]byte, error) { + return _IInputBox.Contract.GetInputHash(&_IInputBox.CallOpts, appContract, index) +} + +// GetInputHash is a free data retrieval call binding the contract method 0x677087c9. +// +// Solidity: function getInputHash(address appContract, uint256 index) view returns(bytes32) +func (_IInputBox *IInputBoxCallerSession) GetInputHash(appContract common.Address, index *big.Int) ([32]byte, error) { + return _IInputBox.Contract.GetInputHash(&_IInputBox.CallOpts, appContract, index) +} + +// GetNumberOfInputs is a free data retrieval call binding the contract method 0x61a93c87. +// +// Solidity: function getNumberOfInputs(address appContract) view returns(uint256) +func (_IInputBox *IInputBoxCaller) GetNumberOfInputs(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { + var out []interface{} + err := _IInputBox.contract.Call(opts, &out, "getNumberOfInputs", appContract) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetNumberOfInputs is a free data retrieval call binding the contract method 0x61a93c87. +// +// Solidity: function getNumberOfInputs(address appContract) view returns(uint256) +func (_IInputBox *IInputBoxSession) GetNumberOfInputs(appContract common.Address) (*big.Int, error) { + return _IInputBox.Contract.GetNumberOfInputs(&_IInputBox.CallOpts, appContract) +} + +// GetNumberOfInputs is a free data retrieval call binding the contract method 0x61a93c87. +// +// Solidity: function getNumberOfInputs(address appContract) view returns(uint256) +func (_IInputBox *IInputBoxCallerSession) GetNumberOfInputs(appContract common.Address) (*big.Int, error) { + return _IInputBox.Contract.GetNumberOfInputs(&_IInputBox.CallOpts, appContract) +} + +// AddInput is a paid mutator transaction binding the contract method 0x1789cd63. +// +// Solidity: function addInput(address appContract, bytes payload) returns(bytes32) +func (_IInputBox *IInputBoxTransactor) AddInput(opts *bind.TransactOpts, appContract common.Address, payload []byte) (*types.Transaction, error) { + return _IInputBox.contract.Transact(opts, "addInput", appContract, payload) +} + +// AddInput is a paid mutator transaction binding the contract method 0x1789cd63. +// +// Solidity: function addInput(address appContract, bytes payload) returns(bytes32) +func (_IInputBox *IInputBoxSession) AddInput(appContract common.Address, payload []byte) (*types.Transaction, error) { + return _IInputBox.Contract.AddInput(&_IInputBox.TransactOpts, appContract, payload) +} + +// AddInput is a paid mutator transaction binding the contract method 0x1789cd63. +// +// Solidity: function addInput(address appContract, bytes payload) returns(bytes32) +func (_IInputBox *IInputBoxTransactorSession) AddInput(appContract common.Address, payload []byte) (*types.Transaction, error) { + return _IInputBox.Contract.AddInput(&_IInputBox.TransactOpts, appContract, payload) +} + +// IInputBoxInputAddedIterator is returned from FilterInputAdded and is used to iterate over the raw logs and unpacked data for InputAdded events raised by the IInputBox contract. +type IInputBoxInputAddedIterator struct { + Event *IInputBoxInputAdded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IInputBoxInputAddedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IInputBoxInputAdded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IInputBoxInputAdded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IInputBoxInputAddedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IInputBoxInputAddedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IInputBoxInputAdded represents a InputAdded event raised by the IInputBox contract. +type IInputBoxInputAdded struct { + AppContract common.Address + Index *big.Int + Input []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInputAdded is a free log retrieval operation binding the contract event 0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98. +// +// Solidity: event InputAdded(address indexed appContract, uint256 indexed index, bytes input) +func (_IInputBox *IInputBoxFilterer) FilterInputAdded(opts *bind.FilterOpts, appContract []common.Address, index []*big.Int) (*IInputBoxInputAddedIterator, error) { + + var appContractRule []interface{} + for _, appContractItem := range appContract { + appContractRule = append(appContractRule, appContractItem) + } + var indexRule []interface{} + for _, indexItem := range index { + indexRule = append(indexRule, indexItem) + } + + logs, sub, err := _IInputBox.contract.FilterLogs(opts, "InputAdded", appContractRule, indexRule) + if err != nil { + return nil, err + } + return &IInputBoxInputAddedIterator{contract: _IInputBox.contract, event: "InputAdded", logs: logs, sub: sub}, nil +} + +// WatchInputAdded is a free log subscription operation binding the contract event 0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98. +// +// Solidity: event InputAdded(address indexed appContract, uint256 indexed index, bytes input) +func (_IInputBox *IInputBoxFilterer) WatchInputAdded(opts *bind.WatchOpts, sink chan<- *IInputBoxInputAdded, appContract []common.Address, index []*big.Int) (event.Subscription, error) { + + var appContractRule []interface{} + for _, appContractItem := range appContract { + appContractRule = append(appContractRule, appContractItem) + } + var indexRule []interface{} + for _, indexItem := range index { + indexRule = append(indexRule, indexItem) + } + + logs, sub, err := _IInputBox.contract.WatchLogs(opts, "InputAdded", appContractRule, indexRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IInputBoxInputAdded) + if err := _IInputBox.contract.UnpackLog(event, "InputAdded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseInputAdded is a log parse operation binding the contract event 0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98. +// +// Solidity: event InputAdded(address indexed appContract, uint256 indexed index, bytes input) +func (_IInputBox *IInputBoxFilterer) ParseInputAdded(log types.Log) (*IInputBoxInputAdded, error) { + event := new(IInputBoxInputAdded) + if err := _IInputBox.contract.UnpackLog(event, "InputAdded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/contracts/inputs/inputs.go b/pkg/contracts/inputs/inputs.go new file mode 100644 index 00000000..24203d61 --- /dev/null +++ b/pkg/contracts/inputs/inputs.go @@ -0,0 +1,202 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package inputs + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// InputsMetaData contains all meta data concerning the Inputs contract. +var InputsMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"appContract\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"msgSender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"blockTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"prevRandao\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"payload\",\"type\":\"bytes\"}],\"name\":\"EvmAdvance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// InputsABI is the input ABI used to generate the binding from. +// Deprecated: Use InputsMetaData.ABI instead. +var InputsABI = InputsMetaData.ABI + +// Inputs is an auto generated Go binding around an Ethereum contract. +type Inputs struct { + InputsCaller // Read-only binding to the contract + InputsTransactor // Write-only binding to the contract + InputsFilterer // Log filterer for contract events +} + +// InputsCaller is an auto generated read-only Go binding around an Ethereum contract. +type InputsCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// InputsTransactor is an auto generated write-only Go binding around an Ethereum contract. +type InputsTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// InputsFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type InputsFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// InputsSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type InputsSession struct { + Contract *Inputs // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// InputsCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type InputsCallerSession struct { + Contract *InputsCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// InputsTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type InputsTransactorSession struct { + Contract *InputsTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// InputsRaw is an auto generated low-level Go binding around an Ethereum contract. +type InputsRaw struct { + Contract *Inputs // Generic contract binding to access the raw methods on +} + +// InputsCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type InputsCallerRaw struct { + Contract *InputsCaller // Generic read-only contract binding to access the raw methods on +} + +// InputsTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type InputsTransactorRaw struct { + Contract *InputsTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewInputs creates a new instance of Inputs, bound to a specific deployed contract. +func NewInputs(address common.Address, backend bind.ContractBackend) (*Inputs, error) { + contract, err := bindInputs(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Inputs{InputsCaller: InputsCaller{contract: contract}, InputsTransactor: InputsTransactor{contract: contract}, InputsFilterer: InputsFilterer{contract: contract}}, nil +} + +// NewInputsCaller creates a new read-only instance of Inputs, bound to a specific deployed contract. +func NewInputsCaller(address common.Address, caller bind.ContractCaller) (*InputsCaller, error) { + contract, err := bindInputs(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &InputsCaller{contract: contract}, nil +} + +// NewInputsTransactor creates a new write-only instance of Inputs, bound to a specific deployed contract. +func NewInputsTransactor(address common.Address, transactor bind.ContractTransactor) (*InputsTransactor, error) { + contract, err := bindInputs(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &InputsTransactor{contract: contract}, nil +} + +// NewInputsFilterer creates a new log filterer instance of Inputs, bound to a specific deployed contract. +func NewInputsFilterer(address common.Address, filterer bind.ContractFilterer) (*InputsFilterer, error) { + contract, err := bindInputs(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &InputsFilterer{contract: contract}, nil +} + +// bindInputs binds a generic wrapper to an already deployed contract. +func bindInputs(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := InputsMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Inputs *InputsRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Inputs.Contract.InputsCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Inputs *InputsRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Inputs.Contract.InputsTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Inputs *InputsRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Inputs.Contract.InputsTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Inputs *InputsCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Inputs.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Inputs *InputsTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Inputs.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Inputs *InputsTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Inputs.Contract.contract.Transact(opts, method, params...) +} + +// EvmAdvance is a paid mutator transaction binding the contract method 0x415bf363. +// +// Solidity: function EvmAdvance(uint256 chainId, address appContract, address msgSender, uint256 blockNumber, uint256 blockTimestamp, uint256 prevRandao, uint256 index, bytes payload) returns() +func (_Inputs *InputsTransactor) EvmAdvance(opts *bind.TransactOpts, chainId *big.Int, appContract common.Address, msgSender common.Address, blockNumber *big.Int, blockTimestamp *big.Int, prevRandao *big.Int, index *big.Int, payload []byte) (*types.Transaction, error) { + return _Inputs.contract.Transact(opts, "EvmAdvance", chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload) +} + +// EvmAdvance is a paid mutator transaction binding the contract method 0x415bf363. +// +// Solidity: function EvmAdvance(uint256 chainId, address appContract, address msgSender, uint256 blockNumber, uint256 blockTimestamp, uint256 prevRandao, uint256 index, bytes payload) returns() +func (_Inputs *InputsSession) EvmAdvance(chainId *big.Int, appContract common.Address, msgSender common.Address, blockNumber *big.Int, blockTimestamp *big.Int, prevRandao *big.Int, index *big.Int, payload []byte) (*types.Transaction, error) { + return _Inputs.Contract.EvmAdvance(&_Inputs.TransactOpts, chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload) +} + +// EvmAdvance is a paid mutator transaction binding the contract method 0x415bf363. +// +// Solidity: function EvmAdvance(uint256 chainId, address appContract, address msgSender, uint256 blockNumber, uint256 blockTimestamp, uint256 prevRandao, uint256 index, bytes payload) returns() +func (_Inputs *InputsTransactorSession) EvmAdvance(chainId *big.Int, appContract common.Address, msgSender common.Address, blockNumber *big.Int, blockTimestamp *big.Int, prevRandao *big.Int, index *big.Int, payload []byte) (*types.Transaction, error) { + return _Inputs.Contract.EvmAdvance(&_Inputs.TransactOpts, chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload) +} diff --git a/pkg/contracts/iselfhostedapplicationfactory/iselfhostedapplicationfactory.go b/pkg/contracts/iselfhostedapplicationfactory/iselfhostedapplicationfactory.go new file mode 100644 index 00000000..912399ea --- /dev/null +++ b/pkg/contracts/iselfhostedapplicationfactory/iselfhostedapplicationfactory.go @@ -0,0 +1,296 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package iselfhostedapplicationfactory + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// ISelfHostedApplicationFactoryMetaData contains all meta data concerning the ISelfHostedApplicationFactory contract. +var ISelfHostedApplicationFactoryMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"authorityOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"epochLength\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"appOwner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"templateHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"}],\"name\":\"calculateAddresses\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"authorityOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"epochLength\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"appOwner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"templateHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"}],\"name\":\"deployContracts\",\"outputs\":[{\"internalType\":\"contractIApplication\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"contractIAuthority\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getApplicationFactory\",\"outputs\":[{\"internalType\":\"contractIApplicationFactory\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAuthorityFactory\",\"outputs\":[{\"internalType\":\"contractIAuthorityFactory\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// ISelfHostedApplicationFactoryABI is the input ABI used to generate the binding from. +// Deprecated: Use ISelfHostedApplicationFactoryMetaData.ABI instead. +var ISelfHostedApplicationFactoryABI = ISelfHostedApplicationFactoryMetaData.ABI + +// ISelfHostedApplicationFactory is an auto generated Go binding around an Ethereum contract. +type ISelfHostedApplicationFactory struct { + ISelfHostedApplicationFactoryCaller // Read-only binding to the contract + ISelfHostedApplicationFactoryTransactor // Write-only binding to the contract + ISelfHostedApplicationFactoryFilterer // Log filterer for contract events +} + +// ISelfHostedApplicationFactoryCaller is an auto generated read-only Go binding around an Ethereum contract. +type ISelfHostedApplicationFactoryCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ISelfHostedApplicationFactoryTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ISelfHostedApplicationFactoryTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ISelfHostedApplicationFactoryFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ISelfHostedApplicationFactoryFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ISelfHostedApplicationFactorySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ISelfHostedApplicationFactorySession struct { + Contract *ISelfHostedApplicationFactory // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ISelfHostedApplicationFactoryCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ISelfHostedApplicationFactoryCallerSession struct { + Contract *ISelfHostedApplicationFactoryCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ISelfHostedApplicationFactoryTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ISelfHostedApplicationFactoryTransactorSession struct { + Contract *ISelfHostedApplicationFactoryTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ISelfHostedApplicationFactoryRaw is an auto generated low-level Go binding around an Ethereum contract. +type ISelfHostedApplicationFactoryRaw struct { + Contract *ISelfHostedApplicationFactory // Generic contract binding to access the raw methods on +} + +// ISelfHostedApplicationFactoryCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ISelfHostedApplicationFactoryCallerRaw struct { + Contract *ISelfHostedApplicationFactoryCaller // Generic read-only contract binding to access the raw methods on +} + +// ISelfHostedApplicationFactoryTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ISelfHostedApplicationFactoryTransactorRaw struct { + Contract *ISelfHostedApplicationFactoryTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewISelfHostedApplicationFactory creates a new instance of ISelfHostedApplicationFactory, bound to a specific deployed contract. +func NewISelfHostedApplicationFactory(address common.Address, backend bind.ContractBackend) (*ISelfHostedApplicationFactory, error) { + contract, err := bindISelfHostedApplicationFactory(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &ISelfHostedApplicationFactory{ISelfHostedApplicationFactoryCaller: ISelfHostedApplicationFactoryCaller{contract: contract}, ISelfHostedApplicationFactoryTransactor: ISelfHostedApplicationFactoryTransactor{contract: contract}, ISelfHostedApplicationFactoryFilterer: ISelfHostedApplicationFactoryFilterer{contract: contract}}, nil +} + +// NewISelfHostedApplicationFactoryCaller creates a new read-only instance of ISelfHostedApplicationFactory, bound to a specific deployed contract. +func NewISelfHostedApplicationFactoryCaller(address common.Address, caller bind.ContractCaller) (*ISelfHostedApplicationFactoryCaller, error) { + contract, err := bindISelfHostedApplicationFactory(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ISelfHostedApplicationFactoryCaller{contract: contract}, nil +} + +// NewISelfHostedApplicationFactoryTransactor creates a new write-only instance of ISelfHostedApplicationFactory, bound to a specific deployed contract. +func NewISelfHostedApplicationFactoryTransactor(address common.Address, transactor bind.ContractTransactor) (*ISelfHostedApplicationFactoryTransactor, error) { + contract, err := bindISelfHostedApplicationFactory(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ISelfHostedApplicationFactoryTransactor{contract: contract}, nil +} + +// NewISelfHostedApplicationFactoryFilterer creates a new log filterer instance of ISelfHostedApplicationFactory, bound to a specific deployed contract. +func NewISelfHostedApplicationFactoryFilterer(address common.Address, filterer bind.ContractFilterer) (*ISelfHostedApplicationFactoryFilterer, error) { + contract, err := bindISelfHostedApplicationFactory(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ISelfHostedApplicationFactoryFilterer{contract: contract}, nil +} + +// bindISelfHostedApplicationFactory binds a generic wrapper to an already deployed contract. +func bindISelfHostedApplicationFactory(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ISelfHostedApplicationFactoryMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ISelfHostedApplicationFactory.Contract.ISelfHostedApplicationFactoryCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.Contract.ISelfHostedApplicationFactoryTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.Contract.ISelfHostedApplicationFactoryTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ISelfHostedApplicationFactory.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.Contract.contract.Transact(opts, method, params...) +} + +// CalculateAddresses is a free data retrieval call binding the contract method 0xde4d53fd. +// +// Solidity: function calculateAddresses(address authorityOwner, uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt) view returns(address, address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCaller) CalculateAddresses(opts *bind.CallOpts, authorityOwner common.Address, epochLength *big.Int, appOwner common.Address, templateHash [32]byte, salt [32]byte) (common.Address, common.Address, error) { + var out []interface{} + err := _ISelfHostedApplicationFactory.contract.Call(opts, &out, "calculateAddresses", authorityOwner, epochLength, appOwner, templateHash, salt) + + if err != nil { + return *new(common.Address), *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + + return out0, out1, err + +} + +// CalculateAddresses is a free data retrieval call binding the contract method 0xde4d53fd. +// +// Solidity: function calculateAddresses(address authorityOwner, uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt) view returns(address, address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactorySession) CalculateAddresses(authorityOwner common.Address, epochLength *big.Int, appOwner common.Address, templateHash [32]byte, salt [32]byte) (common.Address, common.Address, error) { + return _ISelfHostedApplicationFactory.Contract.CalculateAddresses(&_ISelfHostedApplicationFactory.CallOpts, authorityOwner, epochLength, appOwner, templateHash, salt) +} + +// CalculateAddresses is a free data retrieval call binding the contract method 0xde4d53fd. +// +// Solidity: function calculateAddresses(address authorityOwner, uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt) view returns(address, address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCallerSession) CalculateAddresses(authorityOwner common.Address, epochLength *big.Int, appOwner common.Address, templateHash [32]byte, salt [32]byte) (common.Address, common.Address, error) { + return _ISelfHostedApplicationFactory.Contract.CalculateAddresses(&_ISelfHostedApplicationFactory.CallOpts, authorityOwner, epochLength, appOwner, templateHash, salt) +} + +// GetApplicationFactory is a free data retrieval call binding the contract method 0xe63d50ff. +// +// Solidity: function getApplicationFactory() view returns(address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCaller) GetApplicationFactory(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ISelfHostedApplicationFactory.contract.Call(opts, &out, "getApplicationFactory") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetApplicationFactory is a free data retrieval call binding the contract method 0xe63d50ff. +// +// Solidity: function getApplicationFactory() view returns(address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactorySession) GetApplicationFactory() (common.Address, error) { + return _ISelfHostedApplicationFactory.Contract.GetApplicationFactory(&_ISelfHostedApplicationFactory.CallOpts) +} + +// GetApplicationFactory is a free data retrieval call binding the contract method 0xe63d50ff. +// +// Solidity: function getApplicationFactory() view returns(address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCallerSession) GetApplicationFactory() (common.Address, error) { + return _ISelfHostedApplicationFactory.Contract.GetApplicationFactory(&_ISelfHostedApplicationFactory.CallOpts) +} + +// GetAuthorityFactory is a free data retrieval call binding the contract method 0x75689f83. +// +// Solidity: function getAuthorityFactory() view returns(address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCaller) GetAuthorityFactory(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ISelfHostedApplicationFactory.contract.Call(opts, &out, "getAuthorityFactory") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetAuthorityFactory is a free data retrieval call binding the contract method 0x75689f83. +// +// Solidity: function getAuthorityFactory() view returns(address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactorySession) GetAuthorityFactory() (common.Address, error) { + return _ISelfHostedApplicationFactory.Contract.GetAuthorityFactory(&_ISelfHostedApplicationFactory.CallOpts) +} + +// GetAuthorityFactory is a free data retrieval call binding the contract method 0x75689f83. +// +// Solidity: function getAuthorityFactory() view returns(address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryCallerSession) GetAuthorityFactory() (common.Address, error) { + return _ISelfHostedApplicationFactory.Contract.GetAuthorityFactory(&_ISelfHostedApplicationFactory.CallOpts) +} + +// DeployContracts is a paid mutator transaction binding the contract method 0xffc643ca. +// +// Solidity: function deployContracts(address authorityOwner, uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt) returns(address, address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryTransactor) DeployContracts(opts *bind.TransactOpts, authorityOwner common.Address, epochLength *big.Int, appOwner common.Address, templateHash [32]byte, salt [32]byte) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.contract.Transact(opts, "deployContracts", authorityOwner, epochLength, appOwner, templateHash, salt) +} + +// DeployContracts is a paid mutator transaction binding the contract method 0xffc643ca. +// +// Solidity: function deployContracts(address authorityOwner, uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt) returns(address, address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactorySession) DeployContracts(authorityOwner common.Address, epochLength *big.Int, appOwner common.Address, templateHash [32]byte, salt [32]byte) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.Contract.DeployContracts(&_ISelfHostedApplicationFactory.TransactOpts, authorityOwner, epochLength, appOwner, templateHash, salt) +} + +// DeployContracts is a paid mutator transaction binding the contract method 0xffc643ca. +// +// Solidity: function deployContracts(address authorityOwner, uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt) returns(address, address) +func (_ISelfHostedApplicationFactory *ISelfHostedApplicationFactoryTransactorSession) DeployContracts(authorityOwner common.Address, epochLength *big.Int, appOwner common.Address, templateHash [32]byte, salt [32]byte) (*types.Transaction, error) { + return _ISelfHostedApplicationFactory.Contract.DeployContracts(&_ISelfHostedApplicationFactory.TransactOpts, authorityOwner, epochLength, appOwner, templateHash, salt) +} diff --git a/pkg/contracts/main.go b/pkg/contracts/main.go new file mode 100644 index 00000000..b9e4f2d2 --- /dev/null +++ b/pkg/contracts/main.go @@ -0,0 +1,10 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// This package contains the Go bindings for the Cartesi Rollups contracts. +// These bindings are generated automatically by the `generate` binary. +// To regenerate the bindings, run `go generate` from this directory. +// To modify the list of contracts, edit the `generate` binary. +package contracts + +//go:generate go run ./generate diff --git a/pkg/contracts/outputs/outputs.go b/pkg/contracts/outputs/outputs.go new file mode 100644 index 00000000..172cbc00 --- /dev/null +++ b/pkg/contracts/outputs/outputs.go @@ -0,0 +1,244 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package outputs + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// OutputsMetaData contains all meta data concerning the Outputs contract. +var OutputsMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"payload\",\"type\":\"bytes\"}],\"name\":\"DelegateCallVoucher\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"payload\",\"type\":\"bytes\"}],\"name\":\"Notice\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"payload\",\"type\":\"bytes\"}],\"name\":\"Voucher\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// OutputsABI is the input ABI used to generate the binding from. +// Deprecated: Use OutputsMetaData.ABI instead. +var OutputsABI = OutputsMetaData.ABI + +// Outputs is an auto generated Go binding around an Ethereum contract. +type Outputs struct { + OutputsCaller // Read-only binding to the contract + OutputsTransactor // Write-only binding to the contract + OutputsFilterer // Log filterer for contract events +} + +// OutputsCaller is an auto generated read-only Go binding around an Ethereum contract. +type OutputsCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// OutputsTransactor is an auto generated write-only Go binding around an Ethereum contract. +type OutputsTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// OutputsFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type OutputsFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// OutputsSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type OutputsSession struct { + Contract *Outputs // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// OutputsCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type OutputsCallerSession struct { + Contract *OutputsCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// OutputsTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type OutputsTransactorSession struct { + Contract *OutputsTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// OutputsRaw is an auto generated low-level Go binding around an Ethereum contract. +type OutputsRaw struct { + Contract *Outputs // Generic contract binding to access the raw methods on +} + +// OutputsCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type OutputsCallerRaw struct { + Contract *OutputsCaller // Generic read-only contract binding to access the raw methods on +} + +// OutputsTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type OutputsTransactorRaw struct { + Contract *OutputsTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewOutputs creates a new instance of Outputs, bound to a specific deployed contract. +func NewOutputs(address common.Address, backend bind.ContractBackend) (*Outputs, error) { + contract, err := bindOutputs(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Outputs{OutputsCaller: OutputsCaller{contract: contract}, OutputsTransactor: OutputsTransactor{contract: contract}, OutputsFilterer: OutputsFilterer{contract: contract}}, nil +} + +// NewOutputsCaller creates a new read-only instance of Outputs, bound to a specific deployed contract. +func NewOutputsCaller(address common.Address, caller bind.ContractCaller) (*OutputsCaller, error) { + contract, err := bindOutputs(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &OutputsCaller{contract: contract}, nil +} + +// NewOutputsTransactor creates a new write-only instance of Outputs, bound to a specific deployed contract. +func NewOutputsTransactor(address common.Address, transactor bind.ContractTransactor) (*OutputsTransactor, error) { + contract, err := bindOutputs(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &OutputsTransactor{contract: contract}, nil +} + +// NewOutputsFilterer creates a new log filterer instance of Outputs, bound to a specific deployed contract. +func NewOutputsFilterer(address common.Address, filterer bind.ContractFilterer) (*OutputsFilterer, error) { + contract, err := bindOutputs(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &OutputsFilterer{contract: contract}, nil +} + +// bindOutputs binds a generic wrapper to an already deployed contract. +func bindOutputs(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := OutputsMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Outputs *OutputsRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Outputs.Contract.OutputsCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Outputs *OutputsRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Outputs.Contract.OutputsTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Outputs *OutputsRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Outputs.Contract.OutputsTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Outputs *OutputsCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Outputs.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Outputs *OutputsTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Outputs.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Outputs *OutputsTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Outputs.Contract.contract.Transact(opts, method, params...) +} + +// DelegateCallVoucher is a paid mutator transaction binding the contract method 0x10321e8b. +// +// Solidity: function DelegateCallVoucher(address destination, bytes payload) returns() +func (_Outputs *OutputsTransactor) DelegateCallVoucher(opts *bind.TransactOpts, destination common.Address, payload []byte) (*types.Transaction, error) { + return _Outputs.contract.Transact(opts, "DelegateCallVoucher", destination, payload) +} + +// DelegateCallVoucher is a paid mutator transaction binding the contract method 0x10321e8b. +// +// Solidity: function DelegateCallVoucher(address destination, bytes payload) returns() +func (_Outputs *OutputsSession) DelegateCallVoucher(destination common.Address, payload []byte) (*types.Transaction, error) { + return _Outputs.Contract.DelegateCallVoucher(&_Outputs.TransactOpts, destination, payload) +} + +// DelegateCallVoucher is a paid mutator transaction binding the contract method 0x10321e8b. +// +// Solidity: function DelegateCallVoucher(address destination, bytes payload) returns() +func (_Outputs *OutputsTransactorSession) DelegateCallVoucher(destination common.Address, payload []byte) (*types.Transaction, error) { + return _Outputs.Contract.DelegateCallVoucher(&_Outputs.TransactOpts, destination, payload) +} + +// Notice is a paid mutator transaction binding the contract method 0xc258d6e5. +// +// Solidity: function Notice(bytes payload) returns() +func (_Outputs *OutputsTransactor) Notice(opts *bind.TransactOpts, payload []byte) (*types.Transaction, error) { + return _Outputs.contract.Transact(opts, "Notice", payload) +} + +// Notice is a paid mutator transaction binding the contract method 0xc258d6e5. +// +// Solidity: function Notice(bytes payload) returns() +func (_Outputs *OutputsSession) Notice(payload []byte) (*types.Transaction, error) { + return _Outputs.Contract.Notice(&_Outputs.TransactOpts, payload) +} + +// Notice is a paid mutator transaction binding the contract method 0xc258d6e5. +// +// Solidity: function Notice(bytes payload) returns() +func (_Outputs *OutputsTransactorSession) Notice(payload []byte) (*types.Transaction, error) { + return _Outputs.Contract.Notice(&_Outputs.TransactOpts, payload) +} + +// Voucher is a paid mutator transaction binding the contract method 0x237a816f. +// +// Solidity: function Voucher(address destination, uint256 value, bytes payload) returns() +func (_Outputs *OutputsTransactor) Voucher(opts *bind.TransactOpts, destination common.Address, value *big.Int, payload []byte) (*types.Transaction, error) { + return _Outputs.contract.Transact(opts, "Voucher", destination, value, payload) +} + +// Voucher is a paid mutator transaction binding the contract method 0x237a816f. +// +// Solidity: function Voucher(address destination, uint256 value, bytes payload) returns() +func (_Outputs *OutputsSession) Voucher(destination common.Address, value *big.Int, payload []byte) (*types.Transaction, error) { + return _Outputs.Contract.Voucher(&_Outputs.TransactOpts, destination, value, payload) +} + +// Voucher is a paid mutator transaction binding the contract method 0x237a816f. +// +// Solidity: function Voucher(address destination, uint256 value, bytes payload) returns() +func (_Outputs *OutputsTransactorSession) Voucher(destination common.Address, value *big.Int, payload []byte) (*types.Transaction, error) { + return _Outputs.Contract.Voucher(&_Outputs.TransactOpts, destination, value, payload) +}