├── .dockerignore ├── config ├── Readme.md ├── genesis_test.go ├── genesis.go ├── errors.go ├── logger.go └── deploy_config.go ├── docs ├── commitment │ ├── loop.md │ └── loop.png ├── sync_loop.png ├── coordinator.png ├── tx_statuses.png ├── account_tree.png ├── batch │ ├── submission.png │ └── structure.md ├── benchmarks │ ├── sync.prof │ ├── creation.prof │ ├── after-bench-update.prof │ ├── sync-1s-block-timer.prof │ ├── creation-1s-block-timer.prof │ └── after-serialization-optimizations.prof ├── claim_optimistic.png ├── deploy_sequence.png ├── user_state_tree.png ├── proposals │ ├── mass_c2t.png │ ├── tx_statuses.png │ ├── mempool │ │ ├── target.png │ │ ├── users.png │ │ └── current.png │ └── state_tree_overlays.png ├── Readme.md ├── contracts-dependancies.png ├── rollup_loop_data_structures.png ├── commander_troubleshooting_flowchart.png ├── SUMMARY.md ├── withdrawals.md ├── deposits.md ├── mass_migration.md └── naming.md ├── testutils ├── simulator │ ├── Readme.md │ └── backend.go ├── deposits.go ├── wallets.go ├── test.go └── iterate_index.go ├── utils ├── generate.go ├── Readme.md ├── string_in_slice.go ├── map.go ├── event_before.go ├── project_root.go ├── chain_spec.go ├── consts │ └── consts.go ├── units.go ├── camel_case_to_snake_case.go ├── camel_case_to_snake_case_test.go ├── map_test.go ├── random_test.go ├── bytes.go ├── hash.go ├── bytes_test.go ├── badger.go ├── units_test.go ├── badger_test.go ├── random.go ├── ref │ └── ref.go └── any_error.go ├── bls ├── sdk │ ├── Readme.md │ ├── Makefile │ └── clangwrap.sh ├── Readme.md ├── wallet_test.go ├── domain.go ├── aggregated_signature_test.go ├── domain_test.go ├── public_key.go └── signature_test.go ├── commander ├── applier │ ├── Readme.md │ ├── applier.go │ ├── apply_deposits.go │ ├── synced_tx.go │ └── apply_transfer.go ├── disputer │ ├── Readme.md │ ├── context.go │ └── dispute_signature_test_suite.go ├── syncer │ ├── Readme.md │ ├── deposits_context.go │ └── txs_context.go ├── tracker │ ├── Readme.md │ └── sending.go ├── prover │ ├── context.go │ └── vacancy_proof.go ├── executor │ ├── Readme.md │ ├── errors.go │ ├── tx_priority.go │ ├── new_commitment.go │ ├── deposit_context.go │ ├── tx_queue.go │ ├── rollup_loop_context.go │ ├── transactions_context_test_suite.go │ └── execution_context_test_suite.go ├── get_chain_connection.go ├── Readme.md ├── errors.go ├── validate_state_root.go ├── rollup_controls.go ├── verify_commitment.go ├── workers.go └── lifecycle.go ├── api ├── version.go ├── get_genesis_accounts.go ├── get_status.go ├── api_error_test.go ├── version_test.go ├── admin │ ├── get_failed_transactions.go │ ├── configure.go │ ├── authentication.go │ ├── get_pending_transactions.go │ ├── api.go │ ├── authentication_test.go │ └── mempool.go ├── middleware │ ├── measure_request_duration.go │ └── default_handler.go ├── log_transaction.go ├── get_status_test.go ├── metrics.go ├── rpc │ ├── server.go │ └── server_test.go ├── sign_transfer.go ├── sign_mass_migration.go ├── calculate_transfer_status.go ├── sign_create2transfer.go ├── get_public_key.go ├── get_public_key_proof.go ├── get_transaction.go ├── api_error.go ├── api_test.go └── get_user_state.go ├── db ├── Readme.md ├── controller_adapter.go ├── encoder_test.go ├── iterator_test.go ├── test_db.go └── tx_controller.go ├── models ├── dto │ ├── merkle_path.go │ ├── configure.go │ ├── transaction_receipt.go │ ├── recompute_pending_state.go │ ├── pending_batch.go │ ├── user_state.go │ ├── network_info.go │ ├── deposit.go │ ├── transaction_base.go │ ├── generic_transaction.go │ └── dispute_proofs.go ├── vacancy_proof.go ├── enums │ ├── healthstatus │ │ └── health_status.go │ ├── result │ │ └── dispute_result.go │ ├── errors │ │ └── unsupported.go │ ├── batchstatus │ │ └── batch_status.go │ └── batchtype │ │ └── batch_type.go ├── Readme.md ├── merkle_tree.go ├── registered_spoke.go ├── registered_token.go ├── tx_error.go ├── witness.go ├── bh_utils.go ├── pending_batch.go ├── commitment_id_test.go ├── batch.go ├── stored │ ├── sizes.go │ ├── state_leaf_test.go │ ├── failed_tx_test.go │ ├── batch_test.go │ ├── batched_tx_test.go │ ├── primitive_encoder.go │ └── primitive_encoder_test.go ├── dispute_signature.go ├── commitment.go ├── commitment_id.go ├── timestamp_test.go ├── pending_stake_withdrawal.go ├── transfer.go ├── commitment_base_test.go ├── genesis_account_test.go ├── genesis_account.go ├── mass_migration.go ├── dispute_transition.go ├── timestamp.go ├── create2transfer.go ├── commitment_base.go ├── deposit_commitment.go ├── transaction_base.go ├── meta_test.go └── commitment_slot.go ├── e2e ├── Readme.md ├── geth-data │ ├── keystore │ │ ├── UTC--2021-06-07T12-34-01.339648000Z--9f758331b439c1b664e86f2050f2360370f06849 │ │ └── UTC--2022-02-24T08-41-23.950510000Z--644cad6dd685f042cf9a2c4979476638e414c39c │ └── fundSecondAccount.sh └── setup │ ├── create_unregistered_wallets.go │ ├── local_commander.go │ └── commander.go ├── .gitmodules ├── main ├── Readme.md ├── export.go ├── deploy.go ├── utils.go └── start.go ├── eth ├── Readme.md ├── get_nonce.go ├── proposer_test.go ├── chain │ └── rpc_backend.go ├── get_blocks_to_finalise.go ├── domain.go ├── raw_transact.go ├── withdraw_stake.go ├── proposer.go ├── commitment.go ├── deployer │ └── proof_of_authority.go ├── get_batch.go ├── domain_test.go ├── filter_logs.go ├── errors.go ├── register_account_test.go ├── get_blocks_to_finalise_test.go ├── contracts.go └── utils.go ├── contracts ├── Readme.md ├── spokeregistry │ └── events.go ├── tokenregistry │ └── events.go ├── depositmanager │ └── events.go └── accountregistry │ └── events.go ├── metrics ├── utils.go ├── measure_duration.go ├── save_histogram_measurement.go ├── server.go ├── sync_metrics.go ├── types.go ├── const.go └── blockchain_metrics.go ├── storage ├── Readme.md ├── mm_commitment.go ├── tx_commitment.go ├── deposit_commitment.go ├── mass_migration.go ├── create2transfer.go ├── transfer.go ├── temporary_storage.go ├── state_update.go ├── account.go ├── test_storage.go ├── registered_spoke.go └── registered_token.go ├── book.toml ├── .editorconfig ├── deploy ├── hubble │ ├── templates │ │ ├── headless.yaml │ │ ├── serviceaccount.yaml │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ └── ebs-wipe.yaml │ ├── values-stage.yaml │ ├── .helmignore │ ├── values.yaml │ └── Chart.yaml └── geth │ ├── templates │ ├── serviceaccount.yaml │ ├── service.yaml │ └── service-headless.yaml │ ├── .helmignore │ └── Chart.yaml ├── deployer-config.example.yaml ├── .gitignore ├── encoder ├── meta.go └── encoder.go ├── docker-compose.yml ├── scripts └── export_accounts.go ├── o11y └── logging.go ├── .github └── workflows │ ├── rollback-prod.yaml │ ├── rollback-stage.yaml │ └── deploy-stage-contracts.yml ├── client └── batch.go └── commander-config.example.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | Dockerfile 3 | build 4 | -------------------------------------------------------------------------------- /config/Readme.md: -------------------------------------------------------------------------------- 1 | # Config loading code 2 | 3 | -------------------------------------------------------------------------------- /docs/commitment/loop.md: -------------------------------------------------------------------------------- 1 | # ♻️ Commitment loop 2 | 3 | ![](loop.png) 4 | -------------------------------------------------------------------------------- /testutils/simulator/Readme.md: -------------------------------------------------------------------------------- 1 | # In-process Ethereum Geth node for testing 2 | -------------------------------------------------------------------------------- /utils/generate.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | //go:generate sh -c "python3 generate.py" 4 | -------------------------------------------------------------------------------- /docs/sync_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/sync_loop.png -------------------------------------------------------------------------------- /bls/sdk/Readme.md: -------------------------------------------------------------------------------- 1 | # BLS Wrapper for Mobile client 2 | 3 | Compiles the `bls` to iOS or Android. 4 | -------------------------------------------------------------------------------- /commander/applier/Readme.md: -------------------------------------------------------------------------------- 1 | # Applier 2 | 3 | * Applying single transactions 4 | * Applying fees 5 | -------------------------------------------------------------------------------- /docs/coordinator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/coordinator.png -------------------------------------------------------------------------------- /docs/tx_statuses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/tx_statuses.png -------------------------------------------------------------------------------- /api/version.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | func (a *API) GetVersion() string { 4 | return a.cfg.Version 5 | } 6 | -------------------------------------------------------------------------------- /docs/account_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/account_tree.png -------------------------------------------------------------------------------- /docs/batch/submission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/batch/submission.png -------------------------------------------------------------------------------- /docs/benchmarks/sync.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/benchmarks/sync.prof -------------------------------------------------------------------------------- /docs/claim_optimistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/claim_optimistic.png -------------------------------------------------------------------------------- /docs/commitment/loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/commitment/loop.png -------------------------------------------------------------------------------- /docs/deploy_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/deploy_sequence.png -------------------------------------------------------------------------------- /docs/user_state_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/user_state_tree.png -------------------------------------------------------------------------------- /bls/sdk/Makefile: -------------------------------------------------------------------------------- 1 | 2 | ios: 3 | gomobile bind -target=ios . 4 | 5 | android: 6 | gomobile bind -target=android . 7 | -------------------------------------------------------------------------------- /db/Readme.md: -------------------------------------------------------------------------------- 1 | # Database abstraction 2 | 3 | Wrapper around the Badger database taking care of type marshalling. 4 | -------------------------------------------------------------------------------- /docs/proposals/mass_c2t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/proposals/mass_c2t.png -------------------------------------------------------------------------------- /models/dto/merkle_path.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type MerklePath struct { 4 | Path uint32 5 | Depth uint8 6 | } 7 | -------------------------------------------------------------------------------- /docs/benchmarks/creation.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/benchmarks/creation.prof -------------------------------------------------------------------------------- /docs/proposals/tx_statuses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/proposals/tx_statuses.png -------------------------------------------------------------------------------- /commander/disputer/Readme.md: -------------------------------------------------------------------------------- 1 | # Disputer 2 | 3 | * Preparing fraud proof data 4 | * Submitting dispute transactions on chain 5 | -------------------------------------------------------------------------------- /docs/Readme.md: -------------------------------------------------------------------------------- 1 | # 🔭 Hubble Documentation 2 | 3 | Technical documentation on hubble architecture and implementation. 4 | 5 | -------------------------------------------------------------------------------- /docs/contracts-dependancies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/contracts-dependancies.png -------------------------------------------------------------------------------- /docs/proposals/mempool/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/proposals/mempool/target.png -------------------------------------------------------------------------------- /docs/proposals/mempool/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/proposals/mempool/users.png -------------------------------------------------------------------------------- /docs/proposals/mempool/current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/proposals/mempool/current.png -------------------------------------------------------------------------------- /docs/proposals/state_tree_overlays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/proposals/state_tree_overlays.png -------------------------------------------------------------------------------- /docs/rollup_loop_data_structures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/rollup_loop_data_structures.png -------------------------------------------------------------------------------- /docs/benchmarks/after-bench-update.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/benchmarks/after-bench-update.prof -------------------------------------------------------------------------------- /docs/benchmarks/sync-1s-block-timer.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/benchmarks/sync-1s-block-timer.prof -------------------------------------------------------------------------------- /models/vacancy_proof.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SubtreeVacancyProof struct { 4 | PathAtDepth uint32 5 | Witness Witness 6 | } 7 | -------------------------------------------------------------------------------- /e2e/Readme.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests and benchmarks 2 | 3 | Can run in docker, local or in-process mode. Currently, in-process is best supported. 4 | -------------------------------------------------------------------------------- /models/dto/configure.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ConfigureParams struct { 4 | CreateBatches *bool 5 | AcceptTransactions *bool 6 | } 7 | -------------------------------------------------------------------------------- /models/enums/healthstatus/health_status.go: -------------------------------------------------------------------------------- 1 | package healthstatus 2 | 3 | const ( 4 | Ready = "READY" 5 | Migrating = "MIGRATING" 6 | ) 7 | -------------------------------------------------------------------------------- /docs/benchmarks/creation-1s-block-timer.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/benchmarks/creation-1s-block-timer.prof -------------------------------------------------------------------------------- /docs/commander_troubleshooting_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/commander_troubleshooting_flowchart.png -------------------------------------------------------------------------------- /utils/Readme.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | 3 | `ref` syntax sugar for creating references to values. 4 | 5 | `merkletree` in-memory merkle tree implementation 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hubble-contracts"] 2 | path = hubble-contracts 3 | url = git@github.com:Worldcoin/hubble-contracts.git 4 | branch = fork-master 5 | -------------------------------------------------------------------------------- /main/Readme.md: -------------------------------------------------------------------------------- 1 | # Command line interface 2 | 3 | * Creates a `Commander` struct and runs commander. 4 | * Deploys smart contracts 5 | * Exports data from database 6 | -------------------------------------------------------------------------------- /docs/benchmarks/after-serialization-optimizations.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/hubble-commander/HEAD/docs/benchmarks/after-serialization-optimizations.prof -------------------------------------------------------------------------------- /eth/Readme.md: -------------------------------------------------------------------------------- 1 | # Ethereum client 2 | 3 | Wrappers around the generated ABI. Adds gas handling, timeouts, etc. 4 | 5 | 6 | Deployer: Deploy the rollup contracts 7 | 8 | -------------------------------------------------------------------------------- /models/Readme.md: -------------------------------------------------------------------------------- 1 | # Repository of types 2 | 3 | 4 | dto: (Data transfer objects) Types used in API. These are exposed publically and need to maintain compatibility. 5 | 6 | -------------------------------------------------------------------------------- /models/merkle_tree.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type MerkleTreeNode struct { 6 | MerklePath MerklePath 7 | DataHash common.Hash 8 | } 9 | -------------------------------------------------------------------------------- /contracts/Readme.md: -------------------------------------------------------------------------------- 1 | # Smart contract wrappers 2 | 3 | Generated from `hubble-contracts` which is included as a git-submodule using `abi-gen`. 4 | 5 | The generation tool is in `/utils/generate.py`. 6 | -------------------------------------------------------------------------------- /models/registered_spoke.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | ) 6 | 7 | type RegisteredSpoke struct { 8 | ID Uint256 9 | Contract common.Address 10 | } 11 | -------------------------------------------------------------------------------- /models/registered_token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | ) 6 | 7 | type RegisteredToken struct { 8 | ID Uint256 9 | Contract common.Address 10 | } 11 | -------------------------------------------------------------------------------- /utils/string_in_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func StringInSlice(a string, list []string) bool { 4 | for _, b := range list { 5 | if b == a { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /models/tx_error.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type TxError struct { 6 | TxHash common.Hash 7 | SenderStateID uint32 8 | ErrorMessage string 9 | } 10 | -------------------------------------------------------------------------------- /utils/map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func CopyStringUint32Map(m map[string]uint32) map[string]uint32 { 4 | cp := make(map[string]uint32) 5 | for k, v := range m { 6 | cp[k] = v 7 | } 8 | 9 | return cp 10 | } 11 | -------------------------------------------------------------------------------- /commander/syncer/Readme.md: -------------------------------------------------------------------------------- 1 | # Syncer 2 | 3 | * Syncing batches, commitments and transactions 4 | * Validating transactions 5 | * Verifying commitment signatures 6 | * Returning errors that trigger disputes in case of fraud detection 7 | -------------------------------------------------------------------------------- /api/get_genesis_accounts.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | ) 6 | 7 | func (a *API) GetGenesisAccounts() *models.GenesisAccounts { 8 | return &a.client.ChainState.GenesisAccounts 9 | } 10 | -------------------------------------------------------------------------------- /metrics/utils.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/Worldcoin/hubble-commander/utils" 4 | 5 | func EventNameToMetricsEventFilterCallLabel(eventName string) string { 6 | return utils.CamelCaseToSnakeCase(eventName) + "_filter_log_call" 7 | } 8 | -------------------------------------------------------------------------------- /db/controller_adapter.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "github.com/dgraph-io/badger/v3" 4 | 5 | type ControllerAdapter struct { 6 | *badger.Txn 7 | } 8 | 9 | func (a *ControllerAdapter) Rollback() error { 10 | a.Txn.Discard() 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /models/dto/transaction_receipt.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/txstatus" 5 | ) 6 | 7 | type TransactionReceipt struct { 8 | TransactionWithBatchDetails 9 | Status txstatus.TransactionStatus 10 | } 11 | -------------------------------------------------------------------------------- /api/get_status.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/Worldcoin/hubble-commander/models/enums/healthstatus" 4 | 5 | func (a *API) GetStatus() string { 6 | if a.isMigrating() { 7 | return healthstatus.Migrating 8 | } 9 | return healthstatus.Ready 10 | } 11 | -------------------------------------------------------------------------------- /commander/tracker/Readme.md: -------------------------------------------------------------------------------- 1 | # Tracker 2 | 3 | ### Tracker.SendRequestedTxs 4 | Receives transaction requests and sends them in order to avoid incorrect nonce. 5 | 6 | ### Tracker.TrackSentTxs 7 | Tracks the status of sent transactions and returns an error if the transaction failed. 8 | -------------------------------------------------------------------------------- /storage/Readme.md: -------------------------------------------------------------------------------- 1 | # Storage 2 | 3 | Badger is a key-value database with transaction support. 4 | 5 | `badgerhold` Creates indices on top of Badger. 6 | 7 | See [Badger Data Structures](../docs/badger/data_structures.md) to learn what we store. 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /utils/event_before.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/ethereum/go-ethereum/core/types" 4 | 5 | func EventBefore(left, right *types.Log) bool { 6 | return left.BlockNumber < right.BlockNumber || 7 | (left.BlockNumber == right.BlockNumber && left.Index < right.Index) 8 | } 9 | -------------------------------------------------------------------------------- /models/dto/recompute_pending_state.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/Worldcoin/hubble-commander/models" 4 | 5 | type RecomputePendingState struct { 6 | OldNonce models.Uint256 7 | OldBalance models.Uint256 8 | NewNonce models.Uint256 9 | NewBalance models.Uint256 10 | } 11 | -------------------------------------------------------------------------------- /utils/project_root.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | func GetProjectRoot() string { 10 | _, file, _, _ := runtime.Caller(0) // nolint:dogsled 11 | fileDir := filepath.Dir(file) 12 | return path.Join(fileDir, "..") 13 | } 14 | -------------------------------------------------------------------------------- /commander/prover/context.go: -------------------------------------------------------------------------------- 1 | package prover 2 | 3 | import ( 4 | st "github.com/Worldcoin/hubble-commander/storage" 5 | ) 6 | 7 | type Context struct { 8 | storage *st.Storage 9 | } 10 | 11 | func NewContext(storage *st.Storage) *Context { 12 | return &Context{storage: storage} 13 | } 14 | -------------------------------------------------------------------------------- /api/api_error_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNewAPIError(t *testing.T) { 10 | _ = NewAPIError(1234, "something") 11 | require.Panics(t, func() { 12 | _ = NewAPIError(1234, "something") 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | language = "en" 3 | multilingual = false 4 | src = "docs" 5 | title = "hubble-commander Documentation" 6 | 7 | [build] 8 | create-missing = false 9 | 10 | [preprocessor.index] 11 | 12 | [preprocessor.toc] 13 | command = "mdbook-toc" 14 | renderer = ["html"] 15 | max-level = 4 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 140 10 | tab_width = 4 11 | 12 | [{*.yaml,*.yml}] 13 | indent_size = 2 14 | 15 | [{*.go,*.go2,Makefile}] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /commander/applier/applier.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import ( 4 | st "github.com/Worldcoin/hubble-commander/storage" 5 | ) 6 | 7 | type Applier struct { 8 | storage *st.Storage 9 | } 10 | 11 | func NewApplier(storage *st.Storage) *Applier { 12 | return &Applier{ 13 | storage: storage, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /models/witness.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type Witness []common.Hash 6 | 7 | func (w Witness) Bytes() [][32]byte { 8 | result := make([][32]byte, 0, len(w)) 9 | for i := range w { 10 | result = append(result, w[i]) 11 | } 12 | return result 13 | } 14 | -------------------------------------------------------------------------------- /commander/executor/Readme.md: -------------------------------------------------------------------------------- 1 | # Executor 2 | 3 | * Executing transactions 4 | * Creating commitments 5 | * Creating batches 6 | * Submitting batches on chain 7 | 8 | `ExecutionContext` Wraps common dependencies and manages a database transaction. 9 | `RollupContext` Single use struct created on every Rollup Loop iteration. 10 | -------------------------------------------------------------------------------- /models/bh_utils.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func GetTypeName(dataType interface{}) []byte { 8 | return []byte(reflect.TypeOf(dataType).Name()) 9 | } 10 | 11 | func GetBadgerHoldPrefix(dataType interface{}) []byte { 12 | return []byte("bh_" + reflect.TypeOf(dataType).Name() + ":") 13 | } 14 | -------------------------------------------------------------------------------- /api/version_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/config" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetVersion(t *testing.T) { 11 | api := API{cfg: &config.APIConfig{Version: "v0123"}} 12 | require.Equal(t, "v0123", api.GetVersion()) 13 | } 14 | -------------------------------------------------------------------------------- /metrics/measure_duration.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "time" 4 | 5 | func MeasureDuration(action func() error) (*time.Duration, error) { 6 | startTime := time.Now() 7 | 8 | err := action() 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | duration := time.Since(startTime).Round(time.Millisecond) 14 | return &duration, nil 15 | } 16 | -------------------------------------------------------------------------------- /utils/chain_spec.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func StoreChainSpec(filePath, chainSpec string) error { 9 | dirPath := filepath.Dir(filePath) 10 | err := os.MkdirAll(dirPath, os.ModePerm) 11 | if err != nil { 12 | return err 13 | } 14 | return os.WriteFile(filePath, []byte(chainSpec), 0600) 15 | } 16 | -------------------------------------------------------------------------------- /bls/sdk/clangwrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # go/clangwrap.sh 4 | 5 | SDK_PATH=`xcrun --sdk $SDK --show-sdk-path` 6 | CLANG=`xcrun --sdk $SDK --find clang` 7 | 8 | if [ "$GOARCH" == "amd64" ]; then 9 | CARCH="x86_64" 10 | elif [ "$GOARCH" == "arm64" ]; then 11 | CARCH="arm64" 12 | fi 13 | 14 | exec $CLANG -arch $CARCH -isysroot $SDK_PATH -mios-version-min=10.0 "$@" 15 | -------------------------------------------------------------------------------- /deploy/hubble/templates/headless.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "hubble.fullname" . }}-headless 5 | labels: 6 | {{- include "hubble.labels" . | nindent 4 }} 7 | spec: 8 | ports: 9 | - port: 8080 10 | name: http 11 | clusterIP: None 12 | selector: 13 | {{- include "hubble.selectorLabels" . | nindent 4 }} 14 | -------------------------------------------------------------------------------- /eth/get_nonce.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func (c *Client) GetNonce() (uint64, error) { 10 | nonce, err := c.Blockchain.GetBackend().PendingNonceAt(context.Background(), c.Blockchain.GetAccount().From) 11 | if err != nil { 12 | return 0, errors.WithStack(err) 13 | } 14 | return nonce, nil 15 | } 16 | -------------------------------------------------------------------------------- /metrics/save_histogram_measurement.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func SaveHistogramMeasurement( 10 | duration *time.Duration, 11 | metric *prometheus.HistogramVec, 12 | labels prometheus.Labels, 13 | ) { 14 | metric.With(labels).Observe(float64(duration.Milliseconds())) 15 | } 16 | -------------------------------------------------------------------------------- /storage/mm_commitment.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/stored" 6 | ) 7 | 8 | func (s *CommitmentStorage) addMMCommitment(commitment *models.MMCommitment) error { 9 | return s.database.Badger.Insert(commitment.ID, stored.MakeCommitmentFromMMCommitment(commitment)) 10 | } 11 | -------------------------------------------------------------------------------- /storage/tx_commitment.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/stored" 6 | ) 7 | 8 | func (s *CommitmentStorage) addTxCommitment(commitment *models.TxCommitment) error { 9 | return s.database.Badger.Insert(commitment.ID, stored.MakeCommitmentFromTxCommitment(commitment)) 10 | } 11 | -------------------------------------------------------------------------------- /utils/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | const ( 6 | L2Unit = 1e9 7 | AuthKeyHeader = "Auth-Key" 8 | PendingID = uint32(0) 9 | ) 10 | 11 | // ZeroHash is the same as keccak256(abi.encode(0)) 12 | var ZeroHash = common.HexToHash("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563") 13 | -------------------------------------------------------------------------------- /deployer-config.example.yaml: -------------------------------------------------------------------------------- 1 | #bootstrap: 2 | # genesis_path: genesis.yaml 3 | # blocks_to_finalise: 40320 # 7 * 24 * 60 * 4 4 | # chooser_address: # empty by default - deploy chooser 5 | # 6 | #ethereum: 7 | # rpc_url: ws://localhost:8546 8 | # chain_id: 1337 9 | # private_key: ee79b5f6e221356af78cf4c36f4f7885a11b67dfcc81c34d80249947330c0f82 10 | # mine_timeout: 5m 11 | -------------------------------------------------------------------------------- /models/enums/result/dispute_result.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | type DisputeResult uint8 4 | 5 | const ( 6 | Ok DisputeResult = iota 7 | InvalidTokenAmount 8 | NotEnoughTokenBalance 9 | BadFromTokenID 10 | BadToTokenID 11 | BadSignature 12 | MismatchedAmount 13 | BadWithdrawRoot 14 | BadCompression 15 | TooManyTx 16 | BadPrecompileCall 17 | NonexistentReceiver 18 | ) 19 | -------------------------------------------------------------------------------- /storage/deposit_commitment.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/stored" 6 | ) 7 | 8 | func (s *CommitmentStorage) addDepositCommitment(commitment *models.DepositCommitment) error { 9 | return s.database.Badger.Insert(commitment.ID, stored.MakeCommitmentFromDepositCommitment(commitment)) 10 | } 11 | -------------------------------------------------------------------------------- /api/admin/get_failed_transactions.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | ) 8 | 9 | func (a *API) GetFailedTransactions(ctx context.Context) (models.GenericTransactionArray, error) { 10 | err := a.verifyAuthKey(ctx) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return a.storage.GetAllFailedTransactions() 16 | } 17 | -------------------------------------------------------------------------------- /deploy/geth/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "geth.serviceAccountName" . }} 6 | labels: 7 | {{- include "geth.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /docs/batch/structure.md: -------------------------------------------------------------------------------- 1 | # 🗞 Batch Structure and Fraud Proofs 2 | 3 | ## Batch Submission 4 | 5 | ![batch_submission.png](submission.png) 6 | 7 | ## Batch types 8 | 9 | ```go 10 | const ( 11 | Genesis BatchType = 0 12 | Transfer BatchType = 1 13 | MassMigration BatchType = 2 14 | Create2Transfer BatchType = 3 15 | Deposit BatchType = 4 16 | ) 17 | ``` 18 | -------------------------------------------------------------------------------- /deploy/hubble/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "hubble.serviceAccountName" . }} 6 | labels: 7 | {{- include "hubble.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /deploy/hubble/values-stage.yaml: -------------------------------------------------------------------------------- 1 | environment: stage 2 | 3 | ingress: 4 | host: "hubble.stage-crypto.worldcoin.dev" 5 | 6 | resources: 7 | limits: 8 | cpu: 4 9 | memory: 6Gi 10 | requests: 11 | cpu: 3 12 | memory: 4Gi 13 | 14 | persistentStorage: 15 | size: 100Gi 16 | volumeID: aws://us-east-1a/vol-065fd87c2124454e6 # example: aws://us-east-1c/vol-aabbcc123 17 | 18 | wipeDisk: false 19 | -------------------------------------------------------------------------------- /eth/proposer_test.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestClient_IsActiveProposer(t *testing.T) { 10 | client, err := NewTestClient() 11 | require.NoError(t, err) 12 | defer client.Close() 13 | 14 | isActiveProposer, err := client.IsActiveProposer() 15 | require.NoError(t, err) 16 | 17 | require.True(t, isActiveProposer) 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | .DS_Store 4 | .env 5 | .env.docker 6 | db/data 7 | e2e-data 8 | book 9 | commander-config.yaml 10 | deployer-config.yaml 11 | e2e/geth-data/geth 12 | chain-spec.yaml 13 | exported-data.json 14 | 15 | # mobile libraries 16 | bls/sdk/HubbleSDK.xcframework 17 | bls/sdk/HubbleSDK-sources.jar 18 | bls/sdk/HubbleSDK.aar 19 | 20 | # generated by `ctags` 21 | tags 22 | data 23 | 24 | .vscode 25 | -------------------------------------------------------------------------------- /eth/chain/rpc_backend.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/ethclient" 5 | "github.com/ethereum/go-ethereum/rpc" 6 | ) 7 | 8 | type RPCBackend struct { 9 | *ethclient.Client 10 | } 11 | 12 | func NewRPCBackend(c *rpc.Client) *RPCBackend { 13 | return &RPCBackend{ 14 | Client: ethclient.NewClient(c), 15 | } 16 | } 17 | 18 | func (c *RPCBackend) Commit() { 19 | // NOOP 20 | } 21 | -------------------------------------------------------------------------------- /utils/units.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/params" 7 | ) 8 | 9 | func ParseUnits(value string, unit int64) *big.Int { 10 | base, _ := new(big.Int).SetString(value, 10) 11 | multiplier := big.NewInt(unit) 12 | return new(big.Int).Mul(base, multiplier) 13 | } 14 | 15 | func ParseEther(value string) *big.Int { 16 | return ParseUnits(value, params.Ether) 17 | } 18 | -------------------------------------------------------------------------------- /api/middleware/measure_request_duration.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Worldcoin/hubble-commander/metrics" 7 | ) 8 | 9 | func measureRequestDuration(start time.Time, commanderMetrics *metrics.CommanderMetrics) time.Duration { 10 | duration := time.Since(start).Round(time.Millisecond) 11 | commanderMetrics.APIRequestDuration.Observe(float64(duration.Milliseconds())) 12 | return duration 13 | } 14 | -------------------------------------------------------------------------------- /config/genesis_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestReadGenesisFile(t *testing.T) { 11 | genesisPath := path.Join("..", "genesis.yaml") 12 | genesisAccounts, err := readGenesisFile(genesisPath) 13 | require.NoError(t, err) 14 | require.Greater(t, len(genesisAccounts), 0) 15 | require.Equal(t, genesisAccounts[0].State.Balance.CmpN(0), 1) 16 | } 17 | -------------------------------------------------------------------------------- /deploy/hubble/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "hubble.fullname" . }} 5 | labels: 6 | {{- include "hubble.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "hubble.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /utils/camel_case_to_snake_case.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 9 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 10 | 11 | func CamelCaseToSnakeCase(str string) string { 12 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") 13 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 14 | return strings.ToLower(snake) 15 | } 16 | -------------------------------------------------------------------------------- /deploy/geth/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/hubble/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /eth/get_blocks_to_finalise.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import "github.com/Worldcoin/hubble-commander/utils/ref" 4 | 5 | func (c *Client) GetBlocksToFinalise() (*int64, error) { 6 | if c.blocksToFinalise != nil { 7 | return c.blocksToFinalise, nil 8 | } 9 | 10 | blocksToFinalise, err := c.rollup().ParamBlocksToFinalise() 11 | if err != nil { 12 | return nil, err 13 | } 14 | c.blocksToFinalise = ref.Int64(blocksToFinalise.Int64()) 15 | return c.blocksToFinalise, nil 16 | } 17 | -------------------------------------------------------------------------------- /utils/camel_case_to_snake_case_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestTCamelCaseToSnakeCase(t *testing.T) { 10 | res := CamelCaseToSnakeCase("DepositSubTreeReady") 11 | require.Equal(t, "deposit_sub_tree_ready", res) 12 | } 13 | 14 | func TestTCamelCaseToSnakeCase_FirstWordLowerCase(t *testing.T) { 15 | res := CamelCaseToSnakeCase("oneTwoThree") 16 | require.Equal(t, "one_two_three", res) 17 | } 18 | -------------------------------------------------------------------------------- /utils/map_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCopyStringUint32Map(t *testing.T) { 10 | m1 := map[string]uint32{ 11 | "a": 123, 12 | "b": 456, 13 | } 14 | 15 | m2 := CopyStringUint32Map(m1) 16 | 17 | m1["a"] = 999 18 | delete(m1, "b") 19 | 20 | require.Equal(t, map[string]uint32{"a": 999}, m1) 21 | require.Equal(t, map[string]uint32{ 22 | "a": 123, 23 | "b": 456, 24 | }, m2) 25 | } 26 | -------------------------------------------------------------------------------- /utils/random_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestRandomBytes(t *testing.T) { 10 | bytes := RandomBytes(32) 11 | require.Len(t, bytes, 32) 12 | } 13 | 14 | func TestRandomHex(t *testing.T) { 15 | hex := RandomHex(32) 16 | require.Len(t, hex, 32) 17 | } 18 | 19 | func TestRandomHash(t *testing.T) { 20 | hash1 := RandomHash() 21 | hash2 := RandomHash() 22 | require.NotEqual(t, hash1, hash2) 23 | } 24 | -------------------------------------------------------------------------------- /eth/domain.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 6 | ) 7 | 8 | func (c *Client) GetDomain() (*bls.Domain, error) { 9 | if c.domain != nil { 10 | return c.domain, nil 11 | } 12 | 13 | domainSeparator, err := c.Rollup.DomainSeparator(&bind.CallOpts{}) 14 | if err != nil { 15 | return nil, err 16 | } 17 | domain := bls.Domain(domainSeparator) 18 | c.domain = &domain 19 | return &domain, nil 20 | } 21 | -------------------------------------------------------------------------------- /models/pending_batch.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 5 | "github.com/ethereum/go-ethereum/common" 6 | ) 7 | 8 | type PendingBatch struct { 9 | ID Uint256 10 | Type batchtype.BatchType 11 | TransactionHash common.Hash 12 | PrevStateRoot common.Hash 13 | Commitments []PendingCommitment 14 | } 15 | 16 | type PendingCommitment struct { 17 | Commitment 18 | Transactions GenericTransactionArray 19 | } 20 | -------------------------------------------------------------------------------- /db/encoder_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/stored" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDecodeKey(t *testing.T) { 11 | prefix := []byte("bh_prefix") 12 | value := uint64(123456789) 13 | 14 | encoded := stored.EncodeUint64(value) 15 | 16 | var decoded uint64 17 | err := DecodeKey(append(prefix, encoded...), &decoded, prefix) 18 | require.NoError(t, err) 19 | require.Equal(t, value, decoded) 20 | } 21 | -------------------------------------------------------------------------------- /models/commitment_id_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCommitmentID_Bytes(t *testing.T) { 10 | commitmentID := CommitmentID{ 11 | BatchID: MakeUint256(24), 12 | IndexInBatch: 4, 13 | } 14 | 15 | bytes := commitmentID.Bytes() 16 | 17 | var decodedCommitmentID CommitmentID 18 | err := decodedCommitmentID.SetBytes(bytes) 19 | require.NoError(t, err) 20 | require.Equal(t, commitmentID, decodedCommitmentID) 21 | } 22 | -------------------------------------------------------------------------------- /config/genesis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | func readGenesisFile(filepath string) ([]models.GenesisAccount, error) { 11 | var genesisAccounts []models.GenesisAccount 12 | 13 | yamlFile, err := os.ReadFile(filepath) 14 | if err != nil { 15 | return nil, err 16 | } 17 | err = yaml.Unmarshal(yamlFile, &genesisAccounts) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return genesisAccounts, nil 22 | } 23 | -------------------------------------------------------------------------------- /api/middleware/default_handler.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/Worldcoin/hubble-commander/metrics" 8 | ) 9 | 10 | func DefaultHandler(next http.Handler, commanderMetrics *metrics.CommanderMetrics) http.Handler { 11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | commanderMetrics.APITotalRequests.Inc() 13 | 14 | start := time.Now() 15 | defer measureRequestDuration(start, commanderMetrics) 16 | 17 | next.ServeHTTP(w, r) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /utils/bytes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Pad bytes slice with 0s from left to size length 4 | // Returns original bytes slice in case len(slice) >= size 5 | func PadLeft(bytes []byte, size int) []byte { 6 | l := len(bytes) 7 | if l >= size { 8 | return bytes 9 | } 10 | paddedBytes := make([]byte, size) 11 | copy(paddedBytes[size-l:], bytes) 12 | return paddedBytes 13 | } 14 | 15 | func ByteSliceTo32ByteArray(source []byte) [32]byte { 16 | var target [32]byte 17 | copy(target[:], source) 18 | 19 | return target 20 | } 21 | -------------------------------------------------------------------------------- /encoder/meta.go: -------------------------------------------------------------------------------- 1 | package encoder 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func DecodeMeta(meta [32]byte) models.BatchMeta { 12 | return models.BatchMeta{ 13 | BatchType: batchtype.BatchType(meta[0]), 14 | Size: meta[1], 15 | Committer: common.BytesToAddress(meta[2:22]), 16 | FinaliseOn: binary.BigEndian.Uint32(meta[22:26]), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/admin/configure.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/dto" 7 | ) 8 | 9 | func (a *API) Configure(ctx context.Context, params dto.ConfigureParams) error { 10 | err := a.verifyAuthKey(ctx) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if params.CreateBatches != nil { 16 | a.enableBatchCreation(*params.CreateBatches) 17 | } 18 | if params.AcceptTransactions != nil { 19 | a.enableTxsAcceptance(*params.AcceptTransactions) 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /metrics/server.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/Worldcoin/hubble-commander/config" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | ) 10 | 11 | func (c *CommanderMetrics) NewServer(cfg *config.MetricsConfig) *http.Server { 12 | handler := promhttp.HandlerFor(c.registry, promhttp.HandlerOpts{}) 13 | 14 | mux := http.NewServeMux() 15 | mux.Handle(cfg.Endpoint, handler) 16 | 17 | addr := fmt.Sprintf(":%s", cfg.Port) 18 | return &http.Server{Addr: addr, Handler: mux} 19 | } 20 | -------------------------------------------------------------------------------- /models/batch.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 5 | "github.com/ethereum/go-ethereum/common" 6 | ) 7 | 8 | type Batch struct { 9 | ID Uint256 10 | Type batchtype.BatchType 11 | TransactionHash common.Hash 12 | Hash *common.Hash // root of tree containing all commitments included in this batch 13 | FinalisationBlock *uint32 14 | AccountTreeRoot *common.Hash 15 | PrevStateRoot *common.Hash 16 | MinedTime *Timestamp 17 | } 18 | -------------------------------------------------------------------------------- /bls/Readme.md: -------------------------------------------------------------------------------- 1 | # BLS Signature library 2 | 3 | Implements the [BLS digital signature](https://en.wikipedia.org/wiki/BLS_digital_signature) scheme. This signature scheme supports aggregation. 4 | 5 | Uses the Ethereum compatible BN254 pairing curve (aka alt-BN128; see [EIP-196](https://eips.ethereum.org/EIPS/eip-196)). We may prefer BLS12-381 or another pairing curve once Ethereum gains support for it (see [EIP-2539](https://eips.ethereum.org/EIPS/eip-2539)). 6 | 7 | The BN254 pairing curve and BLS implementation are from . 8 | -------------------------------------------------------------------------------- /commander/disputer/context.go: -------------------------------------------------------------------------------- 1 | package disputer 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/commander/prover" 5 | "github.com/Worldcoin/hubble-commander/eth" 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | ) 8 | 9 | type Context struct { 10 | storage *st.Storage 11 | client *eth.Client 12 | proverCtx *prover.Context 13 | } 14 | 15 | func NewContext(storage *st.Storage, client *eth.Client) *Context { 16 | return &Context{ 17 | storage: storage, 18 | client: client, 19 | proverCtx: prover.NewContext(storage), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/geth-data/keystore/UTC--2021-06-07T12-34-01.339648000Z--9f758331b439c1b664e86f2050f2360370f06849: -------------------------------------------------------------------------------- 1 | {"address":"9f758331b439c1b664e86f2050f2360370f06849","crypto":{"cipher":"aes-128-ctr","ciphertext":"e1860e27080f7c6e7353cd132ae498b889d045849aa3f224d470d5d97ea09fc4","cipherparams":{"iv":"3626816e04320eaa4bd4a7626787e839"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"34636cdd63c05f88c20632b5bdc312896125ec35e1b572a9c3276f75cab2408e"},"mac":"0a7edb666e831f892b06791da54c8d9ee61cbf0f99ab2ec03b6d812ee0ef334e"},"id":"db483d8e-2d98-490b-997e-26ed52ee6365","version":3} -------------------------------------------------------------------------------- /e2e/geth-data/keystore/UTC--2022-02-24T08-41-23.950510000Z--644cad6dd685f042cf9a2c4979476638e414c39c: -------------------------------------------------------------------------------- 1 | {"address":"644cad6dd685f042cf9a2c4979476638e414c39c","crypto":{"cipher":"aes-128-ctr","ciphertext":"cd9a3aa5f77a8991bcecad277e6347e611c94c4debcbbf63a69d452ecad54f3f","cipherparams":{"iv":"ef2eef122472d55f5836bb445f75bd04"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"2196e516d6eda5257ad35d40cda06f94baa369f897110d8763d0479a3eff26e3"},"mac":"026ae8c313f76225cd7df3e31092e646b5d1b824e659958adbab95bc98a3e5b3"},"id":"c9305271-2be3-40c8-9f2e-fc281501b212","version":3} -------------------------------------------------------------------------------- /testutils/deposits.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import "github.com/Worldcoin/hubble-commander/models" 4 | 5 | func GetFourDeposits() []models.PendingDeposit { 6 | deposits := make([]models.PendingDeposit, 4) 7 | for i := range deposits { 8 | deposits[i] = models.PendingDeposit{ 9 | ID: models.DepositID{ 10 | SubtreeID: models.MakeUint256(1), 11 | DepositIndex: models.MakeUint256(uint64(i))}, 12 | ToPubKeyID: 1, 13 | TokenID: models.MakeUint256(0), 14 | L2Amount: models.MakeUint256(10000000000), 15 | } 16 | } 17 | return deposits 18 | } 19 | -------------------------------------------------------------------------------- /models/enums/errors/unsupported.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | type UnsupportedError struct { 9 | field string 10 | } 11 | 12 | func NewUnsupportedError(field string) *UnsupportedError { 13 | return &UnsupportedError{field: field} 14 | } 15 | 16 | func (n *UnsupportedError) Error() string { 17 | return fmt.Sprintf("unsupported %s", n.field) 18 | } 19 | 20 | func IsUnsupportedError(err error) bool { 21 | if err == nil { 22 | return false 23 | } 24 | target := &UnsupportedError{} 25 | return errors.As(err, &target) 26 | } 27 | -------------------------------------------------------------------------------- /utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/ethereum/go-ethereum/crypto" 6 | ) 7 | 8 | func HashTwo(a, b common.Hash) common.Hash { 9 | buf := make([]byte, 64) 10 | copy(buf[0:32], a.Bytes()) 11 | copy(buf[32:64], b.Bytes()) 12 | return crypto.Keccak256Hash(buf) 13 | } 14 | 15 | /* 16 | * This syntax was added in go 1.17 17 | * - https://tip.golang.org/ref/spec#Conversions_from_slice_to_array_pointer 18 | */ 19 | func HashToByteArray(a *common.Hash) [32]byte { 20 | return *(*[32]byte)(a.Bytes()) 21 | } 22 | -------------------------------------------------------------------------------- /models/stored/sizes.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import "github.com/Worldcoin/hubble-commander/models" 4 | 5 | const ( 6 | sizeCommitmentID = models.CommitmentIDDataLength 7 | sizeCommitmentSlot = sizeCommitmentID + 1 8 | sizeHash = 32 9 | sizeTxType = 1 10 | sizeU32 = 4 11 | sizeU256 = 32 12 | sizeSignature = 64 13 | sizeTimestamp = 16 14 | sizePendingTxNoBody = sizeHash + sizeTxType + sizeU32 + 3*sizeU256 + sizeSignature + sizeTimestamp 15 | sizeBatchedTxNoBody = sizePendingTxNoBody + sizeCommitmentSlot 16 | ) 17 | -------------------------------------------------------------------------------- /e2e/geth-data/fundSecondAccount.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ "$1" = "docker" ]; then 3 | docker="docker exec hubble-geth" 4 | fi 5 | 6 | i=1 7 | while [ "$i" -le 120 ]; do 8 | echo "Trying to fund second account (attempt #$i)... " 9 | response=$($docker geth attach --exec "eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: 10e36})" http://localhost:8545) 10 | res=$(echo "$response" | cut -c1-3) 11 | if [ "$res" = "\"0x" ]; then 12 | break 13 | fi 14 | sleep 0.5 15 | 16 | i=$(expr $i + 1) 17 | done 18 | 19 | echo "Second account funded!" 20 | -------------------------------------------------------------------------------- /utils/bytes_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPadLeft(t *testing.T) { 10 | require.Equal(t, []byte{0, 0, 1}, PadLeft([]byte{1}, 3)) 11 | require.Equal(t, []byte{0, 0, 1}, PadLeft([]byte{0, 0, 1}, 3)) 12 | require.Equal(t, []byte{0, 0, 1}, PadLeft([]byte{0, 0, 1}, 2)) 13 | } 14 | 15 | func TestByteSliceTo32ByteArray(t *testing.T) { 16 | require.Equal(t, [32]byte{0, 0, 1}, ByteSliceTo32ByteArray([]byte{0, 0, 1})) 17 | require.Equal(t, [32]byte{1, 2, 3}, ByteSliceTo32ByteArray([]byte{1, 2, 3})) 18 | } 19 | -------------------------------------------------------------------------------- /commander/executor/errors.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type RollupError struct { 8 | Reason string 9 | IsLoggable bool 10 | } 11 | 12 | func NewRollupError(reason string) *RollupError { 13 | return &RollupError{ 14 | Reason: reason, 15 | IsLoggable: false, 16 | } 17 | } 18 | 19 | func NewLoggableRollupError(reason string) *RollupError { 20 | return &RollupError{ 21 | Reason: reason, 22 | IsLoggable: true, 23 | } 24 | } 25 | 26 | func (e RollupError) Error() string { 27 | return fmt.Sprintf("failed to submit batch: %s", e.Reason) 28 | } 29 | -------------------------------------------------------------------------------- /utils/badger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func ValueToInterfaceSlice(slice interface{}, fieldName string) []interface{} { 8 | s := reflect.ValueOf(slice) 9 | if s.Kind() != reflect.Slice { 10 | panic("ValueToInterfaceSlice() given a non-slice type") 11 | } 12 | 13 | // Keep the distinction between nil and empty slice input 14 | if s.IsNil() { 15 | return nil 16 | } 17 | 18 | result := make([]interface{}, s.Len()) 19 | for i := 0; i < s.Len(); i++ { 20 | result[i] = s.Index(i).FieldByName(fieldName).Interface() 21 | } 22 | 23 | return result 24 | } 25 | -------------------------------------------------------------------------------- /models/dispute_signature.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type SignatureProof struct { 6 | UserStates []StateMerkleProof 7 | PublicKeys []PublicKeyProof 8 | } 9 | 10 | type PublicKeyProof struct { 11 | PublicKey *PublicKey 12 | Witness Witness 13 | } 14 | 15 | type SignatureProofWithReceiver struct { 16 | UserStates []StateMerkleProof 17 | SenderPublicKeys []PublicKeyProof 18 | ReceiverPublicKeys []ReceiverPublicKeyProof 19 | } 20 | 21 | type ReceiverPublicKeyProof struct { 22 | PublicKeyHash common.Hash 23 | Witness Witness 24 | } 25 | -------------------------------------------------------------------------------- /api/log_transaction.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const txHashField = "txHash" 11 | 12 | func logReceivedTransaction(hash common.Hash, tx interface{}) { 13 | if log.IsLevelEnabled(log.DebugLevel) { 14 | jsonTx, err := json.Marshal(tx) 15 | if err != nil { 16 | log.WithField(txHashField, hash).Errorln("Marshaling received transaction failed") 17 | return 18 | } 19 | log.WithField(txHashField, hash).Debugf("API: received new transaction: %s", string(jsonTx)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /utils/units_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | 7 | "github.com/ethereum/go-ethereum/params" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParseUnits_OneGWei(t *testing.T) { 12 | require.Equal(t, ParseUnits("1", params.GWei), big.NewInt(1e9)) 13 | } 14 | 15 | func TestParseEther_OneEther(t *testing.T) { 16 | require.Equal(t, ParseEther("1"), big.NewInt(1e18)) 17 | } 18 | 19 | func TestParseEther_1234Ether(t *testing.T) { 20 | expected := new(big.Int).Mul(big.NewInt(1234), big.NewInt(1e18)) 21 | require.Equal(t, ParseEther("1234"), expected) 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | geth: 5 | image: ethereum/client-go:stable 6 | container_name: hubble-geth 7 | restart: unless-stopped 8 | ports: 9 | - "30303:30303" 10 | - "30303:30303/udp" 11 | - "8545:8545" 12 | - "8546:8546" 13 | volumes: 14 | - ./e2e/geth-data:/root/ethereum 15 | command: 16 | - --datadir=/root/ethereum 17 | - --dev 18 | - --dev.period=1 19 | - --http 20 | - --http.addr=0.0.0.0 21 | - --http.api=eth,miner 22 | - --ws 23 | - --ws.addr=0.0.0.0 24 | - --ws.api=eth,miner 25 | -------------------------------------------------------------------------------- /main/export.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Worldcoin/hubble-commander/scripts" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | var exportTypes = []string{"state", "accounts"} 11 | 12 | func exportData(ctx *cli.Context) error { 13 | file := ctx.String("file") 14 | 15 | var err error 16 | switch ctx.String("type") { 17 | case exportTypes[0]: 18 | err = scripts.ExportStateLeaves(file) 19 | case exportTypes[1]: 20 | err = scripts.ExportAccounts(file) 21 | default: 22 | return fmt.Errorf("invalid export data type, supported: %v", exportTypes) 23 | } 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /bls/wallet_test.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNewWallet(t *testing.T) { 10 | data := []byte{1, 2, 3} 11 | 12 | wallet, err := NewRandomWallet(TestDomain) 13 | require.NoError(t, err) 14 | 15 | signature, err := wallet.Sign(data) 16 | require.NoError(t, err) 17 | 18 | privateKey, _ := wallet.Bytes() 19 | 20 | newWallet, err := NewWallet(privateKey, TestDomain) 21 | require.NoError(t, err) 22 | 23 | isValid, err := signature.Verify(data, newWallet.PublicKey()) 24 | require.NoError(t, err) 25 | require.True(t, isValid) 26 | } 27 | -------------------------------------------------------------------------------- /db/iterator_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dgraph-io/badger/v3" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNewSeekPrefix_CopiesToNewMemoryLocationToAvoidRaceConditions(t *testing.T) { 11 | prefix := make([]byte, 3, 4) 12 | prefix[0] = 1 13 | prefix[1] = 2 14 | prefix[2] = 3 15 | 16 | underlyingArrayAddress := &prefix[0] 17 | 18 | newPrefix := newSeekPrefix(prefix, badger.IteratorOptions{ 19 | Reverse: true, 20 | }) 21 | 22 | require.Same(t, underlyingArrayAddress, &prefix[0]) 23 | require.NotSame(t, underlyingArrayAddress, &newPrefix[0]) 24 | } 25 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](Readme.md) 4 | - [JSON RPC API](json_rpc.md) 5 | - [Badger]() 6 | - [Data structures](badger/data_structures.md) 7 | - [Operations](badger/operations.md) 8 | - [Batch Structure and Fraud Proofs](batch/structure.md) 9 | - [Commitment loop](commitment/loop.md) 10 | - [Transaction Structure](transaction_structure.md) 11 | - [Contracts upgradeability](contracts-upgradeability.md) 12 | - [Deposits](deposits.md) 13 | - [Mass Migrations](mass_migration.md) 14 | - [State Rent](state_rent.md) 15 | ------ 16 | [Naming](naming.md) 17 | [Benchmarks & profiling](benchmarks_and_profiling.md) 18 | -------------------------------------------------------------------------------- /config/errors.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type ErrNonMatchingKeys struct { 6 | publicKey string 7 | } 8 | 9 | func NewErrNonMatchingKeys(publicKey string) *ErrNonMatchingKeys { 10 | return &ErrNonMatchingKeys{publicKey: publicKey} 11 | } 12 | 13 | func (e *ErrNonMatchingKeys) Error() string { 14 | return fmt.Sprintf("public key does not match the private key of a genesis account (%s)", e.publicKey) 15 | } 16 | 17 | func (e *ErrNonMatchingKeys) Is(other error) bool { 18 | otherErr, ok := other.(*ErrNonMatchingKeys) 19 | if !ok { 20 | return false 21 | } 22 | return e.publicKey == otherErr.publicKey 23 | } 24 | -------------------------------------------------------------------------------- /deploy/geth/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "geth.fullname" . }} 5 | labels: 6 | {{- include "geth.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | - port: 8546 15 | targetPort: 8546 16 | protocol: TCP 17 | name: rpc-b 18 | - port: 8545 19 | targetPort: 8545 20 | protocol: TCP 21 | name: rpc-a 22 | selector: 23 | {{- include "geth.selectorLabels" . | nindent 4 }} 24 | -------------------------------------------------------------------------------- /testutils/simulator/backend.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | 7 | "github.com/ethereum/go-ethereum" 8 | "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" 9 | ) 10 | 11 | type Backend struct { 12 | *backends.SimulatedBackend 13 | } 14 | 15 | func NewBackend(simulatedBackend *backends.SimulatedBackend) *Backend { 16 | return &Backend{SimulatedBackend: simulatedBackend} 17 | } 18 | 19 | // nolint:gocritic 20 | func (b *Backend) CallContract(ctx context.Context, call ethereum.CallMsg, _ *big.Int) ([]byte, error) { 21 | return b.SimulatedBackend.CallContract(ctx, call, nil) 22 | } 23 | -------------------------------------------------------------------------------- /api/admin/authentication.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Worldcoin/hubble-commander/api/rpc" 8 | ) 9 | 10 | var ( 11 | errMissingAuthKey = fmt.Errorf("missing authentication key") 12 | errInvalidAuthKey = fmt.Errorf("invalid authentication key value") 13 | ) 14 | 15 | func (a *API) verifyAuthKey(ctx context.Context) error { 16 | authKeyValue := ctx.Value(rpc.AuthKey) 17 | if authKeyValue == nil || authKeyValue == "" { 18 | return errMissingAuthKey 19 | } 20 | 21 | if authKeyValue != a.cfg.AuthenticationKey { 22 | return errInvalidAuthKey 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /commander/applier/apply_deposits.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func (a *Applier) ApplyDeposits(startStateID uint32, deposits []models.PendingDeposit) error { 9 | for i := range deposits { 10 | _, err := a.storage.StateTree.Set(startStateID+uint32(i), &models.UserState{ 11 | PubKeyID: deposits[i].ToPubKeyID, 12 | TokenID: deposits[i].TokenID, 13 | Balance: deposits[i].L2Amount, 14 | Nonce: models.MakeUint256(0), 15 | }) 16 | if err != nil { 17 | return errors.WithStack(err) 18 | } 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /api/get_status_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/enums/healthstatus" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAPI_GetStatus_Ready(t *testing.T) { 11 | api := &API{isMigrating: func() bool { 12 | return false 13 | }} 14 | 15 | status := api.GetStatus() 16 | require.Equal(t, healthstatus.Ready, status) 17 | } 18 | 19 | func TestAPI_GetStatus_Migrating(t *testing.T) { 20 | api := &API{isMigrating: func() bool { 21 | return true 22 | }} 23 | 24 | status := api.GetStatus() 25 | require.Equal(t, healthstatus.Migrating, status) 26 | } 27 | -------------------------------------------------------------------------------- /eth/raw_transact.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | ) 9 | 10 | func (c *Client) RawTransact(value *big.Int, gasLimit uint64, calldata []byte) (*types.Transaction, error) { 11 | return c.Rollup.BoundContract.RawTransact(c.transactOpts(value, gasLimit), calldata) 12 | } 13 | 14 | func (c *Client) transactOpts(value *big.Int, gasLimit uint64) *bind.TransactOpts { 15 | transactOpts := *c.Blockchain.GetAccount() 16 | transactOpts.Value = value 17 | transactOpts.GasLimit = gasLimit 18 | return &transactOpts 19 | } 20 | -------------------------------------------------------------------------------- /models/stored/state_leaf_test.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/utils" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestStateLeaf_Bytes(t *testing.T) { 12 | leaf := FlatStateLeaf{ 13 | StateID: 1, 14 | DataHash: utils.RandomHash(), 15 | PubKeyID: 2, 16 | TokenID: models.MakeUint256(3), 17 | Balance: models.MakeUint256(4), 18 | Nonce: models.MakeUint256(5), 19 | } 20 | 21 | var decodedLeaf FlatStateLeaf 22 | _ = decodedLeaf.SetBytes(leaf.Bytes()) 23 | require.Equal(t, leaf, decodedLeaf) 24 | } 25 | -------------------------------------------------------------------------------- /storage/mass_migration.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (s *TransactionStorage) GetMassMigration(hash common.Hash) (*models.MassMigration, error) { 11 | tx, err := s.getTransactionByHash(hash) 12 | if err != nil { 13 | return nil, err 14 | } 15 | if tx.Type() != txtype.MassMigration { 16 | return nil, errors.WithStack(NewNotFoundError("transaction")) 17 | } 18 | transfer := tx.ToMassMigration() 19 | return transfer, nil 20 | } 21 | -------------------------------------------------------------------------------- /commander/get_chain_connection.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/config" 5 | "github.com/Worldcoin/hubble-commander/eth/chain" 6 | "github.com/Worldcoin/hubble-commander/testutils/simulator" 7 | "github.com/Worldcoin/hubble-commander/utils/ref" 8 | ) 9 | 10 | func GetChainConnection(cfg *config.EthereumConfig) (chain.Connection, error) { 11 | if cfg.RPCURL == "simulator" { 12 | return simulator.NewConfiguredSimulator(simulator.Config{ 13 | FirstAccountPrivateKey: ref.String(cfg.PrivateKey), 14 | AutomineEnabled: ref.Bool(true), 15 | }) 16 | } 17 | return chain.NewRPCConnection(cfg) 18 | } 19 | -------------------------------------------------------------------------------- /storage/create2transfer.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (s *TransactionStorage) GetCreate2Transfer(hash common.Hash) (*models.Create2Transfer, error) { 11 | tx, err := s.getTransactionByHash(hash) 12 | if err != nil { 13 | return nil, err 14 | } 15 | if tx.Type() != txtype.Create2Transfer { 16 | return nil, errors.WithStack(NewNotFoundError("transaction")) 17 | } 18 | transfer := tx.ToCreate2Transfer() 19 | return transfer, nil 20 | } 21 | -------------------------------------------------------------------------------- /models/commitment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type Commitment interface { 6 | GetCommitmentBase() *CommitmentBase 7 | SetBodyHash(bodyHash *common.Hash) 8 | GetBodyHash() *common.Hash 9 | GetPostStateRoot() common.Hash 10 | LeafHash() common.Hash 11 | ToTxCommitment() *TxCommitment 12 | ToMMCommitment() *MMCommitment 13 | ToDepositCommitment() *DepositCommitment 14 | } 15 | 16 | type CommitmentWithTxs interface { 17 | CalcAndSetBodyHash(accountRoot common.Hash) 18 | ToCommitment() Commitment 19 | ToTxCommitmentWithTxs() *TxCommitmentWithTxs 20 | ToMMCommitmentWithTxs() *MMCommitmentWithTxs 21 | } 22 | -------------------------------------------------------------------------------- /deploy/geth/templates/service-headless.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "geth.fullname" . }}-headless 5 | labels: 6 | {{- include "geth.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | clusterIP: None 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | - port: 8546 16 | targetPort: 8546 17 | protocol: TCP 18 | name: rpc-b 19 | - port: 8545 20 | targetPort: 8545 21 | protocol: TCP 22 | name: rpc-a 23 | selector: 24 | {{- include "geth.selectorLabels" . | nindent 4 }} 25 | -------------------------------------------------------------------------------- /eth/withdraw_stake.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/ethereum/go-ethereum/core/types" 6 | ) 7 | 8 | func (c *Client) WithdrawStakeAndWait(batchID *models.Uint256) error { 9 | tx, err := c.WithdrawStake(batchID) 10 | if err != nil { 11 | return err 12 | } 13 | _, err = c.WaitToBeMined(tx) 14 | return err 15 | } 16 | 17 | func (c *Client) WithdrawStake(batchID *models.Uint256) (*types.Transaction, error) { 18 | tx, err := c.rollup(). 19 | WithGasLimit(*c.config.StakeWithdrawalGasLimit). 20 | WithdrawStake(batchID.ToBig()) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return tx, nil 25 | } 26 | -------------------------------------------------------------------------------- /models/dto/pending_batch.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type PendingBatch struct { 10 | ID models.Uint256 11 | Type batchtype.BatchType 12 | TransactionHash common.Hash 13 | PrevStateRoot common.Hash 14 | Commitments []PendingCommitment 15 | } 16 | 17 | type PendingCommitment struct { 18 | models.Commitment 19 | 20 | // We're using type from models, because commander is only consumer of API returning that type 21 | Transactions models.GenericTransactionArray 22 | } 23 | -------------------------------------------------------------------------------- /storage/transfer.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // called by a large amount of tests, and nothing else 11 | func (s *TransactionStorage) GetTransfer(hash common.Hash) (*models.Transfer, error) { 12 | tx, err := s.getTransactionByHash(hash) 13 | if err != nil { 14 | return nil, err 15 | } 16 | if tx.Type() != txtype.Transfer { 17 | return nil, errors.WithStack(NewNotFoundError("transaction")) 18 | } 19 | transfer := tx.ToTransfer() 20 | return transfer, nil 21 | } 22 | -------------------------------------------------------------------------------- /api/admin/get_pending_transactions.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | ) 8 | 9 | func (a *API) GetPendingTransactions(ctx context.Context) (models.GenericTransactionArray, error) { 10 | err := a.verifyAuthKey(ctx) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | mempoolTxs, err := a.storage.GetAllMempoolTransactions() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | result := make([]models.GenericTransaction, len(mempoolTxs)) 21 | for i := range mempoolTxs { 22 | result[i] = mempoolTxs[i].ToGenericTransaction() 23 | } 24 | 25 | return models.MakeGenericArray(result...), nil 26 | } 27 | -------------------------------------------------------------------------------- /commander/Readme.md: -------------------------------------------------------------------------------- 1 | # Main application struct 2 | 3 | `Commander` in `commander.go` is the main App struct. 4 | 5 | `rollupLoop` periodically creates `rollupLoopIteration`. Batches have min and max size. 6 | 7 | 8 | `accounts.go`: `syncAccounts` Synchronizes on-chain account state to internal state. 9 | 10 | `batches.go`: `syncBatches` Synchronizes on-chain batch state to internal state. 11 | 12 | `registered_tokens.go`: `syncTokens` Synchronizes on-chain registered tokens to internal state. 13 | 14 | `new_block.go`: `newBlockLoop` Watches for new blocks. Triggers syncing methods. 15 | 16 | `syncForward`: Sync a batch of blocks (batched for internal efficiency, not visible from the outside). 17 | -------------------------------------------------------------------------------- /commander/syncer/deposits_context.go: -------------------------------------------------------------------------------- 1 | package syncer 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/commander/applier" 5 | "github.com/Worldcoin/hubble-commander/eth" 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | ) 8 | 9 | type DepositsContext struct { 10 | storage *st.Storage 11 | client *eth.Client 12 | applier *applier.Applier 13 | } 14 | 15 | func newDepositsContext(storage *st.Storage, client *eth.Client) *DepositsContext { 16 | return &DepositsContext{ 17 | storage: storage, 18 | client: client, 19 | applier: applier.NewApplier(storage), 20 | } 21 | } 22 | 23 | func (c *DepositsContext) Commit() {} 24 | 25 | func (c *DepositsContext) Rollback() {} 26 | -------------------------------------------------------------------------------- /models/stored/failed_tx_test.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNewFailedTxIndex(t *testing.T) { 11 | fromStateID := uint32(1) 12 | nonce := models.NewUint256(11) 13 | failedTxIndex := NewFailedTxIndex(fromStateID, nonce) 14 | 15 | var decodedFromStateID uint32 16 | err := DecodeUint32(failedTxIndex[:4], &decodedFromStateID) 17 | require.NoError(t, err) 18 | require.Equal(t, fromStateID, decodedFromStateID) 19 | 20 | decodedNonce := new(models.Uint256) 21 | decodedNonce.SetBytes(failedTxIndex[4:]) 22 | require.Equal(t, nonce, decodedNonce) 23 | } 24 | -------------------------------------------------------------------------------- /contracts/spokeregistry/events.go: -------------------------------------------------------------------------------- 1 | package spokeregistry 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum" 5 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 6 | "github.com/ethereum/go-ethereum/core/types" 7 | ) 8 | 9 | type SpokeRegisteredIterator struct { 10 | SpokeRegistrySpokeRegisteredIterator 11 | } 12 | 13 | func (i *SpokeRegisteredIterator) SetData(contract *bind.BoundContract, event string, logs chan types.Log, sub ethereum.Subscription) { 14 | i.SpokeRegistrySpokeRegisteredIterator.contract = contract 15 | i.SpokeRegistrySpokeRegisteredIterator.event = event 16 | i.SpokeRegistrySpokeRegisteredIterator.logs = logs 17 | i.SpokeRegistrySpokeRegisteredIterator.sub = sub 18 | } 19 | -------------------------------------------------------------------------------- /contracts/tokenregistry/events.go: -------------------------------------------------------------------------------- 1 | package tokenregistry 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum" 5 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 6 | "github.com/ethereum/go-ethereum/core/types" 7 | ) 8 | 9 | type RegisteredTokenIterator struct { 10 | TokenRegistryTokenRegisteredIterator 11 | } 12 | 13 | func (i *RegisteredTokenIterator) SetData(contract *bind.BoundContract, event string, logs chan types.Log, sub ethereum.Subscription) { 14 | i.TokenRegistryTokenRegisteredIterator.contract = contract 15 | i.TokenRegistryTokenRegisteredIterator.event = event 16 | i.TokenRegistryTokenRegisteredIterator.logs = logs 17 | i.TokenRegistryTokenRegisteredIterator.sub = sub 18 | } 19 | -------------------------------------------------------------------------------- /models/commitment_id.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | const CommitmentIDDataLength = 33 4 | 5 | type CommitmentID struct { 6 | // GetTransactionIDsByBatchIDs assumes BatchID is the first field 7 | BatchID Uint256 8 | IndexInBatch uint8 9 | } 10 | 11 | func (c *CommitmentID) Bytes() []byte { 12 | encoded := make([]byte, CommitmentIDDataLength) 13 | copy(encoded[0:32], c.BatchID.Bytes()) 14 | encoded[32] = c.IndexInBatch 15 | 16 | return encoded 17 | } 18 | 19 | func (c *CommitmentID) SetBytes(data []byte) error { 20 | if len(data) != CommitmentIDDataLength { 21 | return ErrInvalidLength 22 | } 23 | 24 | c.BatchID.SetBytes(data[0:32]) 25 | c.IndexInBatch = data[32] 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /storage/temporary_storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/db" 5 | ) 6 | 7 | type TemporaryStorage struct { 8 | *Storage 9 | } 10 | 11 | func NewTemporaryStorage() (*TemporaryStorage, error) { 12 | badgerDB, err := db.NewInMemoryDatabase() 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | database := &Database{ 18 | Badger: badgerDB, 19 | } 20 | 21 | storage, err := newStorageFromDatabase(database) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | tempStorage := TemporaryStorage{storage} 27 | 28 | return &tempStorage, nil 29 | } 30 | 31 | func (s *TemporaryStorage) Close() error { 32 | return s.database.Badger.Close() 33 | } 34 | -------------------------------------------------------------------------------- /commander/prover/vacancy_proof.go: -------------------------------------------------------------------------------- 1 | package prover 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | st "github.com/Worldcoin/hubble-commander/storage" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func (c *Context) GetVacancyProof(startStateID uint32, subtreeDepth uint8) (*models.SubtreeVacancyProof, error) { 10 | path := models.MerklePath{ 11 | Path: startStateID >> subtreeDepth, 12 | Depth: st.StateTreeDepth - subtreeDepth, 13 | } 14 | witness, err := c.storage.StateTree.GetNodeWitness(path) 15 | if err != nil { 16 | return nil, errors.WithStack(err) 17 | } 18 | 19 | return &models.SubtreeVacancyProof{ 20 | PathAtDepth: path.Path, 21 | Witness: witness, 22 | }, nil 23 | } 24 | -------------------------------------------------------------------------------- /main/deploy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/commander" 5 | "github.com/Worldcoin/hubble-commander/config" 6 | "github.com/Worldcoin/hubble-commander/utils" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func deployContracts(ctx *cli.Context) error { 12 | cfg := config.GetDeployerConfigAndSetupLogger() 13 | blockchain, err := commander.GetChainConnection(cfg.Ethereum) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | chainSpec, err := commander.Deploy(cfg, blockchain) 19 | if err != nil { 20 | return err 21 | } 22 | log.Printf(*chainSpec) 23 | 24 | return utils.StoreChainSpec(ctx.String("file"), *chainSpec) 25 | } 26 | -------------------------------------------------------------------------------- /deploy/hubble/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: {{ include "hubble.fullname" . }} 5 | annotations: 6 | kubernetes.io/ingress.class: traefik 7 | ingress.kubernetes.io/protocol: http 8 | traefik.frontend.rule.type: PathPrefix 9 | traefik.ingress.kubernetes.io/router.entrypoints: websecure 10 | spec: 11 | rules: 12 | {{- range .Values.ingress.hosts }} 13 | - host: {{ . }} 14 | http: 15 | paths: 16 | - path: / 17 | pathType: Prefix 18 | backend: 19 | service: 20 | name: "{{ include "hubble.fullname" $ }}" 21 | port: 22 | name: http 23 | {{- end }} 24 | -------------------------------------------------------------------------------- /utils/badger_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | type testType struct { 10 | First int 11 | Second string 12 | } 13 | 14 | func TestValueToInterfaceSlice(t *testing.T) { 15 | input := make([]testType, 5) 16 | expected := make([]interface{}, 5) 17 | for i := range input { 18 | input[i] = testType{ 19 | First: i, 20 | } 21 | expected[i] = i 22 | } 23 | 24 | output := ValueToInterfaceSlice(input, "First") 25 | require.Equal(t, expected, output) 26 | } 27 | 28 | func TestValueToInterfaceSlice_InvalidType(t *testing.T) { 29 | input := testType{ 30 | First: 1, 31 | } 32 | require.Panics(t, func() { ValueToInterfaceSlice(input, "First") }) 33 | } 34 | -------------------------------------------------------------------------------- /commander/errors.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import "fmt" 4 | 5 | type CannotBootstrapError struct { 6 | reason string 7 | } 8 | 9 | func NewCannotBootstrapError(reason string) *CannotBootstrapError { 10 | return &CannotBootstrapError{reason} 11 | } 12 | 13 | func (c CannotBootstrapError) Error() string { 14 | return fmt.Sprintf("cannot bootstrap: %s", c.reason) 15 | } 16 | 17 | type InconsistentChainIDError struct { 18 | CannotBootstrapError 19 | } 20 | 21 | func NewInconsistentChainIDError(chainIDSource string) *InconsistentChainIDError { 22 | reason := fmt.Sprintf("chain ID conflict between config and %s", chainIDSource) 23 | return &InconsistentChainIDError{ 24 | CannotBootstrapError: *NewCannotBootstrapError(reason), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/export_accounts.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "bufio" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | st "github.com/Worldcoin/hubble-commander/storage" 8 | ) 9 | 10 | func ExportAccounts(filePath string) error { 11 | return exportLeaves(filePath, exportAndCountAccounts) 12 | } 13 | 14 | func exportAndCountAccounts(storage *st.Storage, writer *bufio.Writer) (int, error) { 15 | count := 0 16 | err := storage.AccountTree.IterateLeaves(func(accountLeaf *models.AccountLeaf) error { 17 | if count > 0 { 18 | err := writer.WriteByte(',') 19 | if err != nil { 20 | return err 21 | } 22 | } 23 | count++ 24 | 25 | return writeData(writer, accountLeaf) 26 | }) 27 | if err != nil { 28 | return 0, err 29 | } 30 | return count, nil 31 | } 32 | -------------------------------------------------------------------------------- /commander/validate_state_root.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "fmt" 5 | 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var ErrInvalidStateRoot = fmt.Errorf("current state tree root doesn't match latest commitment post state root") 11 | 12 | func validateStateRoot(storage *st.Storage) error { 13 | latestCommitment, err := storage.GetLatestCommitment() 14 | if st.IsNotFoundError(err) { 15 | return nil 16 | } 17 | if err != nil { 18 | return err 19 | } 20 | stateRoot, err := storage.StateTree.Root() 21 | if err != nil { 22 | return err 23 | } 24 | if latestCommitment.PostStateRoot != *stateRoot { 25 | logLatestCommitment(latestCommitment) 26 | return errors.WithStack(ErrInvalidStateRoot) 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /e2e/setup/create_unregistered_wallets.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/config" 6 | ) 7 | 8 | const InitialGenesisBalance = 1_000_000_000 9 | 10 | func CreateUnregisteredWalletsForBenchmark(txCount int64, domain bls.Domain) ([]bls.Wallet, error) { 11 | cfg := config.GetDeployerTestConfig() 12 | accounts := cfg.Bootstrap.GenesisAccounts 13 | 14 | numberOfNeededWallets := int(txCount) * len(accounts) 15 | wallets := make([]bls.Wallet, 0, numberOfNeededWallets) 16 | for i := 0; i < numberOfNeededWallets; i++ { 17 | newRandomWallet, err := bls.NewRandomWallet(domain) 18 | if err != nil { 19 | return nil, err 20 | } 21 | wallets = append(wallets, *newRandomWallet) 22 | } 23 | 24 | return wallets, nil 25 | } 26 | -------------------------------------------------------------------------------- /o11y/logging.go: -------------------------------------------------------------------------------- 1 | package o11y 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/sirupsen/logrus" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | func TraceFields(ctx context.Context) logrus.Fields { 12 | spanCtx := trace.SpanFromContext(ctx).SpanContext() 13 | if spanCtx.IsValid() { 14 | return logrus.Fields{ 15 | "dd.trace_id": convertTraceID(spanCtx.TraceID().String()), 16 | "dd.span_id": convertTraceID(spanCtx.SpanID().String()), 17 | } 18 | } 19 | return logrus.Fields{} 20 | } 21 | 22 | func convertTraceID(id string) string { 23 | if len(id) < 16 { 24 | return "" 25 | } 26 | if len(id) > 16 { 27 | id = id[16:] 28 | } 29 | intValue, err := strconv.ParseUint(id, 16, 64) 30 | if err != nil { 31 | return "" 32 | } 33 | return strconv.FormatUint(intValue, 10) 34 | } 35 | -------------------------------------------------------------------------------- /main/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/Worldcoin/hubble-commander/bls" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func newWallet(ctx *cli.Context) error { 14 | privateKey := make([]byte, 32) 15 | _, err := rand.Read(privateKey) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | wallet, err := bls.NewWallet(privateKey, bls.Domain{0x00, 0x00, 0x00, 0x00}) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | result, _ := json.Marshal( 26 | struct { 27 | PrivateKey string 28 | PublicKey string 29 | }{ 30 | PrivateKey: fmt.Sprintf("0x%x", privateKey), 31 | PublicKey: wallet.PublicKey().String(), 32 | }, 33 | ) 34 | 35 | fmt.Printf("%s\n", result) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /models/timestamp_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTimestamp_MarshalJSON(t *testing.T) { 12 | timestamp := Timestamp{Time: time.Now()} 13 | json, err := timestamp.MarshalJSON() 14 | require.NoError(t, err) 15 | 16 | seconds := timestamp.Unix() 17 | stringValue := fmt.Sprintf("%d", seconds) 18 | expected := []byte(stringValue) 19 | 20 | require.Equal(t, expected, json) 21 | } 22 | 23 | func TestTimestamp_UnmarshalJSON(t *testing.T) { 24 | seconds := time.Now().Unix() 25 | stringValue := fmt.Sprintf("%d", seconds) 26 | json := []byte(stringValue) 27 | 28 | timestamp := Timestamp{} 29 | err := timestamp.UnmarshalJSON(json) 30 | require.NoError(t, err) 31 | 32 | require.Equal(t, seconds, timestamp.Unix()) 33 | } 34 | -------------------------------------------------------------------------------- /utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "math/big" 6 | "math/rand" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func RandomBytes(size uint64) []byte { 12 | bytes := make([]byte, size) 13 | // nolint:gosec 14 | rand.Read(bytes) 15 | return bytes 16 | } 17 | 18 | func RandomHex(length uint64) string { 19 | return hex.EncodeToString(RandomBytes(length / 2)) 20 | } 21 | 22 | func RandomHash() common.Hash { 23 | return common.BytesToHash(RandomBytes(32)) 24 | } 25 | 26 | func NewRandomHash() *common.Hash { 27 | newHash := RandomHash() 28 | return &newHash 29 | } 30 | 31 | func RandomAddress() common.Address { 32 | return common.BytesToAddress(RandomBytes(20)) 33 | } 34 | 35 | func RandomBigInt() *big.Int { 36 | // nolint:gosec 37 | return new(big.Int).SetUint64(rand.Uint64()) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/rollback-prod.yaml: -------------------------------------------------------------------------------- 1 | name: Rollback production 2 | on: [workflow_dispatch] 3 | 4 | jobs: 5 | rollback-prod: 6 | runs-on: ubuntu-latest 7 | environment: prod 8 | permissions: 9 | id-token: write 10 | contents: read 11 | steps: 12 | - name: Configure AWS Credentials 13 | uses: aws-actions/configure-aws-credentials@v1 14 | with: 15 | aws-region: ${{ secrets.AWS_PROD_REGION }} 16 | role-to-assume: ${{ secrets.AWS_PROD_ROLE }} 17 | role-duration-seconds: 900 18 | - run: aws eks --region ${{ secrets.AWS_PROD_REGION }} update-kubeconfig --name ${{ secrets.AWS_PROD_CLUSTER }} 19 | - name: Rollback 20 | # 0 means back to previous release 21 | run: helm rollback -n "${{ github.event.repository.name }}" "${{ github.event.repository.name }}" 0 22 | -------------------------------------------------------------------------------- /.github/workflows/rollback-stage.yaml: -------------------------------------------------------------------------------- 1 | name: Rollback stage 2 | on: [workflow_dispatch] 3 | 4 | jobs: 5 | rollback-stage: 6 | runs-on: ubuntu-latest 7 | environment: stage 8 | permissions: 9 | id-token: write 10 | contents: read 11 | steps: 12 | - name: Configure AWS Credentials 13 | uses: aws-actions/configure-aws-credentials@v1 14 | with: 15 | aws-region: ${{ secrets.AWS_STAGE_REGION }} 16 | role-to-assume: ${{ secrets.AWS_STAGE_ROLE }} 17 | role-duration-seconds: 900 18 | - run: aws eks --region ${{ secrets.AWS_STAGE_REGION }} update-kubeconfig --name ${{ secrets.AWS_STAGE_CLUSTER }} 19 | - name: Rollback 20 | # 0 means back to previous release 21 | run: helm rollback -n "${{ github.event.repository.name }}" "${{ github.event.repository.name }}" 0 22 | -------------------------------------------------------------------------------- /storage/state_update.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/pkg/errors" 6 | bh "github.com/timshannon/badgerhold/v4" 7 | ) 8 | 9 | func (s *StateTree) addStateUpdate(update *models.StateUpdate) error { 10 | return s.database.Badger.Insert(bh.NextSequence(), *update) 11 | } 12 | 13 | func (s *StateTree) getStateUpdate(id uint64) (*models.StateUpdate, error) { 14 | var stateUpdate models.StateUpdate 15 | err := s.database.Badger.Get(id, &stateUpdate) 16 | if errors.Is(err, bh.ErrNotFound) { 17 | return nil, errors.WithStack(NewNotFoundError("state update")) 18 | } 19 | if err != nil { 20 | return nil, err 21 | } 22 | return &stateUpdate, nil 23 | } 24 | 25 | func (s *StateTree) removeStateUpdate(id uint64) error { 26 | return s.database.Badger.Delete(id, models.StateUpdate{}) 27 | } 28 | -------------------------------------------------------------------------------- /api/metrics.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/metrics" 5 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func (a *API) countAcceptedTx(txType txtype.TransactionType) { 10 | countTransactionWithStatus(a.commanderMetrics, txType, metrics.AcceptedTxStatus) 11 | } 12 | 13 | func (a *API) countRejectedTx(txType txtype.TransactionType) { 14 | countTransactionWithStatus(a.commanderMetrics, txType, metrics.RejectedTxStatus) 15 | } 16 | 17 | func countTransactionWithStatus(commanderMetrics *metrics.CommanderMetrics, transactionType txtype.TransactionType, status string) { 18 | commanderMetrics.APITotalTransactions.With(prometheus.Labels{ 19 | "type": metrics.TxTypeToMetricsTxType(transactionType), 20 | "status": status, 21 | }).Inc() 22 | } 23 | -------------------------------------------------------------------------------- /api/rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/Worldcoin/hubble-commander/utils/consts" 8 | "github.com/ethereum/go-ethereum/rpc" 9 | ) 10 | 11 | type ContextKey int 12 | 13 | const AuthKey ContextKey = iota 14 | 15 | // Server is an RPC server wrapper that pass additional auth header value to context. 16 | type Server struct { 17 | *rpc.Server 18 | } 19 | 20 | func NewServer() *Server { 21 | return &Server{ 22 | Server: rpc.NewServer(), 23 | } 24 | } 25 | 26 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 | authHeaderValue := r.Header.Get(consts.AuthKeyHeader) 28 | if authHeaderValue == "" { 29 | s.Server.ServeHTTP(w, r) 30 | return 31 | } 32 | 33 | ctx := context.WithValue(r.Context(), AuthKey, authHeaderValue) 34 | r = r.WithContext(ctx) 35 | s.Server.ServeHTTP(w, r) 36 | } 37 | -------------------------------------------------------------------------------- /eth/proposer.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/contracts/chooser" 5 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 6 | ) 7 | 8 | // IsActiveProposer checks if the current wallet is the active batch proposer. 9 | func (c *Client) IsActiveProposer() (bool, error) { 10 | chooserAddress, err := c.Rollup.Chooser(nil) 11 | if err != nil { 12 | return false, err 13 | } 14 | 15 | chooserContract, err := chooser.NewChooser(chooserAddress, c.Blockchain.GetBackend()) 16 | if err != nil { 17 | return false, err 18 | } 19 | 20 | currentProposer, err := chooserContract.GetProposer(&bind.CallOpts{ 21 | From: c.Blockchain.GetAccount().From, 22 | }) 23 | if err != nil { 24 | return false, err 25 | } 26 | 27 | currentAddress := c.Blockchain.GetAccount().From 28 | 29 | return currentAddress == currentProposer, nil 30 | } 31 | -------------------------------------------------------------------------------- /testutils/wallets.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/models" 6 | "github.com/Worldcoin/hubble-commander/utils" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func GenerateWallets(s *require.Assertions, domain *bls.Domain, walletsAmount int) []bls.Wallet { 11 | wallets := make([]bls.Wallet, 0, walletsAmount) 12 | for i := 0; i < walletsAmount; i++ { 13 | wallet, err := bls.NewRandomWallet(*domain) 14 | s.NoError(err) 15 | wallets = append(wallets, *wallet) 16 | } 17 | return wallets 18 | } 19 | 20 | func RandomPublicKey() models.PublicKey { 21 | publicKey := models.PublicKey{} 22 | err := publicKey.SetBytes(utils.RandomBytes(models.PublicKeyLength)) 23 | if err != nil { 24 | panic("unable to generate random pubkey") 25 | } 26 | return publicKey 27 | } 28 | -------------------------------------------------------------------------------- /models/pending_stake_withdrawal.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/binary" 4 | 5 | const pendingStakeWithdrawalBytesSize = 36 6 | 7 | var PendingStakeWithdrawalPrefix = GetBadgerHoldPrefix(PendingStakeWithdrawal{}) 8 | 9 | type PendingStakeWithdrawal struct { 10 | BatchID Uint256 11 | FinalisationBlock uint32 12 | } 13 | 14 | func (s *PendingStakeWithdrawal) Bytes() []byte { 15 | data := make([]byte, pendingStakeWithdrawalBytesSize) 16 | copy(data[0:32], s.BatchID.Bytes()) 17 | binary.BigEndian.PutUint32(data[32:36], s.FinalisationBlock) 18 | return data 19 | } 20 | 21 | func (s *PendingStakeWithdrawal) SetBytes(data []byte) error { 22 | if len(data) < pendingStakeWithdrawalBytesSize { 23 | return ErrInvalidLength 24 | } 25 | s.BatchID.SetBytes(data[0:32]) 26 | s.FinalisationBlock = binary.BigEndian.Uint32(data[32:36]) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /eth/commitment.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/encoder" 5 | "github.com/ethereum/go-ethereum/accounts/abi" 6 | ) 7 | 8 | type decodeCommitmentsFunc func(rollupABI *abi.ABI, calldata []byte) ([]encoder.Commitment, error) 9 | 10 | func decodeTxCommitments(rollupABI *abi.ABI, calldata []byte) ([]encoder.Commitment, error) { 11 | commitments, err := encoder.DecodeTransferBatchCalldata(rollupABI, calldata) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return encoder.DecodedCommitmentsToCommitments(commitments...), nil 16 | } 17 | 18 | func decodeMMCommitments(rollupABI *abi.ABI, calldata []byte) ([]encoder.Commitment, error) { 19 | commitments, err := encoder.DecodeMMBatchCalldata(rollupABI, calldata) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return encoder.DecodedMMCommitmentsToCommitments(commitments...), nil 24 | } 25 | -------------------------------------------------------------------------------- /models/dto/user_state.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | ) 6 | 7 | type UserState struct { 8 | PubKeyID uint32 9 | TokenID models.Uint256 10 | Balance models.Uint256 11 | Nonce models.Uint256 12 | } 13 | 14 | type UserStateWithID struct { 15 | StateID uint32 16 | UserState 17 | } 18 | 19 | type PubkeyBalance struct { 20 | PubKey models.PublicKey 21 | Balance models.Uint256 22 | } 23 | 24 | func MakeUserStateWithID(stateID uint32, userState *models.UserState) UserStateWithID { 25 | return UserStateWithID{ 26 | StateID: stateID, 27 | UserState: MakeUserState(userState), 28 | } 29 | } 30 | 31 | func MakeUserState(userState *models.UserState) UserState { 32 | return UserState{ 33 | PubKeyID: userState.PubKeyID, 34 | TokenID: userState.TokenID, 35 | Balance: userState.Balance, 36 | Nonce: userState.Nonce, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /models/transfer.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 5 | ) 6 | 7 | type Transfer struct { 8 | TransactionBase 9 | ToStateID uint32 10 | } 11 | 12 | func (t *Transfer) Type() txtype.TransactionType { 13 | return txtype.Transfer 14 | } 15 | 16 | func (t *Transfer) GetBase() *TransactionBase { 17 | return &t.TransactionBase 18 | } 19 | 20 | func (t *Transfer) GetToStateID() *uint32 { 21 | return &t.ToStateID 22 | } 23 | 24 | func (t *Transfer) ToTransfer() *Transfer { 25 | return t 26 | } 27 | 28 | func (t *Transfer) ToCreate2Transfer() *Create2Transfer { 29 | panic("Transfer cannot be cast to Create2Transfer") 30 | } 31 | 32 | func (t *Transfer) ToMassMigration() *MassMigration { 33 | panic("Transfer cannot be cast to MassMigration") 34 | } 35 | 36 | // nolint:gocritic 37 | func (t Transfer) Copy() GenericTransaction { 38 | return &t 39 | } 40 | -------------------------------------------------------------------------------- /testutils/test.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Worldcoin/hubble-commander/contracts/test/tx" 7 | "github.com/Worldcoin/hubble-commander/contracts/test/types" 8 | "github.com/Worldcoin/hubble-commander/testutils/simulator" 9 | ) 10 | 11 | const TryInterval = 250 * time.Millisecond 12 | 13 | type TestContracts struct { 14 | TestTx *tx.TestTx 15 | TestTypes *types.TestTypes 16 | } 17 | 18 | func DeployTest(sim *simulator.Simulator) (*TestContracts, error) { 19 | deployer := sim.Account 20 | 21 | _, _, testTx, err := tx.DeployTestTx(deployer, sim.Backend) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | _, _, testTypes, err := types.DeployTestTypes(deployer, sim.Backend) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | sim.Backend.Commit() 32 | 33 | return &TestContracts{ 34 | TestTx: testTx, 35 | TestTypes: testTypes, 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /models/commitment_base_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 7 | "github.com/Worldcoin/hubble-commander/utils" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCommitmentBase_Bytes(t *testing.T) { 12 | base := CommitmentBase{ 13 | ID: CommitmentID{ 14 | BatchID: MakeUint256(1), 15 | IndexInBatch: 2, 16 | }, 17 | Type: batchtype.Create2Transfer, 18 | PostStateRoot: utils.RandomHash(), 19 | } 20 | 21 | bytes := base.Bytes() 22 | 23 | var decodedBase CommitmentBase 24 | err := decodedBase.SetBytes(bytes) 25 | require.NoError(t, err) 26 | require.Equal(t, base, decodedBase) 27 | } 28 | 29 | func TestCommitmentBase_Bytes_InvalidLength(t *testing.T) { 30 | var decodedBase CommitmentBase 31 | err := decodedBase.SetBytes([]byte{1, 2, 3}) 32 | require.ErrorIs(t, err, ErrInvalidLength) 33 | } 34 | -------------------------------------------------------------------------------- /models/dto/network_info.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/models" 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type NetworkInfo struct { 10 | ChainID models.Uint256 11 | AccountRegistry common.Address 12 | AccountRegistryDeploymentBlock uint64 13 | TokenRegistry common.Address 14 | SpokeRegistry common.Address 15 | DepositManager common.Address 16 | WithdrawManager common.Address 17 | Rollup common.Address 18 | BlockNumber uint32 19 | TransactionCount uint64 20 | AccountCount uint32 21 | LatestBatch *models.Uint256 22 | LatestFinalisedBatch *models.Uint256 23 | SignatureDomain bls.Domain 24 | } 25 | -------------------------------------------------------------------------------- /models/genesis_account_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPopulatedGenesisAccount_Bytes_ReturnsACopy(t *testing.T) { 10 | account := GenesisAccount{ 11 | PublicKey: PublicKey{1, 2, 0, 5, 4}, 12 | } 13 | bytes := account.Bytes() 14 | bytes[0] = 9 15 | require.Equal(t, PublicKey{1, 2, 0, 5, 4}, account.PublicKey) 16 | } 17 | 18 | func TestPopulatedGenesisAccount_SetBytes(t *testing.T) { 19 | account := GenesisAccount{ 20 | PublicKey: PublicKey{1, 2, 0, 5, 4}, 21 | StateID: 44, 22 | State: UserState{ 23 | PubKeyID: 7, 24 | TokenID: MakeUint256(0), 25 | Balance: MakeUint256(4314), 26 | Nonce: MakeUint256(0), 27 | }, 28 | } 29 | bytes := account.Bytes() 30 | newAccount := GenesisAccount{} 31 | err := newAccount.SetBytes(bytes) 32 | require.NoError(t, err) 33 | require.Equal(t, account, newAccount) 34 | } 35 | -------------------------------------------------------------------------------- /models/genesis_account.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | const populatedGenesisAccountByteSize = 232 // 128 + 4 + 100 8 | 9 | type GenesisAccount struct { 10 | PublicKey PublicKey `yaml:"public_key"` 11 | StateID uint32 `yaml:"state_id"` 12 | State UserState `yaml:"state"` 13 | } 14 | 15 | func (a *GenesisAccount) Bytes() []byte { 16 | b := make([]byte, populatedGenesisAccountByteSize) 17 | 18 | copy(b[:128], a.PublicKey.Bytes()) 19 | binary.BigEndian.PutUint32(b[128:132], a.StateID) 20 | copy(b[132:232], a.State.Bytes()) 21 | 22 | return b 23 | } 24 | 25 | func (a *GenesisAccount) SetBytes(data []byte) error { 26 | err := a.PublicKey.SetBytes(data[:128]) 27 | if err != nil { 28 | return err 29 | } 30 | err = a.State.SetBytes(data[132:232]) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | a.StateID = binary.BigEndian.Uint32(data[128:132]) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /bls/domain.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/ethereum/go-ethereum/common/hexutil" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | const DomainLength = 32 11 | 12 | type Domain [DomainLength]byte 13 | 14 | var ( 15 | TestDomain = Domain{1, 2, 3, 4} 16 | domainT = reflect.TypeOf(Domain{}) 17 | 18 | ErrInvalidDomainLength = errors.New("invalid domain length") 19 | ) 20 | 21 | func (d *Domain) Bytes() []byte { 22 | return d[:] 23 | } 24 | 25 | func DomainFromBytes(data []byte) (*Domain, error) { 26 | if len(data) != DomainLength { 27 | return nil, ErrInvalidDomainLength 28 | } 29 | var domain Domain 30 | copy(domain[:], data) 31 | return &domain, nil 32 | } 33 | 34 | func (d *Domain) UnmarshalJSON(input []byte) error { 35 | return hexutil.UnmarshalFixedJSON(domainT, input, d[:]) 36 | } 37 | 38 | func (d Domain) MarshalText() ([]byte, error) { 39 | return hexutil.Bytes(d[:]).MarshalText() 40 | } 41 | -------------------------------------------------------------------------------- /api/sign_transfer.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/encoder" 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/models/dto" 8 | ) 9 | 10 | func SignTransfer(wallet *bls.Wallet, transfer dto.Transfer) (*dto.Transfer, error) { 11 | encodedTransfer, err := encoder.EncodeTransferForSigning(&models.Transfer{ 12 | TransactionBase: models.TransactionBase{ 13 | FromStateID: *transfer.FromStateID, 14 | Amount: *transfer.Amount, 15 | Fee: *transfer.Fee, 16 | Nonce: *transfer.Nonce, 17 | }, 18 | ToStateID: *transfer.ToStateID, 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | signature, err := wallet.Sign(encodedTransfer) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | transfer.Signature = signature.ModelsSignature() 30 | return &transfer, nil 31 | } 32 | -------------------------------------------------------------------------------- /client/batch.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/dto" 6 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 7 | "github.com/ethereum/go-ethereum/common" 8 | ) 9 | 10 | type Batch struct { 11 | ID models.Uint256 12 | Type batchtype.BatchType 13 | TransactionHash common.Hash 14 | PrevStateRoot common.Hash 15 | Commitments []PendingCommitment 16 | } 17 | 18 | func (b *Batch) ToDTO() dto.PendingBatch { 19 | commitments := make([]dto.PendingCommitment, 0, len(b.Commitments)) 20 | for i := range b.Commitments { 21 | commitments = append(commitments, b.Commitments[i].ToDTO()) 22 | } 23 | 24 | return dto.PendingBatch{ 25 | ID: b.ID, 26 | Type: b.Type, 27 | TransactionHash: b.TransactionHash, 28 | PrevStateRoot: b.PrevStateRoot, 29 | Commitments: commitments, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /deploy/hubble/templates/ebs-wipe.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.wipeDisk }} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: ebs-wipe 6 | annotations: 7 | # This is what defines this resource as a hook. Without this line, the 8 | # job is considered part of the release. 9 | "helm.sh/hook": pre-install 10 | "helm.sh/hook-weight": "3" 11 | spec: 12 | completions: 1 13 | parallelism: 1 14 | backoffLimit: 3 15 | ttlSecondsAfterFinished: 0 16 | template: 17 | spec: 18 | restartPolicy: Never 19 | volumes: 20 | - name: hubble-storage 21 | persistentVolumeClaim: 22 | claimName: {{ include "hubble.fullname" . }} # same storage as Statefulset 23 | containers: 24 | - name: disk-wiper 25 | image: busybox 26 | command: ["rm", "-rf", "/volume/*"] 27 | volumeMounts: 28 | - name: hubble-storage 29 | mountPath: "/volume" 30 | {{ end }} 31 | -------------------------------------------------------------------------------- /eth/deployer/proof_of_authority.go: -------------------------------------------------------------------------------- 1 | package deployer 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Worldcoin/hubble-commander/contracts/proofofauthority" 7 | "github.com/Worldcoin/hubble-commander/eth/chain" 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func DeployProofOfAuthority( 14 | c chain.Connection, 15 | mineTimeout time.Duration, 16 | proposers []common.Address, 17 | ) (*common.Address, *proofofauthority.ProofOfAuthority, error) { 18 | log.Println("Deploying ProofOfAuthority") 19 | poaAddress, tx, poa, err := proofofauthority.DeployProofOfAuthority(c.GetAccount(), c.GetBackend(), proposers) 20 | if err != nil { 21 | return nil, nil, errors.WithStack(err) 22 | } 23 | 24 | _, err = chain.WaitToBeMined(c.GetBackend(), mineTimeout, tx) 25 | if err != nil { 26 | return nil, nil, errors.WithStack(err) 27 | } 28 | 29 | return &poaAddress, poa, nil 30 | } 31 | -------------------------------------------------------------------------------- /storage/account.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/pkg/errors" 6 | bh "github.com/timshannon/badgerhold/v4" 7 | ) 8 | 9 | func (s *Storage) GetFirstPubKeyID(publicKey *models.PublicKey) (*uint32, error) { 10 | var account models.AccountLeaf 11 | err := s.database.Badger.FindOneUsingIndex(&account, *publicKey, "PublicKey") 12 | if errors.Is(err, bh.ErrNotFound) { 13 | return nil, errors.WithStack(NewNotFoundError("pub key id")) 14 | } 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return &account.PubKeyID, nil 20 | } 21 | 22 | func (s *Storage) GetPublicKeyByStateID(stateID uint32) (*models.PublicKey, error) { 23 | stateLeaf, err := s.StateTree.Leaf(stateID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | accountLeaf, err := s.AccountTree.Leaf(stateLeaf.PubKeyID) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &accountLeaf.PublicKey, nil 32 | } 33 | -------------------------------------------------------------------------------- /api/sign_mass_migration.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/encoder" 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/models/dto" 8 | ) 9 | 10 | func SignMassMigration(wallet *bls.Wallet, massMigration dto.MassMigration) (*dto.MassMigration, error) { 11 | encodedMassMigration := encoder.EncodeMassMigrationForSigning(&models.MassMigration{ 12 | TransactionBase: models.TransactionBase{ 13 | FromStateID: *massMigration.FromStateID, 14 | Amount: *massMigration.Amount, 15 | Fee: *massMigration.Fee, 16 | Nonce: *massMigration.Nonce, 17 | }, 18 | SpokeID: *massMigration.SpokeID, 19 | }) 20 | 21 | signature, err := wallet.Sign(encodedMassMigration) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | massMigration.Signature = signature.ModelsSignature() 27 | return &massMigration, nil 28 | } 29 | -------------------------------------------------------------------------------- /bls/aggregated_signature_test.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAggregatedSignature_Verify(t *testing.T) { 11 | messages := [][]byte{ 12 | []byte("0x111111"), 13 | []byte("0x222222"), 14 | []byte("0x333333"), 15 | } 16 | 17 | publicKeys := make([]*models.PublicKey, 0, 3) 18 | signatures := make([]*Signature, 0, 3) 19 | 20 | for _, msg := range messages { 21 | wallet, err := NewRandomWallet(TestDomain) 22 | require.NoError(t, err) 23 | 24 | sig, err := wallet.Sign(msg) 25 | require.NoError(t, err) 26 | 27 | publicKeys = append(publicKeys, wallet.PublicKey()) 28 | signatures = append(signatures, sig) 29 | } 30 | 31 | aggregatedSignature := NewAggregatedSignature(signatures) 32 | isValid, err := aggregatedSignature.Verify(messages, publicKeys) 33 | require.NoError(t, err) 34 | require.True(t, isValid) 35 | } 36 | -------------------------------------------------------------------------------- /utils/ref/ref.go: -------------------------------------------------------------------------------- 1 | package ref 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | func Bool(b bool) *bool { 10 | return &b 11 | } 12 | 13 | func Int(i int) *int { 14 | return &i 15 | } 16 | 17 | func Uint8(u uint8) *uint8 { 18 | return &u 19 | } 20 | 21 | func Uint(u uint) *uint { 22 | return &u 23 | } 24 | 25 | func Int32(i int32) *int32 { 26 | return &i 27 | } 28 | 29 | func Uint32(u uint32) *uint32 { 30 | return &u 31 | } 32 | 33 | func Int64(i int64) *int64 { 34 | return &i 35 | } 36 | 37 | func Uint64(u uint64) *uint64 { 38 | return &u 39 | } 40 | 41 | func String(s string) *string { 42 | return &s 43 | } 44 | 45 | func Time(t time.Time) *time.Time { 46 | return &t 47 | } 48 | 49 | func Duration(d time.Duration) *time.Duration { 50 | return &d 51 | } 52 | 53 | func Hash(h common.Hash) *common.Hash { 54 | return &h 55 | } 56 | 57 | func Address(a common.Address) *common.Address { 58 | return &a 59 | } 60 | -------------------------------------------------------------------------------- /metrics/sync_metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | func (c *CommanderMetrics) initializeSyncingMetrics() { 6 | syncMethodDuration := prometheus.NewHistogramVec( 7 | prometheus.HistogramOpts{ 8 | Namespace: namespace, 9 | Subsystem: syncingSubsystem, 10 | Name: "method_duration_milliseconds", 11 | Help: "Durations of syncing methods", 12 | Buckets: []float64{ 13 | 0.0, 14 | 25.0, 15 | 50.0, 16 | 75.0, 17 | 100.0, 18 | 150.0, 19 | 200.0, 20 | 250.0, 21 | 300.0, 22 | 350.0, 23 | 400.0, 24 | 450.0, 25 | 500.0, 26 | 600.0, 27 | 700.0, 28 | 800.0, 29 | 900.0, 30 | 1000.0, 31 | 2000.0, 32 | 3000.0, 33 | 4000.0, 34 | 5000.0, 35 | }, 36 | }, 37 | []string{"method"}, 38 | ) 39 | 40 | c.registry.MustRegister( 41 | syncMethodDuration, 42 | ) 43 | 44 | c.SyncingMethodDuration = syncMethodDuration 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/deploy-stage-contracts.yml: -------------------------------------------------------------------------------- 1 | name: Deploy stage contracts 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | deploy-contracts: 7 | runs-on: ubuntu-latest 8 | environment: stage 9 | permissions: 10 | id-token: write 11 | contents: read 12 | packages: read 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Configure AWS Credentials 17 | uses: aws-actions/configure-aws-credentials@v1 18 | with: 19 | aws-region: ${{ secrets.AWS_STAGE_REGION }} 20 | role-to-assume: ${{ secrets.AWS_STAGE_ROLE }} 21 | role-duration-seconds: 900 22 | - name: Connect to EKS cluster 23 | run: aws eks update-kubeconfig 24 | --region ${{ secrets.AWS_STAGE_REGION }} 25 | --name ${{ secrets.AWS_STAGE_CLUSTER }} 26 | - name: Deploy contracts 27 | run: kubectl apply -n hubble-commander -f ./deploy/contracts-setup-job/clean-setup.yaml 28 | -------------------------------------------------------------------------------- /db/test_db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type TestDB struct { 10 | DB *Database 11 | Teardown func() error 12 | } 13 | 14 | func NewTestDB() (*TestDB, error) { 15 | db, err := NewInMemoryDatabase() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | teardown := func() error { 21 | return db.Close() 22 | } 23 | 24 | return &TestDB{ 25 | DB: db, 26 | Teardown: teardown, 27 | }, nil 28 | } 29 | 30 | func (d *TestDB) Clone() (*TestDB, error) { 31 | clonedBadger, err := NewTestDB() 32 | if err != nil { 33 | return nil, errors.WithStack(err) 34 | } 35 | 36 | var backup bytes.Buffer 37 | _, err = d.DB.store.Badger().Backup(&backup, 0) 38 | if err != nil { 39 | return nil, errors.WithStack(err) 40 | } 41 | 42 | err = clonedBadger.DB.store.Badger().Load(&backup, 16) 43 | if err != nil { 44 | return nil, errors.WithStack(err) 45 | } 46 | 47 | return clonedBadger, nil 48 | } 49 | -------------------------------------------------------------------------------- /e2e/setup/local_commander.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Worldcoin/hubble-commander/config" 7 | "github.com/Worldcoin/hubble-commander/models" 8 | "github.com/ybbus/jsonrpc/v2" 9 | ) 10 | 11 | type LocalCommander struct { 12 | client jsonrpc.RPCClient 13 | } 14 | 15 | func ConnectToLocalCommander() *LocalCommander { 16 | cfg := config.GetConfig() 17 | endpoint := fmt.Sprintf("http://localhost:%s", cfg.API.Port) 18 | client := jsonrpc.NewClient(endpoint) 19 | return &LocalCommander{client} 20 | } 21 | 22 | func (e *LocalCommander) Start() error { 23 | return nil 24 | } 25 | 26 | func (e *LocalCommander) Stop() error { 27 | return nil 28 | } 29 | 30 | func (e *LocalCommander) Restart() error { 31 | return nil 32 | } 33 | 34 | func (e *LocalCommander) Client() jsonrpc.RPCClient { 35 | return e.client 36 | } 37 | 38 | func (e *LocalCommander) ChainSpec() *models.ChainSpec { 39 | panic("ChainSpec() unimplemented on LocalCommander") 40 | } 41 | -------------------------------------------------------------------------------- /api/calculate_transfer_status.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/txstatus" 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | ) 8 | 9 | func CalculateTransactionStatus( 10 | storage *st.Storage, 11 | transfer *models.TransactionBase, 12 | latestBlockNumber uint32, 13 | ) (*txstatus.TransactionStatus, error) { 14 | if transfer.ErrorMessage != nil { 15 | return txstatus.Error.Ref(), nil 16 | } 17 | 18 | if transfer.CommitmentSlot == nil { 19 | return txstatus.Pending.Ref(), nil 20 | } 21 | 22 | batch, err := storage.GetBatch(transfer.CommitmentSlot.BatchID) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | if batch.FinalisationBlock == nil { 28 | return txstatus.Submitted.Ref(), nil 29 | } 30 | 31 | if latestBlockNumber < *batch.FinalisationBlock { 32 | return txstatus.Mined.Ref(), nil 33 | } 34 | 35 | return txstatus.Finalised.Ref(), nil 36 | } 37 | -------------------------------------------------------------------------------- /main/start.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/Worldcoin/hubble-commander/commander" 9 | "github.com/Worldcoin/hubble-commander/config" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | func startCommander(_ *cli.Context) error { 15 | cfg := config.GetCommanderConfigAndSetupLogger() 16 | blockchain, err := commander.GetChainConnection(cfg.Ethereum) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | cmd := commander.NewCommander(cfg, blockchain) 22 | setupCloseHandler(cmd) 23 | 24 | return cmd.StartAndWait() 25 | } 26 | 27 | func setupCloseHandler(cmd *commander.Commander) { 28 | c := make(chan os.Signal, 1) 29 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 30 | go func() { 31 | <-c 32 | log.Warning("Stopping commander gracefully...") 33 | err := cmd.Stop() 34 | if err != nil { 35 | log.Panicf("Failed to stop commander gracefully: %+v", err) 36 | } 37 | }() 38 | } 39 | -------------------------------------------------------------------------------- /eth/get_batch.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/encoder" 5 | "github.com/Worldcoin/hubble-commander/models" 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type ContractBatch struct { 10 | ID models.Uint256 11 | Hash common.Hash 12 | models.BatchMeta 13 | } 14 | 15 | func (cb *ContractBatch) ToModelBatch() *models.Batch { 16 | return &models.Batch{ 17 | ID: cb.ID, 18 | Type: cb.BatchType, 19 | Hash: &cb.Hash, 20 | FinalisationBlock: &cb.FinaliseOn, 21 | } 22 | } 23 | 24 | func (c *Client) GetContractBatch(batchID *models.Uint256) (*ContractBatch, error) { 25 | batch, err := c.Rollup.GetBatch(nil, batchID.ToBig()) 26 | if err != nil { 27 | return nil, err 28 | } 29 | meta := encoder.DecodeMeta(batch.Meta) 30 | hash := common.BytesToHash(batch.CommitmentRoot[:]) 31 | return &ContractBatch{ 32 | ID: *batchID, 33 | Hash: hash, 34 | BatchMeta: meta, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /models/dto/deposit.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/Worldcoin/hubble-commander/models" 4 | 5 | type DepositID struct { 6 | SubtreeID models.Uint256 7 | DepositIndex models.Uint256 8 | } 9 | 10 | type Deposit struct { 11 | ID DepositID 12 | ToPubKeyID uint32 13 | TokenID models.Uint256 14 | L2Amount models.Uint256 15 | } 16 | 17 | func MakeDeposit(pendingDeposit *models.PendingDeposit) Deposit { 18 | return Deposit{ 19 | ID: DepositID{ 20 | SubtreeID: pendingDeposit.ID.SubtreeID, 21 | DepositIndex: pendingDeposit.ID.DepositIndex, 22 | }, 23 | ToPubKeyID: pendingDeposit.ToPubKeyID, 24 | TokenID: pendingDeposit.TokenID, 25 | L2Amount: pendingDeposit.L2Amount, 26 | } 27 | } 28 | 29 | func MakeDeposits(deposits []models.PendingDeposit) []Deposit { 30 | dtoDeposits := make([]Deposit, 0, len(deposits)) 31 | 32 | for i := range deposits { 33 | dtoDeposits = append(dtoDeposits, MakeDeposit(&deposits[i])) 34 | } 35 | 36 | return dtoDeposits 37 | } 38 | -------------------------------------------------------------------------------- /models/mass_migration.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 5 | ) 6 | 7 | type MassMigration struct { 8 | TransactionBase 9 | SpokeID uint32 10 | } 11 | 12 | func (m *MassMigration) Type() txtype.TransactionType { 13 | return txtype.MassMigration 14 | } 15 | 16 | func (m *MassMigration) GetBase() *TransactionBase { 17 | return &m.TransactionBase 18 | } 19 | 20 | func (m *MassMigration) GetToStateID() *uint32 { 21 | panic("MassMigration does not contain a ToStateID field") 22 | } 23 | 24 | func (m *MassMigration) ToTransfer() *Transfer { 25 | panic("MassMigration cannot be cast to Transfer") 26 | } 27 | 28 | func (m *MassMigration) ToCreate2Transfer() *Create2Transfer { 29 | panic("MassMigration cannot be cast to Create2Transfer") 30 | } 31 | 32 | func (m *MassMigration) ToMassMigration() *MassMigration { 33 | return m 34 | } 35 | 36 | // nolint:gocritic 37 | func (m MassMigration) Copy() GenericTransaction { 38 | return &m 39 | } 40 | -------------------------------------------------------------------------------- /testutils/iterate_index.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/db" 5 | bdg "github.com/dgraph-io/badger/v3" 6 | "github.com/stretchr/testify/require" 7 | bh "github.com/timshannon/badgerhold/v4" 8 | ) 9 | 10 | func IterateIndex( 11 | s *require.Assertions, 12 | badger *db.Database, 13 | typeName []byte, 14 | indexName string, 15 | handleIndex func(encodedKey []byte, keyList bh.KeyList), 16 | ) { 17 | indexPrefix := db.IndexKeyPrefix(typeName, indexName) 18 | err := badger.Iterator(indexPrefix, db.PrefetchIteratorOpts, func(item *bdg.Item) (finish bool, err error) { 19 | // Get key value 20 | encodedKeyValue := item.Key()[len(indexPrefix):] 21 | 22 | // Decode value 23 | var keyList bh.KeyList 24 | err = item.Value(func(val []byte) error { 25 | return db.Decode(val, &keyList) 26 | }) 27 | s.NoError(err) 28 | 29 | handleIndex(encodedKeyValue, keyList) 30 | return false, nil 31 | }) 32 | s.ErrorIs(err, db.ErrIteratorFinished) 33 | } 34 | -------------------------------------------------------------------------------- /metrics/types.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 7 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 8 | ) 9 | 10 | func TxTypeToMetricsTxType(transactionType txtype.TransactionType) string { 11 | switch transactionType { 12 | case txtype.Transfer: 13 | return TransferTxLabel 14 | case txtype.MassMigration: 15 | return MMTxLabel 16 | case txtype.Create2Transfer: 17 | return C2TTxLabel 18 | default: 19 | return strings.ToLower(transactionType.String()) 20 | } 21 | } 22 | 23 | func BatchTypeToMetricsBatchType(batchType batchtype.BatchType) string { 24 | switch batchType { 25 | case batchtype.Transfer: 26 | return TransferBatchLabel 27 | case batchtype.MassMigration: 28 | return MMBatchLabel 29 | case batchtype.Create2Transfer: 30 | return C2TBatchLabel 31 | case batchtype.Deposit: 32 | return DepositBatchLabel 33 | default: 34 | return strings.ToLower(batchType.String()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/logger.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func GetCommanderConfigAndSetupLogger() *Config { 12 | cfg := GetConfig() 13 | setupCommanderLogger(cfg) 14 | logConfig(cfg) 15 | return cfg 16 | } 17 | 18 | func GetDeployerConfigAndSetupLogger() *DeployerConfig { 19 | cfg := GetDeployerConfig() 20 | setupDeployerLogger() 21 | logConfig(cfg) 22 | return cfg 23 | } 24 | 25 | func setupCommanderLogger(cfg *Config) { 26 | if cfg.Log.Format == LogFormatJSON { 27 | log.SetFormatter(&log.JSONFormatter{}) 28 | } 29 | log.SetOutput(os.Stdout) 30 | log.SetLevel(cfg.Log.Level) 31 | } 32 | 33 | func setupDeployerLogger() { 34 | log.SetOutput(os.Stdout) 35 | log.SetLevel(log.DebugLevel) 36 | } 37 | 38 | func logConfig(cfg interface{}) { 39 | jsonCfg, err := json.Marshal(cfg) 40 | if err != nil { 41 | log.Panicf("%+v", errors.WithStack(err)) 42 | } 43 | log.Debugf("Loaded config: %s", string(jsonCfg)) 44 | } 45 | -------------------------------------------------------------------------------- /db/tx_controller.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type rawController interface { 10 | Rollback() error 11 | Commit() error 12 | } 13 | 14 | type TxController struct { 15 | tx rawController 16 | isLocked bool 17 | } 18 | 19 | func NewTxController(tx rawController, isLocked bool) *TxController { 20 | return &TxController{tx, isLocked} 21 | } 22 | 23 | // nolint:gocritic 24 | func (t *TxController) Rollback(cause *error) { 25 | if !t.isLocked { 26 | t.isLocked = true 27 | if rollbackErr := t.tx.Rollback(); rollbackErr != nil { 28 | *cause = fmt.Errorf("rollback caused by: %w, failed with: %v", *cause, rollbackErr) 29 | } 30 | } 31 | } 32 | 33 | func (t *TxController) Commit() error { 34 | if !t.isLocked { 35 | t.isLocked = true 36 | err := t.tx.Commit() 37 | if err != nil { 38 | return errors.WithStack(err) 39 | } 40 | return nil 41 | } 42 | return nil 43 | } 44 | 45 | func (t *TxController) IsLocked() bool { 46 | return t.isLocked 47 | } 48 | -------------------------------------------------------------------------------- /models/dispute_transition.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | ) 6 | 7 | type CommitmentInclusionProofBase struct { 8 | StateRoot common.Hash 9 | Path *MerklePath 10 | Witness Witness 11 | } 12 | 13 | type CommitmentInclusionProof struct { 14 | CommitmentInclusionProofBase 15 | BodyRoot common.Hash 16 | } 17 | 18 | type TransferCommitmentInclusionProof struct { 19 | CommitmentInclusionProofBase 20 | Body *TransferBody 21 | } 22 | 23 | type TransferBody struct { 24 | AccountRoot common.Hash 25 | Signature Signature 26 | FeeReceiver uint32 27 | Transactions []byte 28 | } 29 | 30 | type MMCommitmentInclusionProof struct { 31 | CommitmentInclusionProofBase 32 | Body *MMBody 33 | } 34 | 35 | type MMBody struct { 36 | AccountRoot common.Hash 37 | Signature Signature 38 | Meta *MassMigrationMeta 39 | WithdrawRoot common.Hash 40 | Transactions []byte 41 | } 42 | 43 | type StateMerkleProof struct { 44 | UserState *UserState 45 | Witness Witness 46 | } 47 | -------------------------------------------------------------------------------- /commander/tracker/sending.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Worldcoin/hubble-commander/eth" 8 | ) 9 | 10 | var errChannelClosed = fmt.Errorf("channel closed") 11 | 12 | func (t *Tracker) SendRequestedTxs(ctx context.Context) error { 13 | err := t.sendRequestedTxs(ctx) 14 | close(t.requestsChan) 15 | 16 | for request := range t.requestsChan { 17 | request.ResultTxChan <- eth.SendResponse{Error: errChannelClosed} 18 | } 19 | return err 20 | } 21 | 22 | func (t *Tracker) sendRequestedTxs(ctx context.Context) error { 23 | for { 24 | select { 25 | case <-ctx.Done(): 26 | return nil 27 | case request := <-t.requestsChan: 28 | if err := t.sendTx(request); err != nil { 29 | return err 30 | } 31 | } 32 | } 33 | } 34 | 35 | func (t *Tracker) sendTx(request *eth.TxSendingRequest) error { 36 | tx, err := request.Send(t.nonce) 37 | if err != nil { 38 | return err 39 | } 40 | t.nonce++ 41 | if request.ShouldTrackTx { 42 | t.txsChan <- tx 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /models/timestamp.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type Timestamp struct { 9 | time.Time 10 | } 11 | 12 | func NewTimestamp(t time.Time) *Timestamp { 13 | return &Timestamp{Time: t} 14 | } 15 | 16 | func (t Timestamp) Add(d time.Duration) Timestamp { 17 | return Timestamp{t.Time.Add(d)} 18 | } 19 | 20 | func (t Timestamp) Before(other Timestamp) bool { 21 | return t.Time.Before(other.Time) 22 | } 23 | 24 | func (t *Timestamp) MarshalJSON() ([]byte, error) { 25 | return json.Marshal(t.Unix()) 26 | } 27 | 28 | func (t *Timestamp) UnmarshalJSON(data []byte) error { 29 | var seconds int64 30 | err := json.Unmarshal(data, &seconds) 31 | if err != nil { 32 | return err 33 | } 34 | *t = Timestamp{Time: time.Unix(seconds, 0)} 35 | return nil 36 | } 37 | 38 | func (t *Timestamp) Bytes() []byte { 39 | utcTimestamp := t.Time.UTC() 40 | bytes, _ := utcTimestamp.MarshalBinary() 41 | return bytes 42 | } 43 | 44 | func (t *Timestamp) SetBytes(data []byte) error { 45 | return t.UnmarshalBinary(data) 46 | } 47 | -------------------------------------------------------------------------------- /api/sign_create2transfer.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/encoder" 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/models/dto" 8 | ) 9 | 10 | func SignCreate2Transfer(wallet *bls.Wallet, create2Transfer dto.Create2Transfer) (*dto.Create2Transfer, error) { 11 | encodedCreate2Transfer, err := encoder.EncodeCreate2TransferForSigning(&models.Create2Transfer{ 12 | TransactionBase: models.TransactionBase{ 13 | FromStateID: *create2Transfer.FromStateID, 14 | Amount: *create2Transfer.Amount, 15 | Fee: *create2Transfer.Fee, 16 | Nonce: *create2Transfer.Nonce, 17 | }, 18 | ToPublicKey: *create2Transfer.ToPublicKey, 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | signature, err := wallet.Sign(encodedCreate2Transfer) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | create2Transfer.Signature = signature.ModelsSignature() 30 | return &create2Transfer, nil 31 | } 32 | -------------------------------------------------------------------------------- /api/admin/api.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/config" 5 | "github.com/Worldcoin/hubble-commander/eth" 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | ) 8 | 9 | type API struct { 10 | cfg *config.APIConfig 11 | storage *st.Storage 12 | client *eth.Client 13 | enableBatchCreation func(enable bool) 14 | enableTxsAcceptance func(enable bool) 15 | } 16 | 17 | func NewAPI( 18 | cfg *config.APIConfig, 19 | storage *st.Storage, 20 | client *eth.Client, 21 | enableBatchCreation func(enable bool), 22 | enableTxsAcceptance func(enable bool), 23 | ) *API { 24 | return &API{ 25 | cfg: cfg, 26 | storage: storage, 27 | client: client, 28 | enableBatchCreation: enableBatchCreation, 29 | enableTxsAcceptance: enableTxsAcceptance, 30 | } 31 | } 32 | 33 | func NewTestAPI( 34 | cfg *config.APIConfig, 35 | storage *st.Storage, 36 | client *eth.Client, 37 | ) *API { 38 | return NewAPI( 39 | cfg, storage, client, nil, nil, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /models/stored/batch_test.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Worldcoin/hubble-commander/models" 8 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 9 | "github.com/Worldcoin/hubble-commander/utils" 10 | "github.com/Worldcoin/hubble-commander/utils/ref" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestBatch_Bytes(t *testing.T) { 16 | batch := &Batch{ 17 | ID: models.MakeUint256(10), 18 | BType: batchtype.Transfer, 19 | TransactionHash: utils.RandomHash(), 20 | Hash: &common.Hash{8, 6, 4}, 21 | FinalisationBlock: ref.Uint32(25), 22 | AccountTreeRoot: &common.Hash{4, 5, 6}, 23 | PrevStateRoot: &common.Hash{7, 8, 9}, 24 | MinedTime: models.NewTimestamp(time.Unix(10, 11).UTC()), 25 | } 26 | 27 | bytes := batch.Bytes() 28 | 29 | decodedBatch := Batch{} 30 | err := decodedBatch.SetBytes(bytes) 31 | require.NoError(t, err) 32 | require.EqualValues(t, *batch, decodedBatch) 33 | } 34 | -------------------------------------------------------------------------------- /api/get_public_key.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/storage" 6 | ) 7 | 8 | var getPublicKeyAPIErrors = map[error]*APIError{ 9 | storage.AnyNotFoundError: NewAPIError( 10 | 99001, 11 | "public key not found", 12 | ), 13 | } 14 | 15 | func (a *API) GetPublicKeyByPubKeyID(id uint32) (*models.PublicKey, error) { 16 | publicKey, err := a.unsafeGetPublicKeyByID(id) 17 | if err != nil { 18 | return nil, sanitizeError(err, getPublicKeyAPIErrors) 19 | } 20 | 21 | return publicKey, nil 22 | } 23 | 24 | func (a *API) unsafeGetPublicKeyByID(id uint32) (*models.PublicKey, error) { 25 | leaf, err := a.storage.AccountTree.Leaf(id) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &leaf.PublicKey, nil 30 | } 31 | 32 | func (a *API) GetPublicKeyByStateID(id uint32) (*models.PublicKey, error) { 33 | publicKey, err := a.storage.GetPublicKeyByStateID(id) 34 | if err != nil { 35 | return nil, sanitizeError(err, getPublicKeyAPIErrors) 36 | } 37 | 38 | return publicKey, nil 39 | } 40 | -------------------------------------------------------------------------------- /bls/domain_test.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ethereum/go-ethereum/crypto" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDomainFromBytes(t *testing.T) { 14 | data := crypto.Keccak256([]byte{1, 2, 3}) 15 | domain, err := DomainFromBytes(data) 16 | require.NoError(t, err) 17 | require.Equal(t, data, domain.Bytes()) 18 | } 19 | 20 | func TestDomainFromBytes_InvalidLength(t *testing.T) { 21 | data := make([]byte, 20) 22 | domain, err := DomainFromBytes(data) 23 | require.Equal(t, ErrInvalidDomainLength, err) 24 | require.Nil(t, domain) 25 | } 26 | 27 | func TestDomain_MarshalJSON(t *testing.T) { 28 | domain := Domain{1, 2, 3} 29 | expected := fmt.Sprintf("\"0x010203%s\"", strings.Repeat("0", DomainLength*2-6)) 30 | data, err := json.Marshal(&domain) 31 | require.NoError(t, err) 32 | require.Equal(t, expected, string(data)) 33 | 34 | var result Domain 35 | err = json.Unmarshal(data, &result) 36 | require.NoError(t, err) 37 | require.Equal(t, domain, result) 38 | } 39 | -------------------------------------------------------------------------------- /commander/applier/synced_tx.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import "github.com/Worldcoin/hubble-commander/models" 4 | 5 | type Proofs struct { 6 | SenderStateProof models.StateMerkleProof 7 | ReceiverStateProof *models.StateMerkleProof 8 | } 9 | 10 | type SyncedTxWithProofs struct { 11 | Tx models.GenericTransaction 12 | Proofs 13 | } 14 | 15 | func NewSyncedTxWithProofs(tx models.GenericTransaction, senderUserState, receiverUserState *models.UserState) *SyncedTxWithProofs { 16 | return &SyncedTxWithProofs{ 17 | Tx: tx, 18 | Proofs: Proofs{ 19 | SenderStateProof: models.StateMerkleProof{ 20 | UserState: senderUserState, 21 | }, 22 | ReceiverStateProof: &models.StateMerkleProof{ 23 | UserState: receiverUserState, 24 | }, 25 | }, 26 | } 27 | } 28 | 29 | func NewSyncedTxWithSenderProof(tx models.GenericTransaction, senderUserState *models.UserState) *SyncedTxWithProofs { 30 | return &SyncedTxWithProofs{ 31 | Tx: tx, 32 | Proofs: Proofs{ 33 | SenderStateProof: models.StateMerkleProof{ 34 | UserState: senderUserState, 35 | }, 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /commander/syncer/txs_context.go: -------------------------------------------------------------------------------- 1 | package syncer 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/config" 5 | "github.com/Worldcoin/hubble-commander/eth" 6 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 7 | st "github.com/Worldcoin/hubble-commander/storage" 8 | ) 9 | 10 | type TxsContext struct { 11 | cfg *config.RollupConfig 12 | storage *st.Storage 13 | client *eth.Client 14 | Syncer TransactionSyncer 15 | TxType txtype.TransactionType 16 | } 17 | 18 | func NewTestTxsContext( 19 | storage *st.Storage, 20 | client *eth.Client, 21 | cfg *config.RollupConfig, 22 | txType txtype.TransactionType, 23 | ) (*TxsContext, error) { 24 | return newTxsContext(storage, client, cfg, txType), nil 25 | } 26 | 27 | func newTxsContext( 28 | storage *st.Storage, 29 | client *eth.Client, 30 | cfg *config.RollupConfig, 31 | txType txtype.TransactionType, 32 | ) *TxsContext { 33 | return &TxsContext{ 34 | cfg: cfg, 35 | storage: storage, 36 | client: client, 37 | Syncer: NewTransactionSyncer(storage, txType), 38 | TxType: txType, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /commander/executor/tx_priority.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import "github.com/Worldcoin/hubble-commander/models" 4 | 5 | func byPriority(txs models.GenericTransactionArray) (slice interface{}, less func(i, j int) bool) { 6 | return txs, func(i, j int) bool { 7 | return higherPriority(txs.At(i), txs.At(j)) 8 | } 9 | } 10 | 11 | func higherPriority(leftTx, rightTx models.GenericTransaction) bool { 12 | left, right := leftTx.GetBase(), rightTx.GetBase() 13 | 14 | nonceComparison := left.Nonce.Cmp(&right.Nonce) 15 | if nonceComparison == 0 { 16 | feeComparison := left.Fee.Cmp(&right.Fee) 17 | if feeComparison == 0 { 18 | return earlierTimestamp(left.ReceiveTime, right.ReceiveTime) // earlier receive time first, if receive time is nil push to the back 19 | } 20 | return feeComparison > 0 // highest fee first 21 | } 22 | return nonceComparison < 0 // lowest nonce first 23 | } 24 | 25 | func earlierTimestamp(left, right *models.Timestamp) bool { 26 | if left == nil { 27 | return false 28 | } 29 | if right == nil { 30 | return true 31 | } 32 | return left.Before(*right) 33 | } 34 | -------------------------------------------------------------------------------- /eth/domain_test.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/bls" 7 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 8 | "github.com/stretchr/testify/require" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type DomainTestSuite struct { 13 | *require.Assertions 14 | suite.Suite 15 | client *TestClient 16 | } 17 | 18 | func (s *DomainTestSuite) SetupSuite() { 19 | s.Assertions = require.New(s.T()) 20 | } 21 | 22 | func (s *DomainTestSuite) SetupTest() { 23 | client, err := NewTestClient() 24 | s.NoError(err) 25 | s.client = client 26 | } 27 | 28 | func (s *DomainTestSuite) TearDownTest() { 29 | s.client.Close() 30 | } 31 | 32 | func (s *DomainTestSuite) TestGetDomain() { 33 | expectedDomain, err := s.client.Rollup.DomainSeparator(&bind.CallOpts{}) 34 | s.NoError(err) 35 | 36 | domain, err := s.client.GetDomain() 37 | s.NoError(err) 38 | s.Equal(bls.Domain(expectedDomain), *domain) 39 | s.Equal(s.client.domain, domain) 40 | } 41 | 42 | func TestDomainTestSuite(t *testing.T) { 43 | suite.Run(t, new(DomainTestSuite)) 44 | } 45 | -------------------------------------------------------------------------------- /models/dto/transaction_base.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type TransactionBase struct { 10 | Hash common.Hash 11 | TxType txtype.TransactionType 12 | FromStateID uint32 13 | Amount models.Uint256 14 | Fee models.Uint256 15 | Nonce models.Uint256 16 | Signature models.Signature 17 | ReceiveTime *models.Timestamp 18 | CommitmentSlot *models.CommitmentSlot 19 | ErrorMessage *string 20 | } 21 | 22 | func MakeTransactionBase(tx *models.TransactionBase) TransactionBase { 23 | return TransactionBase{ 24 | Hash: tx.Hash, 25 | TxType: tx.TxType, 26 | FromStateID: tx.FromStateID, 27 | Amount: tx.Amount, 28 | Fee: tx.Fee, 29 | Nonce: tx.Nonce, 30 | Signature: tx.Signature, 31 | ReceiveTime: tx.ReceiveTime, 32 | CommitmentSlot: tx.CommitmentSlot, 33 | ErrorMessage: tx.ErrorMessage, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /eth/filter_logs.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/metrics" 5 | "github.com/ethereum/go-ethereum" 6 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/ethereum/go-ethereum/event" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type Iterator interface { 13 | SetData(contract *bind.BoundContract, event string, logs chan types.Log, sub ethereum.Subscription) 14 | } 15 | 16 | func (c *Client) FilterLogs(contract *bind.BoundContract, eventName string, opts *bind.FilterOpts, it Iterator) (err error) { 17 | var ( 18 | logs chan types.Log 19 | sub event.Subscription 20 | ) 21 | 22 | duration, err := metrics.MeasureDuration(func() error { 23 | logs, sub, err = contract.FilterLogs(opts, eventName) 24 | return errors.WithStack(err) 25 | }) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | c.Metrics.SaveBlockchainCallDurationMeasurement(*duration, metrics.EventNameToMetricsEventFilterCallLabel(eventName)) 31 | 32 | it.SetData(contract, eventName, logs, sub) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /storage/test_storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/db" 5 | ) 6 | 7 | type TestStorage struct { 8 | *Storage 9 | Teardown func() error 10 | } 11 | 12 | type TeardownFunc func() error 13 | 14 | func NewTestStorage() (*TestStorage, error) { 15 | badgerTestDB, err := db.NewTestDB() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | database := &Database{ 21 | Badger: badgerTestDB.DB, 22 | } 23 | 24 | storage, err := newStorageFromDatabase(database) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &TestStorage{ 30 | Storage: storage, 31 | Teardown: badgerTestDB.Teardown, 32 | }, nil 33 | } 34 | 35 | func (s *TestStorage) Clone() (*TestStorage, error) { 36 | database := *s.database 37 | 38 | testBadger := db.TestDB{DB: s.database.Badger} 39 | clonedBadger, err := testBadger.Clone() 40 | if err != nil { 41 | return nil, err 42 | } 43 | database.Badger = clonedBadger.DB 44 | 45 | return &TestStorage{ 46 | Storage: s.copyWithNewDatabase(&database), 47 | Teardown: clonedBadger.Teardown, 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /models/create2transfer.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 5 | ) 6 | 7 | type Create2Transfer struct { 8 | TransactionBase 9 | ToStateID *uint32 10 | ToPublicKey PublicKey 11 | } 12 | 13 | func (t *Create2Transfer) Type() txtype.TransactionType { 14 | return txtype.Create2Transfer 15 | } 16 | 17 | func (t *Create2Transfer) GetBase() *TransactionBase { 18 | return &t.TransactionBase 19 | } 20 | 21 | func (t *Create2Transfer) GetToStateID() *uint32 { 22 | return t.ToStateID 23 | } 24 | 25 | func (t *Create2Transfer) ToTransfer() *Transfer { 26 | panic("Create2Transfer cannot be cast to Transfer") 27 | } 28 | 29 | func (t *Create2Transfer) ToCreate2Transfer() *Create2Transfer { 30 | return t 31 | } 32 | 33 | func (t *Create2Transfer) ToMassMigration() *MassMigration { 34 | panic("Create2Transfer cannot be cast to MassMigration") 35 | } 36 | 37 | // nolint:gocritic 38 | func (t Create2Transfer) Copy() GenericTransaction { 39 | return &t 40 | } 41 | 42 | // nolint:gocritic 43 | func (t Create2Transfer) Clone() *Create2Transfer { 44 | return &t 45 | } 46 | -------------------------------------------------------------------------------- /eth/errors.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Worldcoin/hubble-commander/utils/ref" 7 | ) 8 | 9 | type LogNotFoundError struct { 10 | logName string 11 | } 12 | 13 | func NewLogNotFoundError(logName string) *LogNotFoundError { 14 | return &LogNotFoundError{logName} 15 | } 16 | 17 | func (e LogNotFoundError) Error() string { 18 | return fmt.Sprintf("log not found in the receipt: %s", e.logName) 19 | } 20 | 21 | type DisputeTxRevertedError struct { 22 | batchID uint64 23 | reason *string 24 | } 25 | 26 | func NewDisputeTxRevertedError(batchID uint64, reason string) *DisputeTxRevertedError { 27 | return &DisputeTxRevertedError{ 28 | batchID: batchID, 29 | reason: ref.String(reason), 30 | } 31 | } 32 | 33 | func NewUnknownDisputeTxRevertedError(batchID uint64) *DisputeTxRevertedError { 34 | return &DisputeTxRevertedError{ 35 | batchID: batchID, 36 | } 37 | } 38 | 39 | func (e DisputeTxRevertedError) Error() string { 40 | msg := fmt.Sprintf("dispute of batch #%d failed", e.batchID) 41 | if e.reason != nil { 42 | msg += fmt.Sprintf(": %s", *e.reason) 43 | } 44 | return msg 45 | } 46 | -------------------------------------------------------------------------------- /models/commitment_base.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 5 | "github.com/ethereum/go-ethereum/common" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | const CommitmentBaseDataLength = CommitmentIDDataLength + 1 + 32 10 | 11 | type CommitmentBase struct { 12 | ID CommitmentID 13 | Type batchtype.BatchType 14 | PostStateRoot common.Hash 15 | } 16 | 17 | func (c *CommitmentBase) GetPostStateRoot() common.Hash { 18 | return c.PostStateRoot 19 | } 20 | 21 | func (c *CommitmentBase) Bytes() []byte { 22 | b := make([]byte, CommitmentBaseDataLength) 23 | copy(b[0:33], c.ID.Bytes()) 24 | b[33] = byte(c.Type) 25 | copy(b[34:66], c.PostStateRoot.Bytes()) 26 | 27 | return b 28 | } 29 | 30 | func (c *CommitmentBase) SetBytes(data []byte) error { 31 | if len(data) != CommitmentBaseDataLength { 32 | return errors.WithStack(ErrInvalidLength) 33 | } 34 | err := c.ID.SetBytes(data[0:33]) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | c.Type = batchtype.BatchType(data[33]) 40 | c.PostStateRoot.SetBytes(data[34:66]) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /commander/executor/new_commitment.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 6 | ) 7 | 8 | func (c *DepositsContext) newCommitment( 9 | batchID models.Uint256, 10 | depositSubtree *models.PendingDepositSubtree, 11 | ) (*models.DepositCommitment, error) { 12 | stateRoot, err := c.storage.StateTree.Root() 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return &models.DepositCommitment{ 18 | CommitmentBase: models.CommitmentBase{ 19 | ID: models.CommitmentID{ 20 | BatchID: batchID, 21 | IndexInBatch: 0, 22 | }, 23 | Type: batchtype.Deposit, 24 | PostStateRoot: *stateRoot, 25 | }, 26 | SubtreeID: depositSubtree.ID, 27 | SubtreeRoot: depositSubtree.Root, 28 | Deposits: depositSubtree.Deposits, 29 | }, nil 30 | } 31 | 32 | func (c *TxsContext) NextCommitmentID() (*models.CommitmentID, error) { 33 | nextBatchID, err := c.storage.GetNextBatchID() 34 | if err != nil { 35 | return nil, err 36 | } 37 | return &models.CommitmentID{BatchID: *nextBatchID}, nil 38 | } 39 | -------------------------------------------------------------------------------- /metrics/const.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | const ( 4 | namespace = "hubble" 5 | 6 | // Subsystems 7 | apiSubsystem = "api" 8 | rollupSubsystem = "rollup" 9 | syncingSubsystem = "syncing" 10 | blockchainSubsystem = "blockchain" 11 | ) 12 | 13 | // API metrics 14 | const ( 15 | // Transaction labels 16 | TransferTxLabel = "transfer" 17 | C2TTxLabel = "create2transfer" 18 | MMTxLabel = "mass_migration" 19 | 20 | // Transaction statuses 21 | AcceptedTxStatus = "accepted" 22 | RejectedTxStatus = "rejected" 23 | ) 24 | 25 | // Rollup metrics 26 | const ( 27 | TransferBatchLabel = "transfer" 28 | C2TBatchLabel = "create2transfer" 29 | MMBatchLabel = "mass_migration" 30 | DepositBatchLabel = "deposit" 31 | ) 32 | 33 | // Syncing metrics 34 | const ( 35 | SyncAccountsMethod = "sync_accounts" 36 | SyncBatchesMethod = "sync_batches" 37 | SyncRangeMethod = "sync_range" 38 | SyncDepositsMethod = "sync_deposits" 39 | SyncTokensMethod = "sync_tokens" 40 | SyncSpokesMethod = "sync_spokes" 41 | SyncStakeWithdrawalsMethod = "sync_stake_withdrawals" 42 | ) 43 | -------------------------------------------------------------------------------- /models/stored/batched_tx_test.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 8 | "github.com/Worldcoin/hubble-commander/utils" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBatchedTx_GenericTransactionRoundTrip(t *testing.T) { 13 | batchedTx := BatchedTx{ 14 | PendingTx: PendingTx{ 15 | Hash: utils.RandomHash(), 16 | 17 | TxType: txtype.Transfer, 18 | FromStateID: 1, 19 | Amount: models.MakeUint256(1000), 20 | Fee: models.MakeUint256(100), 21 | Nonce: models.MakeUint256(0), 22 | Signature: models.MakeRandomSignature(), 23 | ReceiveTime: nil, 24 | 25 | Body: &TxTransferBody{ 26 | ToStateID: 2, 27 | }, 28 | }, 29 | ID: models.CommitmentSlot{ 30 | BatchID: models.MakeUint256(1), 31 | IndexInBatch: 2, 32 | IndexInCommitment: 3, 33 | }, 34 | } 35 | 36 | origBytes := batchedTx.Bytes() 37 | roundTripBytes := NewBatchedTx(batchedTx.ToGenericTransaction()).Bytes() 38 | require.Equal(t, origBytes, roundTripBytes) 39 | } 40 | -------------------------------------------------------------------------------- /api/admin/authentication_test.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Worldcoin/hubble-commander/api/rpc" 8 | "github.com/Worldcoin/hubble-commander/config" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestAPI_verifyAuthKey_ValidKey(t *testing.T) { 13 | api := API{cfg: &config.APIConfig{AuthenticationKey: authKeyValue}} 14 | 15 | err := api.verifyAuthKey(contextWithAuthKey(authKeyValue)) 16 | require.NoError(t, err) 17 | } 18 | 19 | func TestAPI_verifyAuthKey_MissingKey(t *testing.T) { 20 | api := API{cfg: &config.APIConfig{AuthenticationKey: authKeyValue}} 21 | 22 | err := api.verifyAuthKey(context.Background()) 23 | require.ErrorIs(t, err, errMissingAuthKey) 24 | } 25 | 26 | func TestAPI_verifyAuthKey_InvalidKey(t *testing.T) { 27 | api := API{cfg: &config.APIConfig{AuthenticationKey: authKeyValue}} 28 | 29 | err := api.verifyAuthKey(contextWithAuthKey("invalid key")) 30 | require.ErrorIs(t, err, errInvalidAuthKey) 31 | } 32 | 33 | func contextWithAuthKey(authKeyValue string) context.Context { 34 | return context.WithValue(context.Background(), rpc.AuthKey, authKeyValue) 35 | } 36 | -------------------------------------------------------------------------------- /bls/public_key.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/kilic/bn254/bls" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func fromBLSPublicKey(blsPK *bls.PublicKey) *models.PublicKey { 10 | var pk models.PublicKey 11 | pkBytes := blsPK.ToBytes() 12 | copy(pk[:32], pkBytes[32:64]) 13 | copy(pk[32:64], pkBytes[:32]) 14 | copy(pk[64:96], pkBytes[96:]) 15 | copy(pk[96:], pkBytes[64:96]) 16 | return &pk 17 | } 18 | 19 | func toBLSPublicKey(pk *models.PublicKey) *bls.PublicKey { 20 | blsBytes := make([]byte, 128) 21 | copy(blsBytes[:32], pk[32:64]) 22 | copy(blsBytes[32:64], pk[:32]) 23 | copy(blsBytes[64:96], pk[96:]) 24 | copy(blsBytes[96:], pk[64:96]) 25 | blsPK, err := bls.PublicKeyFromBytes(blsBytes) 26 | if err != nil { 27 | log.Panicf("failed to convert public key byte array to bls type: %v", err) 28 | } 29 | return blsPK 30 | } 31 | 32 | func PrivateToPublicKey(privateKey [32]byte) (*models.PublicKey, error) { 33 | keyPair, err := bls.NewKeyPairFromSecret(privateKey[:]) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return fromBLSPublicKey(keyPair.Public), nil 38 | } 39 | -------------------------------------------------------------------------------- /commander/rollup_controls.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | ) 7 | 8 | // nolint:structcheck 9 | type rollupControls struct { 10 | batchCreationEnabled bool 11 | migrate uint32 12 | 13 | rollupLoopActive uint32 14 | cancelRollupLoop context.CancelFunc 15 | } 16 | 17 | func makeRollupControls(migrate bool) rollupControls { 18 | controls := rollupControls{ 19 | batchCreationEnabled: true, 20 | } 21 | controls.setMigrate(migrate) 22 | return controls 23 | } 24 | 25 | func (c *rollupControls) isRollupLoopActive() bool { 26 | return atomic.LoadUint32(&c.rollupLoopActive) != 0 27 | } 28 | 29 | func (c *rollupControls) setRollupLoopActive(active bool) { 30 | activeFlag := uint32(0) 31 | if active { 32 | activeFlag = 1 33 | } 34 | atomic.StoreUint32(&c.rollupLoopActive, activeFlag) 35 | } 36 | 37 | func (c *rollupControls) isMigrating() bool { 38 | return atomic.LoadUint32(&c.migrate) != 0 39 | } 40 | 41 | func (c *rollupControls) setMigrate(migrate bool) { 42 | migrateFlag := uint32(0) 43 | if migrate { 44 | migrateFlag = 1 45 | } 46 | atomic.StoreUint32(&c.migrate, migrateFlag) 47 | } 48 | -------------------------------------------------------------------------------- /models/stored/primitive_encoder.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | func EncodeHash(hash *common.Hash) ([]byte, error) { 10 | return hash.Bytes(), nil 11 | } 12 | 13 | func DecodeHash(data []byte, hash *common.Hash) error { 14 | hash.SetBytes(data) 15 | return nil 16 | } 17 | 18 | func EncodeUint32(value uint32) []byte { 19 | b := make([]byte, 4) 20 | binary.BigEndian.PutUint32(b, value) 21 | return b 22 | } 23 | 24 | func DecodeUint32(data []byte, number *uint32) error { 25 | newUint32 := binary.BigEndian.Uint32(data) 26 | *number = newUint32 27 | return nil 28 | } 29 | 30 | func EncodeUint64(value uint64) []byte { 31 | b := make([]byte, 8) 32 | binary.BigEndian.PutUint64(b, value) 33 | return b 34 | } 35 | 36 | func DecodeUint64(data []byte, value *uint64) error { 37 | newUint64 := binary.BigEndian.Uint64(data) 38 | *value = newUint64 39 | return nil 40 | } 41 | 42 | func EncodeString(value string) []byte { 43 | return []byte(value) 44 | } 45 | 46 | func DecodeString(data []byte, value *string) error { 47 | *value = string(data) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /eth/register_account_test.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/stretchr/testify/require" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type RegisterAccountTestSuite struct { 12 | *require.Assertions 13 | suite.Suite 14 | client *TestClient 15 | } 16 | 17 | func (s *RegisterAccountTestSuite) SetupSuite() { 18 | s.Assertions = require.New(s.T()) 19 | } 20 | 21 | func (s *RegisterAccountTestSuite) SetupTest() { 22 | client, err := NewTestClient() 23 | s.NoError(err) 24 | s.client = client 25 | } 26 | 27 | func (s *RegisterAccountTestSuite) TearDownTest() { 28 | s.client.Close() 29 | } 30 | 31 | func (s *RegisterAccountTestSuite) TestRegisterAccountAndWait() { 32 | publicKey := models.PublicKey{1, 2, 3} 33 | pubKeyID, err := s.client.RegisterAccountAndWait(&publicKey) 34 | s.NoError(err) 35 | s.Equal(uint32(0), *pubKeyID) 36 | 37 | pubKeyID, err = s.client.RegisterAccountAndWait(&publicKey) 38 | s.NoError(err) 39 | s.Equal(uint32(1), *pubKeyID) 40 | } 41 | 42 | func TestRegisterAccountTestSuite(t *testing.T) { 43 | suite.Run(t, new(RegisterAccountTestSuite)) 44 | } 45 | -------------------------------------------------------------------------------- /eth/get_blocks_to_finalise_test.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/config" 7 | "github.com/stretchr/testify/require" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type GetBlocksToFinaliseTestSuite struct { 12 | *require.Assertions 13 | suite.Suite 14 | client *TestClient 15 | } 16 | 17 | func (s *GetBlocksToFinaliseTestSuite) SetupSuite() { 18 | s.Assertions = require.New(s.T()) 19 | } 20 | 21 | func (s *GetBlocksToFinaliseTestSuite) SetupTest() { 22 | client, err := NewTestClient() 23 | s.NoError(err) 24 | s.client = client 25 | } 26 | 27 | func (s *GetBlocksToFinaliseTestSuite) TearDownTest() { 28 | s.client.Close() 29 | } 30 | 31 | func (s *GetBlocksToFinaliseTestSuite) TestGetBlocksToFinalise() { 32 | expected := int64(5760) 33 | s.Equal(expected, int64(config.DefaultBlocksToFinalise)) 34 | 35 | blocksToFinalise, err := s.client.GetBlocksToFinalise() 36 | s.NoError(err) 37 | s.Equal(expected, *blocksToFinalise) 38 | s.Equal(expected, *s.client.blocksToFinalise) 39 | } 40 | 41 | func TestGetBlocksToFinaliseTestSuite(t *testing.T) { 42 | suite.Run(t, new(GetBlocksToFinaliseTestSuite)) 43 | } 44 | -------------------------------------------------------------------------------- /models/deposit_commitment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/utils" 5 | "github.com/Worldcoin/hubble-commander/utils/consts" 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type DepositCommitment struct { 10 | CommitmentBase 11 | SubtreeID Uint256 12 | SubtreeRoot common.Hash 13 | Deposits []PendingDeposit 14 | } 15 | 16 | func (c *DepositCommitment) GetCommitmentBase() *CommitmentBase { 17 | return &c.CommitmentBase 18 | } 19 | 20 | func (c *DepositCommitment) SetBodyHash(_ *common.Hash) { 21 | // NOOP 22 | } 23 | 24 | func (c *DepositCommitment) GetBodyHash() *common.Hash { 25 | return &consts.ZeroHash 26 | } 27 | 28 | func (c *DepositCommitment) LeafHash() common.Hash { 29 | return utils.HashTwo(c.PostStateRoot, *c.GetBodyHash()) 30 | } 31 | 32 | func (c *DepositCommitment) ToTxCommitment() *TxCommitment { 33 | panic("cannot cast DepositCommitment to TxCommitment") 34 | } 35 | 36 | func (c *DepositCommitment) ToMMCommitment() *MMCommitment { 37 | panic("cannot cast DepositCommitment to MMCommitment") 38 | } 39 | 40 | func (c *DepositCommitment) ToDepositCommitment() *DepositCommitment { 41 | return c 42 | } 43 | -------------------------------------------------------------------------------- /models/transaction_base.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/enums/txtype" 7 | "github.com/ethereum/go-ethereum/common" 8 | ) 9 | 10 | type TransactionBase struct { 11 | Hash common.Hash 12 | TxType txtype.TransactionType 13 | FromStateID uint32 14 | Amount Uint256 15 | Fee Uint256 16 | Nonce Uint256 17 | Signature Signature 18 | ReceiveTime *Timestamp 19 | CommitmentSlot *CommitmentSlot 20 | ErrorMessage *string 21 | } 22 | 23 | func (t *TransactionBase) GetFromStateID() uint32 { 24 | return t.FromStateID 25 | } 26 | 27 | func (t *TransactionBase) GetAmount() Uint256 { 28 | return t.Amount 29 | } 30 | 31 | func (t *TransactionBase) GetFee() Uint256 { 32 | return t.Fee 33 | } 34 | 35 | func (t *TransactionBase) GetNonce() Uint256 { 36 | return t.Nonce 37 | } 38 | 39 | func (t *TransactionBase) SetNonce(nonce Uint256) { 40 | t.Nonce = nonce 41 | } 42 | 43 | func (t *TransactionBase) GetSignature() Signature { 44 | return t.Signature 45 | } 46 | 47 | func (t *TransactionBase) SetReceiveTime() { 48 | t.ReceiveTime = NewTimestamp(time.Now().UTC()) 49 | } 50 | -------------------------------------------------------------------------------- /commander/executor/deposit_context.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/commander/prover" 7 | "github.com/Worldcoin/hubble-commander/config" 8 | "github.com/Worldcoin/hubble-commander/eth" 9 | "github.com/Worldcoin/hubble-commander/metrics" 10 | st "github.com/Worldcoin/hubble-commander/storage" 11 | ) 12 | 13 | type DepositsContext struct { 14 | *ExecutionContext 15 | proverCtx *prover.Context 16 | } 17 | 18 | func NewDepositsContext( 19 | storage *st.Storage, 20 | client *eth.Client, 21 | cfg *config.RollupConfig, 22 | commanderMetrics *metrics.CommanderMetrics, 23 | ctx context.Context, 24 | ) *DepositsContext { 25 | executionCtx := NewExecutionContext(storage, client, cfg, commanderMetrics, ctx) 26 | return newDepositsContext(executionCtx) 27 | } 28 | 29 | func NewTestDepositsContext(executionCtx *ExecutionContext) *DepositsContext { 30 | return newDepositsContext(executionCtx) 31 | } 32 | 33 | func newDepositsContext(executionCtx *ExecutionContext) *DepositsContext { 34 | return &DepositsContext{ 35 | ExecutionContext: executionCtx, 36 | proverCtx: prover.NewContext(executionCtx.storage), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /models/meta_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Worldcoin/hubble-commander/utils" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMassMigrationMeta_Bytes_ReturnsACopy(t *testing.T) { 11 | meta := MassMigrationMeta{ 12 | SpokeID: 1, 13 | TokenID: MakeUint256(2), 14 | Amount: MakeUint256(3), 15 | FeeReceiver: 4, 16 | } 17 | 18 | expected := meta 19 | 20 | bytes := meta.Bytes() 21 | bytes[0] = 9 22 | 23 | require.Equal(t, expected, meta) 24 | } 25 | 26 | func TestMassMigrationMeta_SetBytes(t *testing.T) { 27 | meta := MassMigrationMeta{ 28 | SpokeID: 1, 29 | TokenID: MakeUint256(2), 30 | Amount: MakeUint256(3), 31 | FeeReceiver: 4, 32 | } 33 | 34 | bytes := meta.Bytes() 35 | newMeta := MassMigrationMeta{} 36 | err := newMeta.SetBytes(bytes) 37 | require.NoError(t, err) 38 | require.Equal(t, meta, newMeta) 39 | } 40 | 41 | func TestMassMigrationMeta_SetBytes_InvalidLength(t *testing.T) { 42 | bytes := utils.PadLeft([]byte{1, 2, 3}, 130) 43 | meta := MassMigrationMeta{} 44 | err := meta.SetBytes(bytes) 45 | require.NotNil(t, err) 46 | require.ErrorIs(t, err, ErrInvalidLength) 47 | } 48 | -------------------------------------------------------------------------------- /contracts/depositmanager/events.go: -------------------------------------------------------------------------------- 1 | package depositmanager 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum" 5 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 6 | "github.com/ethereum/go-ethereum/core/types" 7 | ) 8 | 9 | type DepositQueuedIterator struct { 10 | DepositManagerDepositQueuedIterator 11 | } 12 | 13 | func (i *DepositQueuedIterator) SetData(contract *bind.BoundContract, event string, logs chan types.Log, sub ethereum.Subscription) { 14 | i.DepositManagerDepositQueuedIterator.contract = contract 15 | i.DepositManagerDepositQueuedIterator.event = event 16 | i.DepositManagerDepositQueuedIterator.logs = logs 17 | i.DepositManagerDepositQueuedIterator.sub = sub 18 | } 19 | 20 | type DepositSubTreeReadyIterator struct { 21 | DepositManagerDepositSubTreeReadyIterator 22 | } 23 | 24 | func (i *DepositSubTreeReadyIterator) SetData(contract *bind.BoundContract, event string, logs chan types.Log, sub ethereum.Subscription) { 25 | i.DepositManagerDepositSubTreeReadyIterator.contract = contract 26 | i.DepositManagerDepositSubTreeReadyIterator.event = event 27 | i.DepositManagerDepositSubTreeReadyIterator.logs = logs 28 | i.DepositManagerDepositSubTreeReadyIterator.sub = sub 29 | } 30 | -------------------------------------------------------------------------------- /metrics/blockchain_metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func (c *CommanderMetrics) initializeBlockchainMetrics() { 10 | blockchainCallDuration := prometheus.NewHistogramVec( 11 | prometheus.HistogramOpts{ 12 | Namespace: namespace, 13 | Subsystem: blockchainSubsystem, 14 | Name: "method_call_duration_milliseconds", 15 | Help: "Durations of blockchain calls", 16 | Buckets: []float64{ 17 | 0.0, 18 | 1.0, 19 | 2.0, 20 | 3.0, 21 | 4.0, 22 | 5.0, 23 | 10.0, 24 | 15.0, 25 | 20.0, 26 | 30.0, 27 | 40.0, 28 | 50.0, 29 | 100.0, 30 | }, 31 | }, 32 | []string{"method"}, 33 | ) 34 | 35 | c.registry.MustRegister( 36 | blockchainCallDuration, 37 | ) 38 | 39 | c.BlockchainCallDuration = blockchainCallDuration 40 | } 41 | 42 | func (c *CommanderMetrics) SaveBlockchainCallDurationMeasurement( 43 | duration time.Duration, 44 | contractEventMetricsLabel string, 45 | ) { 46 | c.BlockchainCallDuration. 47 | With(prometheus.Labels{ 48 | "method": contractEventMetricsLabel, 49 | }). 50 | Observe(float64(duration.Milliseconds())) 51 | } 52 | -------------------------------------------------------------------------------- /docs/withdrawals.md: -------------------------------------------------------------------------------- 1 | # Withdrawals 2 | 3 | ## Flow 4 | 5 | 1. User sends a mass migration transaction with a selected spoke. 6 | 2. User waits a certain amount of time until the batch with sent transactions is finalized (finalization status can be checked 7 | with `hubble_getBatch`, `hubble_getCommitment` or `hubble_getTransaction`). 8 | 3. User gathers the mass migration commitment inclusion proof with `hubble_getMassMigrationCommitmentProof`. 9 | 4. User or anyone else calls `WithdrawManager.processWithdrawCommitment`. 10 | 1. `WithdrawManager` verifies the entire commitment and marks it as processed (`WithdrawManager.claimTokens` can be now called for all 11 | mass migration transactions from said commitment). 12 | 2. ERC20 tokens are transferred from the `Vault` to the `WithdrawManager` contract. 13 | 5. User gathers the withdrawal proof with `hubble_getWithdrawProof`. 14 | 6. User gathers the public key proof with `hubble_getPublicKeyProofByPubKeyID`. 15 | 7. User signs their ethereum address with their BLS private key. 16 | 8. User calls `WithdrawManager.claimTokens`. 17 | 1. `WithdrawManager` verifies the request. 18 | 2. ERC20 tokens are transferred from the `WithdrawManager` to the user. 19 | -------------------------------------------------------------------------------- /eth/contracts.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/contracts/accountregistry" 5 | "github.com/Worldcoin/hubble-commander/contracts/depositmanager" 6 | "github.com/Worldcoin/hubble-commander/contracts/rollup" 7 | "github.com/Worldcoin/hubble-commander/contracts/spokeregistry" 8 | "github.com/Worldcoin/hubble-commander/contracts/tokenregistry" 9 | "github.com/ethereum/go-ethereum/accounts/abi" 10 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 11 | ) 12 | 13 | type Contract struct { 14 | ABI *abi.ABI 15 | BoundContract *bind.BoundContract 16 | } 17 | 18 | func MakeContract(abi_ *abi.ABI, boundContract *bind.BoundContract) Contract { 19 | return Contract{ABI: abi_, BoundContract: boundContract} 20 | } 21 | 22 | type AccountRegistry struct { 23 | *accountregistry.AccountRegistry 24 | Contract 25 | } 26 | 27 | type Rollup struct { 28 | *rollup.Rollup 29 | Contract 30 | } 31 | 32 | type TokenRegistry struct { 33 | *tokenregistry.TokenRegistry 34 | Contract 35 | } 36 | 37 | type DepositManager struct { 38 | *depositmanager.DepositManager 39 | Contract 40 | } 41 | 42 | type SpokeRegistry struct { 43 | *spokeregistry.SpokeRegistry 44 | Contract 45 | } 46 | -------------------------------------------------------------------------------- /api/get_public_key_proof.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/dto" 5 | "github.com/Worldcoin/hubble-commander/storage" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | var getPublicKeyProofAPIErrors = map[error]*APIError{ 10 | storage.AnyNotFoundError: NewAPIError(50002, "public key inclusion proof could not be generated"), 11 | } 12 | 13 | func (a *API) GetPublicKeyProofByPubKeyID(id uint32) (*dto.PublicKeyProof, error) { 14 | if !a.cfg.EnableProofMethods { 15 | return nil, APIErrProofMethodsDisabled 16 | } 17 | publicKeyProof, err := a.unsafeGetPublicKeyProofByPubKeyID(id) 18 | if err != nil { 19 | return nil, sanitizeError(err, getPublicKeyProofAPIErrors) 20 | } 21 | return publicKeyProof, nil 22 | } 23 | 24 | func (a *API) unsafeGetPublicKeyProofByPubKeyID(id uint32) (*dto.PublicKeyProof, error) { 25 | leaf, err := a.storage.AccountTree.Leaf(id) 26 | if err != nil { 27 | return nil, errors.WithStack(err) 28 | } 29 | 30 | witness, err := a.storage.AccountTree.GetWitness(id) 31 | if err != nil { 32 | return nil, errors.WithStack(err) 33 | } 34 | 35 | return &dto.PublicKeyProof{ 36 | PublicKey: &leaf.PublicKey, 37 | Witness: witness, 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /docs/deposits.md: -------------------------------------------------------------------------------- 1 | # Deposits 2 | 3 | ## Flow 4 | 5 | 1. User calls `DepositManager.depositFor` method. 6 | 1. ERC20 tokens are transferred to the `Vault` contract. 7 | 2. A `DepositQueued` event is emitted including `pubkeyID`, `tokenID` and `l2Amount`. 8 | 3. A new `UserState` is created, encoded and hashed. The leaf hash is stored in a **baby tree**. 9 | 2. Other users call the `DepositManager.depositFor` method. 10 | 1. Once the baby tree is full the root hash is added to a FIFO queue for submission in a batch. 11 | 2. A `DepositSubTreeReady` event is emitted including `subtreeID` and `subtreeRoot`. 12 | 3. In the meantime commander picks up individual `DepositQueued` events and adds them to **Deposits** table. Once commander 13 | notices `DepositSubTreeReady` it gathers corresponding deposits and stores a record in **Pending Deposit Subtrees** table. 14 | 4. Rollup loop reads from Pending Deposit Subtrees table and submits deposit batches on chain. After a deposit subtree is submitted it is 15 | removed from Pending Deposit Subtrees table and a corresponding commitment and pending batch are stored. 16 | 5. Once the submission transaction is mined, the commander syncs it and marks the batch as mined. 17 | -------------------------------------------------------------------------------- /deploy/geth/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: geth 3 | description: Ethereum node 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.0.1" 25 | -------------------------------------------------------------------------------- /commander/executor/tx_queue.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | ) 8 | 9 | type TxQueue struct { 10 | transactions models.GenericTransactionArray 11 | } 12 | 13 | func NewTxQueue(transactions models.GenericTransactionArray) *TxQueue { 14 | sort.Slice(byPriority(transactions)) 15 | return &TxQueue{transactions: transactions} 16 | } 17 | 18 | func (q *TxQueue) PickTxsForCommitment() models.GenericTransactionArray { 19 | // TODO: implement logic to return MM grouped and sorted by spokeID 20 | return q.transactions 21 | } 22 | 23 | func (q *TxQueue) RemoveFromQueue(toRemove models.GenericTransactionArray) { 24 | outputIndex := 0 25 | for i := 0; i < q.transactions.Len(); i++ { 26 | tx := q.transactions.At(i) 27 | if !txExists(toRemove, tx) { 28 | q.transactions.Set(outputIndex, tx) 29 | outputIndex++ 30 | } 31 | } 32 | 33 | q.transactions = q.transactions.Slice(0, outputIndex) 34 | } 35 | 36 | func txExists(txList models.GenericTransactionArray, tx models.GenericTransaction) bool { 37 | for i := 0; i < txList.Len(); i++ { 38 | if txList.At(i).GetBase().Hash == tx.GetBase().Hash { 39 | return true 40 | } 41 | } 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /commander/verify_commitment.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/eth" 5 | "github.com/Worldcoin/hubble-commander/models" 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | "github.com/Worldcoin/hubble-commander/utils" 8 | "github.com/Worldcoin/hubble-commander/utils/merkletree" 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ErrInvalidCommitmentRoot = errors.New("invalid commitment root of batch #0") 14 | 15 | func verifyCommitmentRoot(storage *st.Storage, client *eth.Client) error { 16 | firstBatch, err := client.GetContractBatch(models.NewUint256(0)) 17 | if err != nil { 18 | return err 19 | } 20 | stateRoot, err := storage.StateTree.Root() 21 | if err != nil { 22 | return err 23 | } 24 | 25 | zeroHash := merkletree.GetZeroHash(0) // TODO: Is this level zero for both? 26 | commitmentRoot := utils.HashTwo(utils.HashTwo(*stateRoot, zeroHash), zeroHash) 27 | log.WithFields(log.Fields{ 28 | "chainspec": commitmentRoot.Hex(), 29 | "contract": firstBatch.Hash.Hex(), 30 | }).Info("Verifying genesis commitment root") 31 | if firstBatch.Hash != commitmentRoot { 32 | return ErrInvalidCommitmentRoot 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /deploy/hubble/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | 3 | environment: prod 4 | 5 | image: 6 | repository: ghcr.io/worldcoin/hubble-commander 7 | pullPolicy: IfNotPresent 8 | # Overrides the image tag whose default is the chart appVersion. 9 | tag: "latest" 10 | 11 | imagePullSecrets: [] 12 | 13 | serviceAccount: 14 | # Specifies whether a service account should be created 15 | create: true 16 | 17 | securityContext: 18 | capabilities: 19 | add: 20 | - NET_BIND_SERVICE 21 | drop: 22 | - all 23 | allowPrivilegeEscalation: false 24 | runAsNonRoot: true 25 | runAsUser: 10001 26 | runAsGroup: 10001 27 | 28 | service: 29 | type: ClusterIP 30 | port: 80 31 | 32 | ingress: 33 | hosts: 34 | - hubble.crypto.worldcoin.dev 35 | - production.hubble.worldcoin-distributors.com 36 | 37 | resources: 38 | limits: 39 | cpu: 14 40 | memory: 8Gi 41 | requests: 42 | cpu: 3 43 | memory: 5Gi 44 | 45 | nameOverride: "" 46 | fullnameOverride: "" 47 | 48 | nodeSelector: {} 49 | 50 | tolerations: [] 51 | 52 | affinity: {} 53 | 54 | persistentStorage: 55 | size: 100Gi 56 | volumeID: aws://us-east-1a/vol-041bb6e8b3bc69109 # example: aws://us-east-1c/vol-aabbcc123 57 | 58 | wipeDisk: false 59 | -------------------------------------------------------------------------------- /eth/utils.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/eth/chain" 7 | "github.com/ethereum/go-ethereum" 8 | "github.com/ethereum/go-ethereum/core/types" 9 | ) 10 | 11 | func (c *Client) GetRevertMessage(tx *types.Transaction, txReceipt *types.Receipt) error { 12 | callMsg := ethereum.CallMsg{ 13 | From: c.Blockchain.GetAccount().From, 14 | To: tx.To(), 15 | Gas: tx.Gas(), 16 | GasPrice: tx.GasPrice(), 17 | Value: tx.Value(), 18 | Data: tx.Data(), 19 | } 20 | 21 | _, err := c.Blockchain.GetBackend().CallContract(context.Background(), callMsg, txReceipt.BlockNumber) 22 | return err 23 | } 24 | 25 | func (c *Client) WaitToBeMined(tx *types.Transaction) (*types.Receipt, error) { 26 | return chain.WaitToBeMined(c.Blockchain.GetBackend(), *c.config.TxMineTimeout, tx) 27 | } 28 | 29 | func (c *Client) WaitForMultipleTxs(txs ...types.Transaction) ([]types.Receipt, error) { 30 | return chain.WaitForMultipleTxs(c.Blockchain.GetBackend(), *c.config.TxMineTimeout, txs...) 31 | } 32 | 33 | func (a *AccountManager) WaitToBeMined(tx *types.Transaction) (*types.Receipt, error) { 34 | return chain.WaitToBeMined(a.Blockchain.GetBackend(), a.mineTimeout, tx) 35 | } 36 | -------------------------------------------------------------------------------- /docs/mass_migration.md: -------------------------------------------------------------------------------- 1 | # 🎒 Mass Migrations 2 | 3 | ## Withdrawal flow 4 | 5 | 1. User signs and sends a L2 MM tx with spokeId=WithdrawManager 6 | 2. Commander builds and submits a batch with that tx 7 | 1. Wait for the batch to be finalized 8 | 3. Someone (anyone) calls `WithdrawManager.processWithdrawCommitment` 9 | 4. User (from their ethereum wallet) calls `WithdrawManager.claimTokens` 10 | 1. Q: Does the Worldcoin mobile app have user's ethereum wallet or do we build a dapp so you can make this call from metamask (or other wallets)? 11 | 12 | ```go 13 | struct MassMigrationBody { 14 | bytes32 accountRoot; 15 | uint256[2] signature; 16 | uint256 spokeID; 17 | bytes32 withdrawRoot; // root of a merkle tree made up of withdrawLeaves 18 | uint256 tokenID; 19 | uint256 amount; // total amount of withdrawals without fees 20 | uint256 feeReceiver; 21 | bytes txs; 22 | } 23 | ``` 24 | 25 | The withdrawLeaves are new created UserStates, each with nonce=0 and balances corresponding to the amounts deducted from the "migrated" accounts. 26 | 27 | ```go 28 | struct MassMigration { 29 | uint256 fromIndex; 30 | uint256 amount; 31 | uint256 fee; 32 | } 33 | ``` 34 | 35 | TODO: 36 | 37 | - What is SpokeID? -------------------------------------------------------------------------------- /models/stored/primitive_encoder_test.go: -------------------------------------------------------------------------------- 1 | package stored 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEncodeHash(t *testing.T) { 11 | dataHash := common.BytesToHash([]byte{1, 2, 3, 4, 5}) 12 | 13 | var decodedHash common.Hash 14 | encodedDataHash, _ := EncodeHash(&dataHash) 15 | _ = DecodeHash(encodedDataHash, &decodedHash) 16 | require.Equal(t, dataHash, decodedHash) 17 | } 18 | 19 | func TestEncodeUint32(t *testing.T) { 20 | number := uint32(173) 21 | 22 | var decodedNumber uint32 23 | encodedDataHash := EncodeUint32(number) 24 | _ = DecodeUint32(encodedDataHash, &decodedNumber) 25 | require.Equal(t, number, decodedNumber) 26 | } 27 | 28 | func TestEncodeUint64(t *testing.T) { 29 | value := uint64(123456789) 30 | 31 | encoded := EncodeUint64(value) 32 | 33 | var decoded uint64 34 | err := DecodeUint64(encoded, &decoded) 35 | require.NoError(t, err) 36 | require.Equal(t, value, decoded) 37 | } 38 | 39 | func TestEncodeString(t *testing.T) { 40 | encoded := EncodeString(testMessage) 41 | 42 | var decoded string 43 | err := DecodeString(encoded, &decoded) 44 | require.NoError(t, err) 45 | require.Equal(t, testMessage, decoded) 46 | } 47 | -------------------------------------------------------------------------------- /deploy/hubble/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: hubble-commander 3 | description: Hubble layer 2 coordinator node 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.0.1" 25 | -------------------------------------------------------------------------------- /models/dto/generic_transaction.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/ethereum/go-ethereum/common" 6 | ) 7 | 8 | type TransactionWithBatchDetails struct { 9 | TransactionBase 10 | ToStateID *uint32 `json:",omitempty"` 11 | ToPublicKey *models.PublicKey `json:",omitempty"` 12 | SpokeID *uint32 `json:",omitempty"` 13 | BatchHash *common.Hash 14 | MinedTime *models.Timestamp 15 | } 16 | 17 | func MakeTransactionWithBatchDetails(tx *models.TransactionWithBatchDetails) TransactionWithBatchDetails { 18 | dtoTx := TransactionWithBatchDetails{ 19 | BatchHash: tx.BatchHash, 20 | MinedTime: tx.MinedTime, 21 | } 22 | switch subTx := tx.Transaction.(type) { 23 | case *models.Transfer: 24 | dtoTx.TransactionBase = MakeTransactionBase(&subTx.TransactionBase) 25 | dtoTx.ToStateID = &subTx.ToStateID 26 | case *models.Create2Transfer: 27 | dtoTx.TransactionBase = MakeTransactionBase(&subTx.TransactionBase) 28 | dtoTx.ToStateID = subTx.ToStateID 29 | dtoTx.ToPublicKey = &subTx.ToPublicKey 30 | case *models.MassMigration: 31 | dtoTx.TransactionBase = MakeTransactionBase(&subTx.TransactionBase) 32 | dtoTx.SpokeID = &subTx.SpokeID 33 | } 34 | return dtoTx 35 | } 36 | -------------------------------------------------------------------------------- /utils/any_error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // ErrorThatSupportsAny is an error that wraps AnyErrorSupport instance, created with NewAnyError. 9 | // See the tests for this file for example usages. 10 | type ErrorThatSupportsAny interface { 11 | Unwrap() error // should return pointer to the wrapped AnyErrorSupport struct 12 | 13 | // Left just as an additional check, AnyErrorSupport implements these interfaces 14 | error 15 | Is(other error) bool 16 | } 17 | 18 | type AnyError struct { 19 | errorType string 20 | } 21 | 22 | func NewAnyError(err ErrorThatSupportsAny) (*AnyError, *AnyErrorSupport) { 23 | errorType := reflect.ValueOf(err).Elem().Type().Name() 24 | return &AnyError{errorType}, &AnyErrorSupport{errorType} 25 | } 26 | 27 | func (e *AnyError) Error() string { 28 | return fmt.Sprintf("any %s", e.errorType) 29 | } 30 | 31 | type AnyErrorSupport struct { 32 | errorType string 33 | } 34 | 35 | func (a *AnyErrorSupport) Error() string { 36 | return fmt.Sprintf("error of %s type", a.errorType) 37 | } 38 | 39 | func (a *AnyErrorSupport) Is(other error) bool { 40 | otherError, ok := other.(*AnyError) 41 | if !ok { 42 | return false 43 | } 44 | return a.errorType == otherError.errorType 45 | } 46 | -------------------------------------------------------------------------------- /bls/signature_test.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | var data = []byte("0xdeadbeef") 11 | 12 | type SignatureTestSuite struct { 13 | *require.Assertions 14 | suite.Suite 15 | wallet *Wallet 16 | signature *Signature 17 | } 18 | 19 | func (s *SignatureTestSuite) SetupSuite() { 20 | s.Assertions = require.New(s.T()) 21 | } 22 | 23 | func (s *SignatureTestSuite) SetupTest() { 24 | wallet, err := NewRandomWallet(TestDomain) 25 | s.NoError(err) 26 | s.wallet = wallet 27 | 28 | s.signature, err = wallet.Sign(data) 29 | s.NoError(err) 30 | } 31 | 32 | func (s *SignatureTestSuite) TestVerify() { 33 | isValid, err := s.signature.Verify(data, s.wallet.PublicKey()) 34 | s.NoError(err) 35 | s.True(isValid) 36 | } 37 | 38 | func (s *SignatureTestSuite) TestNewSignatureFromBytes() { 39 | bytes := s.signature.Bytes() 40 | 41 | signatureFromBytes, err := NewSignatureFromBytes(bytes, TestDomain) 42 | s.NoError(err) 43 | 44 | isValid, err := signatureFromBytes.Verify(data, s.wallet.PublicKey()) 45 | s.NoError(err) 46 | s.True(isValid) 47 | } 48 | 49 | func TestSignatureTestSuite(t *testing.T) { 50 | suite.Run(t, new(SignatureTestSuite)) 51 | } 52 | -------------------------------------------------------------------------------- /api/get_transaction.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models/dto" 5 | "github.com/Worldcoin/hubble-commander/storage" 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | var getTransactionAPIErrors = map[error]*APIError{ 10 | storage.AnyNotFoundError: NewAPIError(10000, "transaction not found"), 11 | } 12 | 13 | func (a *API) GetTransaction(hash common.Hash) (*dto.TransactionReceipt, error) { 14 | transaction, err := a.unsafeGetTransaction(hash) 15 | if err != nil { 16 | return nil, sanitizeError(err, getTransactionAPIErrors) 17 | } 18 | 19 | return transaction, nil 20 | } 21 | 22 | func (a *API) unsafeGetTransaction(hash common.Hash) (*dto.TransactionReceipt, error) { 23 | transaction, err := a.storage.GetTransactionWithBatchDetails(hash) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | var transactionBase = transaction.Transaction.GetBase() 29 | 30 | status, err := CalculateTransactionStatus(a.storage, transactionBase, a.storage.GetLatestBlockNumber()) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &dto.TransactionReceipt{ 36 | TransactionWithBatchDetails: dto.MakeTransactionWithBatchDetails(transaction), 37 | Status: *status, 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /models/commitment_slot.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type CommitmentSlot struct { 8 | BatchID Uint256 9 | IndexInBatch uint8 10 | IndexInCommitment uint8 11 | } 12 | 13 | const CommitmentSlotLength = 32 + 1 + 1 14 | 15 | func NewCommitmentSlot(commitmentID CommitmentID, indexInCommitment uint8) *CommitmentSlot { 16 | return &CommitmentSlot{ 17 | BatchID: commitmentID.BatchID, 18 | IndexInBatch: commitmentID.IndexInBatch, 19 | IndexInCommitment: indexInCommitment, 20 | } 21 | } 22 | 23 | func (k *CommitmentSlot) CommitmentID() *CommitmentID { 24 | return &CommitmentID{ 25 | BatchID: k.BatchID, 26 | IndexInBatch: k.IndexInBatch, 27 | } 28 | } 29 | 30 | func (k *CommitmentSlot) Bytes() []byte { 31 | var buf bytes.Buffer 32 | buf.Grow(CommitmentSlotLength) 33 | 34 | buf.Write(k.BatchID.Bytes()) 35 | buf.WriteByte(k.IndexInBatch) 36 | buf.WriteByte(k.IndexInCommitment) 37 | 38 | return buf.Bytes() 39 | } 40 | 41 | func (k *CommitmentSlot) SetBytes(data []byte) error { 42 | if len(data) != CommitmentSlotLength { 43 | return ErrInvalidLength 44 | } 45 | 46 | k.BatchID.SetBytes(data[0:32]) 47 | k.IndexInBatch = data[32] 48 | k.IndexInCommitment = data[33] 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /docs/naming.md: -------------------------------------------------------------------------------- 1 | # Naming 2 | 3 | ## Tests 4 | 5 | We use two styles of writing tests: **individual tests** and **test suites** using `testify` library. 6 | 7 | ### Individual tests 8 | 9 | Standalone functions: `func TestFunctionName_OptionalDescription(t *testing.T)` 10 | 11 | Methods on structs: `func TestStructName_MethodName_OptionalDescription(t *testing.T)` 12 | 13 | ### Test suites 14 | 15 | Test suite name is derived from the Go file name or struct name. Therefore we don't repeat the struct name in method test names. 16 | 17 | Standalone functions: 18 | 19 | `func (s *FileNameTestSuite) TestFunctionName_OptionalDescription()` 20 | 21 | Methods on structs: 22 | 23 | `func (s *FileNameTestSuite) TestMethodName_OptionalDescription()` 24 | 25 | ## Fields 26 | 27 | There are different names used for the same things across commander and contracts codebases. We will unify naming as part of a future PR choosing the names in green. 28 | 29 | Account Tree 30 | 31 | - **Value:** Public Key 32 | - **Key:** Account Index = **PubKey ID** = Account ID 33 | 34 | State Tree 35 | 36 | - **Value:** User State (PubKey ID, token id, nonce, balance) 37 | - **Key:** State Index = **State ID** = Leaf Merkle Path 38 | 39 | Transaction: 40 | 41 | - **From** and **to** fields are state IDs -------------------------------------------------------------------------------- /commander/workers.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime/debug" 7 | "sync" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type workers struct { 13 | workersContext context.Context 14 | stopWorkersContext context.CancelFunc 15 | workersWaitGroup sync.WaitGroup 16 | } 17 | 18 | func makeWorkers() workers { 19 | ctx, ctxCancel := context.WithCancel(context.Background()) 20 | return workers{ 21 | workersContext: ctx, 22 | stopWorkersContext: ctxCancel, 23 | } 24 | } 25 | 26 | func (w *workers) startWorker(name string, fn func() error) { 27 | w.workersWaitGroup.Add(1) 28 | go func() { 29 | var err error 30 | defer func() { 31 | if recoverErr := recover(); recoverErr != nil { 32 | log.Errorf("stacktrace from worker panic: %s", debug.Stack()) 33 | 34 | var ok bool 35 | err, ok = recoverErr.(error) 36 | if !ok { 37 | err = fmt.Errorf("%+v", recoverErr) 38 | } 39 | } 40 | if err != nil { 41 | log.Errorf("%s worker failed with: %+v", name, err) 42 | w.stopWorkersContext() 43 | } 44 | w.workersWaitGroup.Done() 45 | }() 46 | 47 | err = fn() 48 | }() 49 | } 50 | 51 | func (w *workers) stopWorkersAndWait() { 52 | w.stopWorkersContext() 53 | w.workersWaitGroup.Wait() 54 | } 55 | -------------------------------------------------------------------------------- /api/api_error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var usedErrorCodes = map[int]bool{} 8 | 9 | const unknownAPIErrorCode = 999 10 | 11 | type APIError struct { 12 | Code int 13 | Message string 14 | Data interface{} `json:",omitempty"` 15 | } 16 | 17 | func (e *APIError) Error() string { 18 | if e.Message == "" { 19 | return fmt.Sprintf("error code: %d", e.Code) 20 | } 21 | return e.Message 22 | } 23 | 24 | func (e *APIError) ErrorCode() int { 25 | return e.Code 26 | } 27 | 28 | func (e *APIError) ErrorData() interface{} { 29 | return e.Data 30 | } 31 | 32 | func NewAPIError(code int, message string) *APIError { 33 | if usedErrorCodes[code] { 34 | panic(fmt.Sprintf("%d API error code is already used", code)) 35 | } 36 | 37 | usedErrorCodes[code] = true 38 | 39 | return &APIError{ 40 | Code: code, 41 | Message: message, 42 | } 43 | } 44 | 45 | func NewUnknownAPIError(err error) *APIError { 46 | unknownAPIErrorMessage := fmt.Sprintf("unknown error: %s", err.Error()) 47 | 48 | if usedErrorCodes[unknownAPIErrorCode] { 49 | return &APIError{ 50 | Code: unknownAPIErrorCode, 51 | Message: unknownAPIErrorMessage, 52 | } 53 | } 54 | 55 | return NewAPIError(unknownAPIErrorCode, fmt.Sprintf("unknown error: %s", err.Error())) 56 | } 57 | -------------------------------------------------------------------------------- /models/dto/dispute_proofs.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/ethereum/go-ethereum/common" 6 | ) 7 | 8 | type CommitmentInclusionProofBase struct { 9 | StateRoot common.Hash 10 | Path *MerklePath 11 | Witness models.Witness 12 | } 13 | 14 | type CommitmentInclusionProof struct { 15 | CommitmentInclusionProofBase 16 | Body *CommitmentProofBody 17 | } 18 | 19 | type MassMigrationCommitmentProof struct { 20 | CommitmentInclusionProofBase 21 | Body *MassMigrationBody 22 | } 23 | 24 | type MassMigrationBody struct { 25 | AccountRoot common.Hash 26 | Signature models.Signature 27 | Meta *MassMigrationMeta 28 | WithdrawRoot common.Hash 29 | Transactions []byte 30 | } 31 | 32 | type StateMerkleProof struct { 33 | UserState *UserState 34 | Witness models.Witness 35 | } 36 | 37 | type WithdrawProof struct { 38 | UserState *UserState 39 | Path MerklePath 40 | Witness models.Witness 41 | Root common.Hash 42 | } 43 | 44 | type PublicKeyProof struct { 45 | PublicKey *models.PublicKey 46 | Witness models.Witness 47 | } 48 | 49 | type CommitmentProofBody struct { 50 | AccountRoot common.Hash 51 | Signature models.Signature 52 | FeeReceiver uint32 53 | Transactions interface{} 54 | } 55 | -------------------------------------------------------------------------------- /commander/executor/rollup_loop_context.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/config" 7 | "github.com/Worldcoin/hubble-commander/eth" 8 | "github.com/Worldcoin/hubble-commander/metrics" 9 | "github.com/Worldcoin/hubble-commander/models" 10 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 11 | st "github.com/Worldcoin/hubble-commander/storage" 12 | ) 13 | 14 | type RollupLoopContext interface { 15 | CreateAndSubmitBatch(ctx context.Context) (*models.Batch, *int, error) 16 | ExecutePendingBatch(batch *models.PendingBatch) error 17 | Rollback(cause *error) 18 | Commit() error 19 | } 20 | 21 | func NewRollupLoopContext( 22 | storage *st.Storage, 23 | client *eth.Client, 24 | cfg *config.RollupConfig, 25 | commanderMetrics *metrics.CommanderMetrics, 26 | ctx context.Context, 27 | batchType batchtype.BatchType, 28 | ) RollupLoopContext { 29 | switch batchType { 30 | case batchtype.Transfer, batchtype.Create2Transfer, batchtype.MassMigration: 31 | return NewTxsContext(storage, client, cfg, commanderMetrics, ctx, batchType) 32 | case batchtype.Deposit: 33 | return NewDepositsContext(storage, client, cfg, commanderMetrics, ctx) 34 | case batchtype.Genesis: 35 | panic("invalid batch type") 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /models/enums/batchstatus/batch_status.go: -------------------------------------------------------------------------------- 1 | package batchstatus 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | enumerr "github.com/Worldcoin/hubble-commander/models/enums/errors" 7 | ) 8 | 9 | type BatchStatus uint 10 | 11 | const ( 12 | Submitted BatchStatus = iota + 1 13 | Mined 14 | Finalised // nolint:misspell 15 | ) 16 | 17 | var BatchStatuses = map[BatchStatus]string{ 18 | Submitted: "SUBMITTED", 19 | Mined: "MINED", 20 | Finalised: "FINALISED", // nolint:misspell 21 | } 22 | 23 | func (s BatchStatus) Ref() *BatchStatus { 24 | return &s 25 | } 26 | 27 | func (s BatchStatus) String() string { 28 | msg, exists := BatchStatuses[s] 29 | if !exists { 30 | return "UNKNOWN" 31 | } 32 | return msg 33 | } 34 | 35 | func (s *BatchStatus) UnmarshalJSON(bytes []byte) error { 36 | var strType string 37 | err := json.Unmarshal(bytes, &strType) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | for k, v := range BatchStatuses { 43 | if v == strType { 44 | *s = k 45 | return nil 46 | } 47 | } 48 | return enumerr.NewUnsupportedError("batch status") 49 | } 50 | 51 | func (s BatchStatus) MarshalJSON() ([]byte, error) { 52 | msg, exists := BatchStatuses[s] 53 | if !exists { 54 | return nil, enumerr.NewUnsupportedError("batch status") 55 | } 56 | return json.Marshal(msg) 57 | } 58 | -------------------------------------------------------------------------------- /commander/lifecycle.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // nolint:structcheck 9 | type lifecycle struct { 10 | mutex sync.Mutex // protects Start method and startAndWaitChan 11 | startAndWaitChan chan struct{} 12 | 13 | active uint32 14 | closeOnce sync.Once 15 | } 16 | 17 | func (l *lifecycle) isActive() bool { 18 | return atomic.LoadUint32(&l.active) != 0 19 | } 20 | 21 | func (l *lifecycle) setActive(active bool) { 22 | activeFlag := uint32(0) 23 | if active { 24 | activeFlag = 1 25 | } 26 | atomic.StoreUint32(&l.active, activeFlag) 27 | } 28 | 29 | func (l *lifecycle) getStartAndWaitChan() <-chan struct{} { 30 | l.mutex.Lock() 31 | defer l.mutex.Unlock() 32 | return l.unsafeGetStartAndWaitChan() 33 | } 34 | 35 | func (l *lifecycle) unsafeGetStartAndWaitChan() chan struct{} { 36 | if l.startAndWaitChan == nil { 37 | l.startAndWaitChan = make(chan struct{}) 38 | } 39 | return l.startAndWaitChan 40 | } 41 | 42 | func (l *lifecycle) releaseStartAndWait() { 43 | l.mutex.Lock() 44 | defer l.mutex.Unlock() 45 | ch := l.unsafeGetStartAndWaitChan() 46 | select { 47 | case <-ch: 48 | // Already closed. Don't close again. 49 | default: 50 | // Safe to close here. We're the only closer, guarded by l.mutex. 51 | close(ch) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/rpc/server_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/Worldcoin/hubble-commander/utils/consts" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type Result struct { 16 | // nolint:revive,stylecheck 17 | JsonRpc string `json:"jsonrpc"` 18 | ID string `json:"id"` 19 | Result interface{} `json:"result"` 20 | } 21 | 22 | type API struct{} 23 | 24 | func (a *API) GetAuthKey(ctx context.Context) interface{} { 25 | return ctx.Value(AuthKey) 26 | } 27 | 28 | func TestServer_ServeHTTP(t *testing.T) { 29 | var jsonStr = []byte(`{"jsonrpc": "2.0", "method": "test_getAuthKey", "id": "1"}`) 30 | req, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(jsonStr)) 31 | require.NoError(t, err) 32 | 33 | keyValue := "secret key" 34 | req.Header.Set("Content-Type", "application/json") 35 | req.Header.Set(consts.AuthKeyHeader, keyValue) 36 | 37 | server := NewServer() 38 | err = server.RegisterName("test", &API{}) 39 | require.NoError(t, err) 40 | 41 | w := httptest.NewRecorder() 42 | server.ServeHTTP(w, req) 43 | 44 | actual := &Result{} 45 | err = json.Unmarshal(w.Body.Bytes(), actual) 46 | require.NoError(t, err) 47 | require.Equal(t, keyValue, actual.Result) 48 | } 49 | -------------------------------------------------------------------------------- /storage/registered_spoke.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/pkg/errors" 6 | bh "github.com/timshannon/badgerhold/v4" 7 | ) 8 | 9 | type RegisteredSpokeStorage struct { 10 | database *Database 11 | } 12 | 13 | func NewRegisteredSpokeStorage(database *Database) *RegisteredSpokeStorage { 14 | return &RegisteredSpokeStorage{ 15 | database: database, 16 | } 17 | } 18 | 19 | func (s *RegisteredSpokeStorage) copyWithNewDatabase(database *Database) *RegisteredSpokeStorage { 20 | newRegisteredSpokeStorage := *s 21 | newRegisteredSpokeStorage.database = database 22 | 23 | return &newRegisteredSpokeStorage 24 | } 25 | 26 | func (s *RegisteredSpokeStorage) AddRegisteredSpoke(registeredSpoke *models.RegisteredSpoke) error { 27 | return s.database.Badger.Insert(registeredSpoke.ID, *registeredSpoke) 28 | } 29 | 30 | func (s *RegisteredSpokeStorage) GetRegisteredSpoke(spokeID models.Uint256) (*models.RegisteredSpoke, error) { 31 | var registeredSpoke models.RegisteredSpoke 32 | err := s.database.Badger.Get(spokeID, ®isteredSpoke) 33 | if errors.Is(err, bh.ErrNotFound) { 34 | return nil, errors.WithStack(NewNotFoundError("registered spoke")) 35 | } 36 | if err != nil { 37 | return nil, err 38 | } 39 | registeredSpoke.ID = spokeID 40 | return ®isteredSpoke, nil 41 | } 42 | -------------------------------------------------------------------------------- /storage/registered_token.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/models" 5 | "github.com/pkg/errors" 6 | bh "github.com/timshannon/badgerhold/v4" 7 | ) 8 | 9 | type RegisteredTokenStorage struct { 10 | database *Database 11 | } 12 | 13 | func NewRegisteredTokenStorage(database *Database) *RegisteredTokenStorage { 14 | return &RegisteredTokenStorage{ 15 | database: database, 16 | } 17 | } 18 | 19 | func (s *RegisteredTokenStorage) copyWithNewDatabase(database *Database) *RegisteredTokenStorage { 20 | newRegisteredTokenStorage := *s 21 | newRegisteredTokenStorage.database = database 22 | 23 | return &newRegisteredTokenStorage 24 | } 25 | 26 | func (s *RegisteredTokenStorage) AddRegisteredToken(registeredToken *models.RegisteredToken) error { 27 | return s.database.Badger.Insert(registeredToken.ID, *registeredToken) 28 | } 29 | 30 | func (s *RegisteredTokenStorage) GetRegisteredToken(tokenID models.Uint256) (*models.RegisteredToken, error) { 31 | var registeredToken models.RegisteredToken 32 | err := s.database.Badger.Get(tokenID, ®isteredToken) 33 | if errors.Is(err, bh.ErrNotFound) { 34 | return nil, errors.WithStack(NewNotFoundError("registered token")) 35 | } 36 | if err != nil { 37 | return nil, err 38 | } 39 | registeredToken.ID = tokenID 40 | return ®isteredToken, nil 41 | } 42 | -------------------------------------------------------------------------------- /commander/disputer/dispute_signature_test_suite.go: -------------------------------------------------------------------------------- 1 | package disputer 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/bls" 5 | "github.com/Worldcoin/hubble-commander/eth" 6 | "github.com/Worldcoin/hubble-commander/models" 7 | ) 8 | 9 | // Other test suites encapsulate disputeSignatureTestSuite. 10 | // Don't add any tests on disputeSignatureTestSuite to avoid repeated runs. 11 | type disputeSignatureTestSuite struct { 12 | testSuiteWithContexts 13 | domain *bls.Domain 14 | } 15 | 16 | func (s *disputeSignatureTestSuite) setupTest() { 17 | s.NotNil(s.client) // make sure testSuiteWithContexts.SetupTest was called before 18 | 19 | var err error 20 | s.domain, err = s.client.GetDomain() 21 | s.NoError(err) 22 | } 23 | 24 | func (s *disputeSignatureTestSuite) signTx(wallet *bls.Wallet, tx models.GenericTransaction) { 25 | encodedTx, err := s.syncCtx.Syncer.EncodeTxForSigning(tx) 26 | s.NoError(err) 27 | signature, err := wallet.Sign(encodedTx) 28 | s.NoError(err) 29 | tx.GetBase().Signature = *signature.ModelsSignature() 30 | } 31 | 32 | func (s *disputeSignatureTestSuite) disputeSignature( 33 | batch *eth.DecodedTxBatch, 34 | transfers models.GenericTransactionArray, 35 | ) error { 36 | proofs, err := s.syncCtx.StateMerkleProofs(transfers) 37 | s.NoError(err) 38 | 39 | return s.disputeCtx.DisputeSignature(batch, 0, proofs) 40 | } 41 | -------------------------------------------------------------------------------- /contracts/accountregistry/events.go: -------------------------------------------------------------------------------- 1 | package accountregistry 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum" 5 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 6 | "github.com/ethereum/go-ethereum/core/types" 7 | ) 8 | 9 | type SinglePubKeyRegisteredIterator struct { 10 | AccountRegistrySinglePubkeyRegisteredIterator 11 | } 12 | 13 | func (i *SinglePubKeyRegisteredIterator) SetData( 14 | contract *bind.BoundContract, 15 | event string, 16 | logs chan types.Log, 17 | sub ethereum.Subscription, 18 | ) { 19 | i.AccountRegistrySinglePubkeyRegisteredIterator.contract = contract 20 | i.AccountRegistrySinglePubkeyRegisteredIterator.event = event 21 | i.AccountRegistrySinglePubkeyRegisteredIterator.logs = logs 22 | i.AccountRegistrySinglePubkeyRegisteredIterator.sub = sub 23 | } 24 | 25 | type BatchPubKeyRegisteredIterator struct { 26 | AccountRegistryBatchPubkeyRegisteredIterator 27 | } 28 | 29 | func (i *BatchPubKeyRegisteredIterator) SetData( 30 | contract *bind.BoundContract, 31 | event string, 32 | logs chan types.Log, 33 | sub ethereum.Subscription, 34 | ) { 35 | i.AccountRegistryBatchPubkeyRegisteredIterator.contract = contract 36 | i.AccountRegistryBatchPubkeyRegisteredIterator.event = event 37 | i.AccountRegistryBatchPubkeyRegisteredIterator.logs = logs 38 | i.AccountRegistryBatchPubkeyRegisteredIterator.sub = sub 39 | } 40 | -------------------------------------------------------------------------------- /e2e/setup/commander.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | 8 | "github.com/Worldcoin/hubble-commander/config" 9 | "github.com/Worldcoin/hubble-commander/models" 10 | "github.com/pkg/errors" 11 | "github.com/ybbus/jsonrpc/v2" 12 | ) 13 | 14 | type Commander interface { 15 | Start() error 16 | Stop() error 17 | Restart() error 18 | Client() jsonrpc.RPCClient 19 | ChainSpec() *models.ChainSpec 20 | } 21 | 22 | func NewConfiguredCommanderFromEnv(commanderConfig *config.Config, deployerConfig *config.DeployerConfig) (Commander, error) { 23 | if commanderConfig != nil { 24 | logRequiredConfig(commanderConfig.Rollup) 25 | } 26 | 27 | switch os.Getenv("HUBBLE_E2E") { 28 | case "docker": 29 | return StartDockerCommander(StartOptions{ 30 | Image: "ghcr.io/worldcoin/hubble-commander:latest", 31 | Prune: true, 32 | DeployContracts: true, 33 | }) 34 | case "local": 35 | return ConnectToLocalCommander(), nil 36 | default: 37 | return DeployAndCreateInProcessCommander(commanderConfig, deployerConfig) 38 | } 39 | } 40 | 41 | func logRequiredConfig(cfg *config.RollupConfig) { 42 | jsonCfg, err := json.Marshal(cfg) 43 | if err != nil { 44 | log.Panicf("%+v", errors.WithStack(err)) 45 | } 46 | log.Printf("Required Rollup config for this test: %s", string(jsonCfg)) 47 | } 48 | -------------------------------------------------------------------------------- /commander/executor/transactions_context_test_suite.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/config" 5 | "github.com/Worldcoin/hubble-commander/models/enums/batchtype" 6 | ) 7 | 8 | type testSuiteWithTxsContext struct { 9 | testSuiteWithExecutionContext 10 | txsCtx *TxsContext 11 | } 12 | 13 | func (s *testSuiteWithTxsContext) SetupTest(batchType batchtype.BatchType) { 14 | s.testSuiteWithExecutionContext.SetupTest() 15 | s.newTestTxsContext(batchType) 16 | } 17 | 18 | func (s *testSuiteWithTxsContext) SetupTestWithConfig(batchType batchtype.BatchType, cfg *config.RollupConfig) { 19 | s.testSuiteWithExecutionContext.SetupTestWithConfig(cfg) 20 | s.newTestTxsContext(batchType) 21 | } 22 | 23 | // AcceptNewConfig testify does not support parameterized test fixtures and propagators are not in 24 | // fashion so when a test changes the RollupConfig it must also redo the relevant parts of setup 25 | func (s *testSuiteWithTxsContext) AcceptNewConfig() { 26 | batchType := s.txsCtx.BatchType 27 | s.executionCtx = NewTestExecutionContext(s.storage.Storage, s.client.Client, s.cfg) 28 | s.newTestTxsContext(batchType) 29 | } 30 | 31 | func (s *testSuiteWithTxsContext) newTestTxsContext(batchType batchtype.BatchType) { 32 | var err error 33 | s.txsCtx, err = NewTestTxsContext(s.executionCtx, batchType) 34 | s.NoError(err) 35 | } 36 | -------------------------------------------------------------------------------- /models/enums/batchtype/batch_type.go: -------------------------------------------------------------------------------- 1 | package batchtype 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | enumerr "github.com/Worldcoin/hubble-commander/models/enums/errors" 7 | ) 8 | 9 | type BatchType uint8 10 | 11 | const ( 12 | Genesis BatchType = iota 13 | Transfer 14 | MassMigration 15 | Create2Transfer 16 | Deposit 17 | ) 18 | 19 | var BatchTypes = map[BatchType]string{ 20 | Genesis: "GENESIS", 21 | Transfer: "TRANSFER", 22 | MassMigration: "MASS_MIGRATION", 23 | Create2Transfer: "CREATE2TRANSFER", 24 | Deposit: "DEPOSIT", 25 | } 26 | 27 | func (s BatchType) Ref() *BatchType { 28 | return &s 29 | } 30 | 31 | func (s BatchType) String() string { 32 | msg, exists := BatchTypes[s] 33 | if !exists { 34 | return "UNKNOWN" 35 | } 36 | return msg 37 | } 38 | 39 | func (s *BatchType) UnmarshalJSON(bytes []byte) error { 40 | var strType string 41 | err := json.Unmarshal(bytes, &strType) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | for k, v := range BatchTypes { 47 | if v == strType { 48 | *s = k 49 | return nil 50 | } 51 | } 52 | return enumerr.NewUnsupportedError("batch type") 53 | } 54 | 55 | func (s BatchType) MarshalJSON() ([]byte, error) { 56 | msg, exists := BatchTypes[s] 57 | if !exists { 58 | return nil, enumerr.NewUnsupportedError("batch type") 59 | } 60 | return json.Marshal(msg) 61 | } 62 | -------------------------------------------------------------------------------- /encoder/encoder.go: -------------------------------------------------------------------------------- 1 | package encoder 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/Worldcoin/hubble-commander/contracts/frontend/generic" 7 | "github.com/Worldcoin/hubble-commander/models" 8 | "github.com/ethereum/go-ethereum/accounts/abi" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/crypto" 11 | ) 12 | 13 | var tUint256, _ = abi.NewType("uint256", "", nil) 14 | 15 | func HashUserState(state *models.UserState) (*common.Hash, error) { 16 | encodedState, err := EncodeUserState(toContractUserState(state)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | dataHash := crypto.Keccak256Hash(encodedState) 21 | return &dataHash, nil 22 | } 23 | 24 | func toContractUserState(state *models.UserState) generic.TypesUserState { 25 | return generic.TypesUserState{ 26 | PubkeyID: big.NewInt(int64(state.PubKeyID)), 27 | TokenID: state.TokenID.ToBig(), 28 | Balance: state.Balance.ToBig(), 29 | Nonce: state.Nonce.ToBig(), 30 | } 31 | } 32 | 33 | func EncodeUserState(state generic.TypesUserState) ([]byte, error) { 34 | arguments := abi.Arguments{ 35 | {Name: "pubkeyID", Type: tUint256}, 36 | {Name: "tokenID", Type: tUint256}, 37 | {Name: "balance", Type: tUint256}, 38 | {Name: "nonce", Type: tUint256}, 39 | } 40 | return arguments.Pack( 41 | state.PubkeyID, 42 | state.TokenID, 43 | state.Balance, 44 | state.Nonce, 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/Worldcoin/hubble-commander/config" 11 | "github.com/Worldcoin/hubble-commander/eth" 12 | "github.com/Worldcoin/hubble-commander/metrics" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | type Result struct { 17 | // nolint:revive,stylecheck 18 | JsonRpc string `json:"jsonrpc"` 19 | ID string `json:"id"` 20 | Result string `json:"result"` 21 | } 22 | 23 | func TestStartApiServer(t *testing.T) { 24 | var jsonStr = []byte(`{"jsonrpc": "2.0", "method": "hubble_getVersion", "id": "1"}`) 25 | req, err := http.NewRequest("POST", "", bytes.NewBuffer(jsonStr)) 26 | require.NoError(t, err) 27 | req.Header.Set("Content-Type", "application/json") 28 | 29 | cfg := config.APIConfig{Version: "v0123"} 30 | commanderMetrics := metrics.NewCommanderMetrics() 31 | server, err := getAPIServer( 32 | &cfg, 33 | nil, 34 | eth.DomainOnlyTestClient, 35 | commanderMetrics, 36 | false, 37 | func(enable bool) {}, 38 | func() bool { return false }, 39 | ) 40 | require.NoError(t, err) 41 | 42 | w := httptest.NewRecorder() 43 | server.ServeHTTP(w, req) 44 | 45 | actual := &Result{} 46 | err = json.Unmarshal(w.Body.Bytes(), actual) 47 | require.NoError(t, err) 48 | 49 | require.Equal(t, &Result{"2.0", "1", "v0123"}, actual) 50 | } 51 | -------------------------------------------------------------------------------- /commander/applier/apply_transfer.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | st "github.com/Worldcoin/hubble-commander/storage" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ErrNonexistentReceiver = fmt.Errorf("receiver state ID does not exist") 12 | 13 | func (a *Applier) ApplyTransfer(tx models.GenericTransaction, commitmentTokenID models.Uint256) ( 14 | applyResult ApplySingleTxResult, txError, appError error, 15 | ) { 16 | receiverLeaf, appError := a.storage.StateTree.Leaf(*tx.GetToStateID()) 17 | if st.IsNotFoundError(appError) { 18 | txError = errors.WithStack(ErrNonexistentReceiver) 19 | return nil, txError, nil 20 | } 21 | if appError != nil { 22 | return nil, nil, appError 23 | } 24 | 25 | txError, appError = a.ApplyTx(tx, receiverLeaf, commitmentTokenID) 26 | if txError != nil || appError != nil { 27 | return nil, txError, appError 28 | } 29 | return &ApplySingleTransferResult{tx: tx.ToTransfer()}, nil, nil 30 | } 31 | 32 | func (a *Applier) ApplyTransferForSync(tx models.GenericTransaction, commitmentTokenID models.Uint256) ( 33 | synced *SyncedTxWithProofs, 34 | txError, appError error, 35 | ) { 36 | receiverLeaf, err := a.storage.StateTree.LeafOrEmpty(*tx.GetToStateID()) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | return a.applyTxForSync(tx, receiverLeaf, commitmentTokenID) 42 | } 43 | -------------------------------------------------------------------------------- /commander/executor/execution_context_test_suite.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "github.com/Worldcoin/hubble-commander/config" 5 | "github.com/Worldcoin/hubble-commander/eth" 6 | st "github.com/Worldcoin/hubble-commander/storage" 7 | "github.com/stretchr/testify/require" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type testSuiteWithExecutionContext struct { 12 | *require.Assertions 13 | suite.Suite 14 | storage *st.TestStorage 15 | cfg *config.RollupConfig 16 | client *eth.TestClient 17 | executionCtx *ExecutionContext 18 | } 19 | 20 | func (s *testSuiteWithExecutionContext) SetupSuite() { 21 | s.Assertions = require.New(s.T()) 22 | } 23 | 24 | func (s *testSuiteWithExecutionContext) SetupTest() { 25 | s.SetupTestWithConfig(&config.RollupConfig{ 26 | MinCommitmentsPerBatch: 1, 27 | MaxCommitmentsPerBatch: 32, 28 | }) 29 | } 30 | 31 | func (s *testSuiteWithExecutionContext) SetupTestWithConfig(cfg *config.RollupConfig) { 32 | var err error 33 | s.storage, err = st.NewTestStorage() 34 | s.NoError(err) 35 | 36 | s.cfg = cfg 37 | 38 | s.client, err = eth.NewTestClient() 39 | s.NoError(err) 40 | 41 | s.executionCtx = NewTestExecutionContext(s.storage.Storage, s.client.Client, s.cfg) 42 | } 43 | 44 | func (s *testSuiteWithExecutionContext) TearDownTest() { 45 | s.client.Close() 46 | err := s.storage.Teardown() 47 | s.NoError(err) 48 | } 49 | -------------------------------------------------------------------------------- /api/admin/mempool.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/dto" 7 | ) 8 | 9 | func (a *API) RecomputePendingState(ctx context.Context, stateID uint32, mutate bool) (*dto.RecomputePendingState, error) { 10 | err := a.verifyAuthKey(ctx) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return a.storage.RecomputePendingState(stateID, mutate) 16 | } 17 | 18 | func (a *API) GetPendingStates(ctx context.Context, startStateID, pageSize uint32) ([]dto.UserStateWithID, error) { 19 | err := a.verifyAuthKey(ctx) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return a.storage.GetPendingStates(startStateID, pageSize) 25 | } 26 | 27 | // reads the pending balances from badger 28 | func (a *API) GetPendingPubkeyBalances(ctx context.Context, startPrefix []byte, pageSize uint32) ([]dto.PubkeyBalance, error) { 29 | err := a.verifyAuthKey(ctx) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return a.storage.GetPendingPubkeyBalances(startPrefix, pageSize) 35 | } 36 | 37 | // scans the mempool to recompute the pending balances 38 | func (a *API) RecomputePubkeyBalances(ctx context.Context, startPrefix []byte, pageSize uint32) ([]dto.PubkeyBalance, error) { 39 | err := a.verifyAuthKey(ctx) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return a.storage.RecomputePendingPubkeyBalances(startPrefix, pageSize) 45 | } 46 | -------------------------------------------------------------------------------- /api/get_user_state.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Worldcoin/hubble-commander/models/dto" 7 | "github.com/Worldcoin/hubble-commander/o11y" 8 | "github.com/Worldcoin/hubble-commander/storage" 9 | log "github.com/sirupsen/logrus" 10 | "go.opentelemetry.io/otel/attribute" 11 | "go.opentelemetry.io/otel/trace" 12 | ) 13 | 14 | var getUserStateAPIErrors = map[error]*APIError{ 15 | storage.AnyNotFoundError: NewAPIError(99002, "user state not found"), 16 | } 17 | 18 | func (a *API) GetUserState(ctx context.Context, id uint32) (*dto.UserStateWithID, error) { 19 | span := trace.SpanFromContext(ctx) 20 | span.SetAttributes(attribute.Int64("hubble.stateID", int64(id))) 21 | 22 | log.WithFields(o11y.TraceFields(ctx)).Infof("Getting state for id: %d", id) 23 | 24 | userState, err := a.unsafeGetUserState(id) 25 | if err != nil { 26 | // WithFields allows APM to associate these lines with the trace 27 | log.WithFields(o11y.TraceFields(ctx)).Errorf("Error getting user state: %v", err) 28 | 29 | return nil, sanitizeError(err, getUserStateAPIErrors) 30 | } 31 | 32 | return userState, nil 33 | } 34 | 35 | func (a *API) unsafeGetUserState(id uint32) (*dto.UserStateWithID, error) { 36 | userState, err := a.storage.GetPendingUserState(id) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | addressibleValue := dto.MakeUserStateWithID(id, userState) 42 | return &addressibleValue, nil 43 | } 44 | -------------------------------------------------------------------------------- /commander-config.example.yaml: -------------------------------------------------------------------------------- 1 | #log: 2 | # level: info 3 | # format: text 4 | # 5 | #bootstrap: 6 | # prune: false 7 | # migrate: false 8 | # node_url: http://localhost:8080 # set this url to bootstrap from a remote node 9 | # chain_spec_path: chain-spec.yaml # set this path to bootstrap from a chain spec file 10 | # 11 | #rollup: 12 | # sync_size: 50 13 | # fee_receiver_pub_key_id: 0 14 | # min_txs_per_commitment: 1 15 | # max_txs_per_commitment: 32 16 | # min_commitments_per_batch: 1 17 | # max_commitments_per_batch: 32 18 | # transfer_batch_submission_gas_limit: 400000 19 | # c2t_batch_submission_gas_limit: 500000 20 | # mm_batch_submission_gas_limit: 550000 21 | # deposit_batch_submission_gas_limit: 220000 22 | # transition_dispute_gas_limit: 4100000 23 | # signature_dispute_gas_limit: 7600000 24 | # batch_account_registration_gas_limit: 8000000 25 | # stake_withdrawal_gas_limit: 200000 26 | # batch_loop_interval: 500ms 27 | # max_txn_delay: 30min 28 | # 29 | #api: 30 | # port: 8080 31 | # enable_proof_methods: false 32 | # authentication_key: secret_authentication_key # required authentication key for admin api 33 | # 34 | #metrics: 35 | # port: 2112 36 | # endpoint: /metrics 37 | # 38 | #badger: 39 | # path: db/data/hubble 40 | # 41 | #ethereum: 42 | # rpc_url: ws://localhost:8546 43 | # chain_id: 1337 44 | # private_key: ee79b5f6e221356af78cf4c36f4f7885a11b67dfcc81c34d80249947330c0f82 45 | # mine_timeout: 5m 46 | -------------------------------------------------------------------------------- /config/deploy_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Worldcoin/hubble-commander/models" 7 | "github.com/Worldcoin/hubble-commander/utils" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const DefaultBlocksToFinalise = uint32(1 * 24 * 60 * 4) // there are 4 eth blocks / min 12 | 13 | func GetDeployerConfig() *DeployerConfig { 14 | setupViper("deployer-config") 15 | 16 | return &DeployerConfig{ 17 | Bootstrap: &DeployerBootstrapConfig{ 18 | BlocksToFinalise: getUint32("bootstrap.blocks_to_finalise", DefaultBlocksToFinalise), // nolint:misspell 19 | GenesisAccounts: getGenesisAccounts(), 20 | Chooser: getAddressOrNil("chooser_address"), 21 | }, 22 | Ethereum: getEthereumConfig(), 23 | } 24 | } 25 | 26 | func GetDeployerTestConfig() *DeployerConfig { 27 | err := os.Chdir(utils.GetProjectRoot()) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | return GetDeployerConfig() 33 | } 34 | 35 | func getGenesisAccounts() []models.GenesisAccount { 36 | filename := getStringOrPanic("bootstrap.genesis_path") 37 | log.Printf("Reading genesis config from %s", filename) 38 | return readGenesisAccounts(filename) 39 | } 40 | 41 | func readGenesisAccounts(filename string) []models.GenesisAccount { 42 | genesisAccounts, err := readGenesisFile(filename) 43 | if err != nil { 44 | log.Panicf("error reading genesis file: %s", err.Error()) 45 | } 46 | return genesisAccounts 47 | } 48 | --------------------------------------------------------------------------------