diff --git a/.github/workflows/build-xmtpd.yml b/.github/workflows/build-xmtpd.yml index 2dacb172..845efe91 100644 --- a/.github/workflows/build-xmtpd.yml +++ b/.github/workflows/build-xmtpd.yml @@ -43,13 +43,23 @@ jobs: type=ref,event=tag type=ref,event=pr type=sha - + - name: Set up Docker image file based on the matrix variable + id: set_dockerfile + run: | + if [[ "${{ matrix.image }}" == "xmtpd" ]]; then + echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.image }}" == "xmtpd-cli" ]]; then + echo "dockerfile=Dockerfile-cli" >> $GITHUB_OUTPUT + else + echo "Unknown image: ${{ matrix.image }}" + exit 1 + fi - name: Build and push Docker image uses: docker/build-push-action@v6 id: push with: context: . - file: ./dev/docker/Dockerfile + file: ./dev/docker/${{ steps.set_dockerfile.outputs.dockerfile }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7960193c..3337b154 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,11 @@ on: push: branches: - main + paths-ignore: + - "contracts/**" pull_request: + paths-ignore: + - "contracts/**" permissions: contents: read jobs: @@ -28,18 +32,3 @@ jobs: uses: nickcharlton/diff-check@main with: command: dev/lint-golines - contracts: - name: Lint (Contracts) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: "nightly-ac81a53d1d5823919ffbadd3c65f081927aa11f2" - - run: forge --version - - name: Run Forge fmt - # only format code, we do not want to format LIB - run: forge fmt contracts/src --check diff --git a/.github/workflows/release-from-tag.yml b/.github/workflows/release-from-tag.yml index 98d0b586..cdc17526 100644 --- a/.github/workflows/release-from-tag.yml +++ b/.github/workflows/release-from-tag.yml @@ -8,6 +8,9 @@ on: jobs: push_to_registry: name: Push Docker Image to GitHub Packages + strategy: + matrix: + image: [ "xmtpd", "xmtpd-cli" ] runs-on: ubuntu-latest permissions: contents: read @@ -23,12 +26,22 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Set up Docker image file based on the matrix variable + id: set_dockerfile + run: | + if [[ "${{ matrix.image }}" == "xmtpd" ]]; then + echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.image }}" == "xmtpd-cli" ]]; then + echo "dockerfile=Dockerfile-cli" >> $GITHUB_OUTPUT + else + echo "Unknown image: ${{ matrix.image }}" + exit 1 + fi - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/xmtp/xmtpd + images: ghcr.io/xmtp/${{ matrix.image }} tags: | type=ref,event=tag type=semver,pattern={{version}} @@ -38,7 +51,7 @@ jobs: id: push with: context: . - file: ./dev/docker/Dockerfile + file: ./dev/docker/${{ steps.set_dockerfile.outputs.dockerfile }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/solidity.yml b/.github/workflows/solidity.yml new file mode 100644 index 00000000..cd345b9b --- /dev/null +++ b/.github/workflows/solidity.yml @@ -0,0 +1,124 @@ +name: CI Solidity + +on: + push: + branches: + - main + paths: + - "contracts/**" + pull_request: + paths: + - "contracts/**" + +concurrency: + group: ci-solidity-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + working-directory: contracts + +jobs: + init: + runs-on: ubuntu-latest + strategy: + fail-fast: true + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install dependencies + run: forge soldeer update + + - name: Build contracts + run: forge build + + - name: Cache data + uses: actions/cache@v4 + with: + path: contracts + key: ci-solidity-${{ github.ref }} + + - id: forge + run: echo "FORGE_PATH=$(which forge)" >> $GITHUB_OUTPUT + + - name: Upload forge + uses: actions/upload-artifact@v4 + with: + name: forge + path: ${{ steps.forge.outputs.FORGE_PATH }} + + test: + needs: init + runs-on: ubuntu-latest + steps: + - name: Restore cache + uses: actions/cache@v4 + with: + path: contracts + key: ci-solidity-${{ github.ref }} + + - name: Restore forge + uses: actions/download-artifact@v4 + with: + name: forge + path: /usr/local/bin + + - run: chmod +x /usr/local/bin/forge + + - name: Run Forge tests + run: forge test -vvv + + lint: + needs: init + runs-on: ubuntu-latest + steps: + - name: Restore cache + uses: actions/cache@v4 + with: + path: contracts + key: ci-solidity-${{ github.ref }} + + - name: Restore forge + uses: actions/download-artifact@v4 + with: + name: forge + path: /usr/local/bin + + - run: chmod +x /usr/local/bin/forge + + - name: Run Forge fmt + run: forge fmt contracts/src --check + + slither: + needs: init + runs-on: ubuntu-latest + steps: + - name: Restore cache + uses: actions/cache@v4 + with: + path: contracts + key: ci-solidity-${{ github.ref }} + + - name: Restore forge + uses: actions/download-artifact@v4 + with: + name: forge + path: /usr/local/bin + + - run: chmod +x /usr/local/bin/forge + + - name: Install Slither + run: pip3 install slither-analyzer + + - name: Run Slither + run: slither . --sarif output.sarif + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: contracts/output.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0a9168f1..63105688 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,11 @@ on: push: branches: - main + paths-ignore: + - "contracts/**" pull_request: + paths-ignore: + - "contracts/**" jobs: test: name: Test (Node) @@ -38,29 +42,3 @@ jobs: service: xmtp-node-go files: report.xml env: ci - contracts: - name: Test (Contracts) - strategy: - fail-fast: true - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: "nightly-ac81a53d1d5823919ffbadd3c65f081927aa11f2" - - - name: Run Forge build - working-directory: contracts - run: | - forge --version - forge build --sizes - - - name: Run Forge tests - working-directory: contracts - run: | - forge test -vvv - id: test diff --git a/cmd/cli/cli_test.go b/cmd/cli/cli_test.go index 2a32761f..088ab853 100644 --- a/cmd/cli/cli_test.go +++ b/cmd/cli/cli_test.go @@ -23,9 +23,9 @@ func TestRegisterNodeArgParse(t *testing.T) { httpAddress, "--admin-private-key", adminPrivateKey, - "--owner-address", + "--node-owner-address", ownerAddress.Hex(), - "--signing-key-pub", + "--node-signing-key-pub", signingKeyPub, }, ) @@ -41,7 +41,7 @@ func TestRegisterNodeArgParse(t *testing.T) { require.Equal( t, err.Error(), - "Could not parse options: the required flags `--admin-private-key', `--http-address', `--owner-address' and `--signing-key-pub' were not specified", + "Could not parse options: the required flags `--admin-private-key', `--http-address', `--node-owner-address' and `--node-signing-key-pub' were not specified", ) } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index adf7e489..25e881c3 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -8,22 +8,18 @@ import ( "log" "os" + "github.com/xmtp/xmtpd/pkg/config" + "github.com/jessevdk/go-flags" "github.com/xmtp/xmtpd/pkg/blockchain" - "github.com/xmtp/xmtpd/pkg/config" "github.com/xmtp/xmtpd/pkg/utils" "go.uber.org/zap" ) var Commit string = "unknown" -type globalOptions struct { - Contracts config.ContractsOptions `group:"Contracts Options" namespace:"contracts"` - Log config.LogOptions `group:"Log Options" namespace:"log"` -} - type CLI struct { - globalOptions + config.GlobalOptions Command string GetPubKey config.GetPubKeyOptions GenerateKey config.GenerateKeyOptions @@ -43,7 +39,7 @@ the options for each subcommand. * */ func parseOptions(args []string) (*CLI, error) { - var options globalOptions + var options config.GlobalOptions var generateKeyOptions config.GenerateKeyOptions var registerNodeOptions config.RegisterNodeOptions var getPubKeyOptions config.GetPubKeyOptions @@ -150,7 +146,7 @@ func registerNode(logger *zap.Logger, options *CLI) { } logger.Info( "successfully added node", - zap.String("owner-address", options.RegisterNode.OwnerAddress), + zap.String("node-owner-address", options.RegisterNode.OwnerAddress), zap.String("node-http-address", options.RegisterNode.HttpAddress), zap.String("node-signing-key-pub", utils.EcdsaPublicKeyToString(signingKeyPub)), ) @@ -308,5 +304,4 @@ func main() { updateAddress(logger, options) return } - } diff --git a/cmd/replication/main.go b/cmd/replication/main.go index 40de3557..685a24d0 100644 --- a/cmd/replication/main.go +++ b/cmd/replication/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "database/sql" "fmt" "log" "sync" @@ -24,16 +25,21 @@ var options config.ServerOptions func main() { _, err := flags.Parse(&options) + if err != nil { + if err, ok := err.(*flags.Error); !ok || err.Type != flags.ErrHelp { + fatal("Could not parse options: %s", err) + } + return + } + if options.Version { fmt.Printf("Version: %s\n", Commit) return } + err = config.ValidateServerOptions(options) if err != nil { - if err, ok := err.(*flags.Error); !ok || err.Type != flags.ErrHelp { - fatal("Could not parse options: %s", err) - } - return + fatal("Could not validate options: %s", err) } logger, _, err := utils.BuildLogger(options.Log) @@ -57,16 +63,19 @@ func main() { var wg sync.WaitGroup doneC := make(chan bool, 1) tracing.GoPanicWrap(ctx, &wg, "main", func(ctx context.Context) { - db, err := db.NewNamespacedDB( - ctx, - options.DB.WriterConnectionString, - utils.BuildNamespace(options), - options.DB.WaitForDB, - options.DB.ReadTimeout, - ) - - if err != nil { - logger.Fatal("initializing database", zap.Error(err)) + var dbInstance *sql.DB + if options.Replication.Enable || options.Sync.Enable || options.Indexer.Enable { + dbInstance, err = db.NewNamespacedDB( + ctx, + options.DB.WriterConnectionString, + utils.BuildNamespace(options), + options.DB.WaitForDB, + options.DB.ReadTimeout, + ) + + if err != nil { + logger.Fatal("initializing database", zap.Error(err)) + } } ethclient, err := blockchain.NewClient(ctx, options.Contracts.RpcUrl) @@ -111,7 +120,7 @@ func main() { logger, options, chainRegistry, - db, + dbInstance, blockchainPublisher, fmt.Sprintf("0.0.0.0:%d", options.API.Port), ) diff --git a/contracts/.gitignore b/contracts/.gitignore index 85198aaa..ebe566f2 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -12,3 +12,8 @@ docs/ # Dotenv file .env + +# Soldeer +/dependencies + +.vscode/ diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 59addb91..66cb3155 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -1,8 +1,16 @@ +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options [profile.default] +auto_detect_solc = true src = "src" out = "out" -libs = ["lib"] +libs = ["dependencies"] +gas_reports = ["*"] +optimizer = true +optimizer_runs = 10_000 -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +[soldeer] +recursive_deps = true -solc = "0.8.28" +[dependencies] +forge-std = "1.9.4" +"@openzeppelin-contracts" = "5.1.0" diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std deleted file mode 160000 index 035de35f..00000000 --- a/contracts/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 035de35f5e366c8d6ed142aec4ccb57fe2dd87d4 diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts deleted file mode 160000 index 8b591bae..00000000 --- a/contracts/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8b591baef460523e5ca1c53712c464bcc1a1c467 diff --git a/contracts/remappings.txt b/contracts/remappings.txt index eede3c19..8b4a64d6 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1 +1,2 @@ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file +@openzeppelin-contracts-5.1.0/=dependencies/@openzeppelin-contracts-5.1.0/ +forge-std-1.9.4/=dependencies/forge-std-1.9.4/ diff --git a/contracts/script/Deployer.s.sol b/contracts/script/Deployer.s.sol index 2742eeb0..e16949d3 100644 --- a/contracts/script/Deployer.s.sol +++ b/contracts/script/Deployer.s.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.28; -import {Script, console} from "forge-std/Script.sol"; +import {Script, console} from "forge-std-1.9.4/src/Script.sol"; import "../src/Nodes.sol"; contract Deployer is Script { diff --git a/contracts/slither.config.json b/contracts/slither.config.json new file mode 100644 index 00000000..1df96896 --- /dev/null +++ b/contracts/slither.config.json @@ -0,0 +1,4 @@ +{ + "detectors_to_exclude": "", + "filter_paths": "dependencies" +} diff --git a/contracts/soldeer.lock b/contracts/soldeer.lock new file mode 100644 index 00000000..ecca7c99 --- /dev/null +++ b/contracts/soldeer.lock @@ -0,0 +1,13 @@ +[[dependencies]] +name = "@openzeppelin-contracts" +version = "5.1.0" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_1_0_19-10-2024_10:28:52_contracts.zip" +checksum = "fd3d1ea561cb27897008aee18ada6e85f248eb161c86e4435272fc2b5777574f" +integrity = "cb6cf6e878f2943b2291d5636a9d72ac51d43d8135896ceb6cf88d36c386f212" + +[[dependencies]] +name = "forge-std" +version = "1.9.4" +url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_4_25-10-2024_14:36:59_forge-std-1.9.zip" +checksum = "b5be24beb5e4dab5e42221b2ad1288b64c826bee5ee71b6159ba93ffe86f14d4" +integrity = "3874463846ab995a6a9a88412913cacec6144f7605daa1af57c2d8bf3f210b13" diff --git a/contracts/src/Nodes.sol b/contracts/src/Nodes.sol index 68894b73..b7d2d0fb 100644 --- a/contracts/src/Nodes.sol +++ b/contracts/src/Nodes.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin-contracts-5.1.0/token/ERC721/ERC721.sol"; +import "@openzeppelin-contracts-5.1.0/access/Ownable.sol"; /** * A NFT contract for XMTP Node Operators. @@ -14,7 +14,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract Nodes is ERC721, Ownable { constructor() ERC721("XMTP Node Operator", "XMTP") Ownable(msg.sender) {} - uint32 private _nodeIncrement = 100; + uint32 private constant NODE_INCREMENT = 100; // uint32 counter so that we cannot create more than max IDs // The ERC721 standard expects the tokenID to be uint256 for standard methods unfortunately uint32 private _nodeCounter = 0; @@ -46,7 +46,7 @@ contract Nodes is ERC721, Ownable { { // the first node starts with 100 _nodeCounter++; - uint32 nodeId = _nodeCounter * _nodeIncrement; + uint32 nodeId = _nodeCounter * NODE_INCREMENT; _mint(to, nodeId); _nodes[nodeId] = Node(signingKeyPub, httpAddress, true); _emitNodeUpdate(nodeId); @@ -93,7 +93,7 @@ contract Nodes is ERC721, Ownable { // First, count the number of healthy nodes for (uint256 i = 0; i < _nodeCounter; i++) { - uint256 nodeId = _nodeIncrement * (i + 1); + uint256 nodeId = NODE_INCREMENT * (i + 1); if (_nodeExists(nodeId) && _nodes[nodeId].isHealthy) { healthyCount++; } @@ -105,7 +105,7 @@ contract Nodes is ERC721, Ownable { // Populate the array with healthy nodes for (uint32 i = 0; i < _nodeCounter; i++) { - uint32 nodeId = _nodeIncrement * (i + 1); + uint32 nodeId = NODE_INCREMENT * (i + 1); if (_nodeExists(nodeId) && _nodes[nodeId].isHealthy) { healthyNodesList[currentIndex] = NodeWithId({nodeId: nodeId, node: _nodes[nodeId]}); currentIndex++; @@ -122,7 +122,7 @@ contract Nodes is ERC721, Ownable { NodeWithId[] memory allNodesList = new NodeWithId[](_nodeCounter); for (uint32 i = 0; i < _nodeCounter; i++) { - uint32 nodeId = _nodeIncrement * (i + 1); + uint32 nodeId = NODE_INCREMENT * (i + 1); if (_nodeExists(nodeId)) { allNodesList[i] = NodeWithId({nodeId: nodeId, node: _nodes[nodeId]}); } diff --git a/contracts/test/GroupMessage.t.sol b/contracts/test/GroupMessage.t.sol index 6c1eb52b..e1f761cd 100644 --- a/contracts/test/GroupMessage.t.sol +++ b/contracts/test/GroupMessage.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.28; -import {Test, console} from "forge-std/Test.sol"; +import {Test, console} from "forge-std-1.9.4/src/Test.sol"; import {GroupMessages} from "../src/GroupMessages.sol"; contract GroupMessagesTest is Test { diff --git a/contracts/test/IdentityUpdates.t.sol b/contracts/test/IdentityUpdates.t.sol index fb413a93..a4d17fa8 100644 --- a/contracts/test/IdentityUpdates.t.sol +++ b/contracts/test/IdentityUpdates.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.28; -import {Test, console} from "forge-std/Test.sol"; +import {Test, console} from "forge-std-1.9.4/src/Test.sol"; import {IdentityUpdates} from "../src/IdentityUpdates.sol"; contract IdentityUpdatesTest is Test { diff --git a/contracts/test/Nodes.sol b/contracts/test/Nodes.sol index 0d5f1fcb..df4b21a5 100644 --- a/contracts/test/Nodes.sol +++ b/contracts/test/Nodes.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.28; -import {Test, console} from "forge-std/Test.sol"; +import {Test, console} from "forge-std-1.9.4/src/Test.sol"; +import {Ownable} from "@openzeppelin-contracts-5.1.0/access/Ownable.sol"; import {Nodes} from "../src/Nodes.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract NodesTest is Test { Nodes public nodes; diff --git a/dev/cli b/dev/cli index f48ae002..eb766f44 100755 --- a/dev/cli +++ b/dev/cli @@ -9,4 +9,6 @@ SCRIPT_DIR=$(dirname "$(realpath "$0")") TOP_LEVEL_DIR=$(realpath "$SCRIPT_DIR/..") cd "$TOP_LEVEL_DIR" +export XMTPD_LOG_ENCODING=json + go run -ldflags="-X main.Commit=$(git rev-parse HEAD)" cmd/cli/main.go "$@" \ No newline at end of file diff --git a/dev/contracts/deploy-ephemeral b/dev/contracts/deploy-ephemeral index 3f6e2a3c..74f9ce17 100755 --- a/dev/contracts/deploy-ephemeral +++ b/dev/contracts/deploy-ephemeral @@ -5,4 +5,7 @@ source dev/contracts/.env cd ./contracts +# Update depencencies +forge soldeer update + forge create --legacy --json --broadcast --rpc-url $DOCKER_RPC_URL --private-key $PRIVATE_KEY "$1:$2" \ No newline at end of file diff --git a/dev/contracts/deploy-local b/dev/contracts/deploy-local index feae3942..daa455fb 100755 --- a/dev/contracts/deploy-local +++ b/dev/contracts/deploy-local @@ -10,6 +10,9 @@ mkdir -p ./build cd ./contracts +# Update depencencies +forge soldeer update + # Deploy a contract and save the output (which includes the contract address) to a JSON file to be used in tests function deploy_contract() { forge create --broadcast --legacy --json --rpc-url $DOCKER_RPC_URL --private-key $PRIVATE_KEY "$1:$2" > ../build/$2.json diff --git a/dev/contracts/deploy-testnet b/dev/contracts/deploy-testnet index d5a18abc..97882a17 100755 --- a/dev/contracts/deploy-testnet +++ b/dev/contracts/deploy-testnet @@ -4,6 +4,9 @@ set -euo cd ./contracts +# Update depencencies +forge soldeer update + # Deploy the contract to the chain with the specified RPC URL and attempt to verify the SC code function deploy_contract() { forge create \ diff --git a/dev/docker/Dockerfile-cli b/dev/docker/Dockerfile-cli index 9e3a970b..530f451b 100644 --- a/dev/docker/Dockerfile-cli +++ b/dev/docker/Dockerfile-cli @@ -23,6 +23,8 @@ LABEL description="XMTPD CLI" # color, nocolor, json ENV GOLOG_LOG_FMT=nocolor +ENV XMTPD_LOG_ENCODING=json + COPY --from=builder /app/bin/xmtpd-cli /usr/bin/ ENTRYPOINT ["/usr/bin/xmtpd-cli"] diff --git a/dev/register-local-node b/dev/register-local-node index 29b019dd..bdc8a6a6 100755 --- a/dev/register-local-node +++ b/dev/register-local-node @@ -8,6 +8,6 @@ export NODE_ADDRESS=$ANVIL_ACC_1_ADDRESS dev/cli register-node \ --http-address=http://localhost:5050 \ - --owner-address=$NODE_ADDRESS \ + --node-owner-address=$NODE_ADDRESS \ --admin-private-key=$PRIVATE_KEY \ - --signing-key-pub=$XMTPD_SIGNER_PUBLIC_KEY \ No newline at end of file + --node-signing-key-pub=$XMTPD_SIGNER_PUBLIC_KEY \ No newline at end of file diff --git a/dev/register-local-node-2 b/dev/register-local-node-2 index c3356e85..ddf22563 100755 --- a/dev/register-local-node-2 +++ b/dev/register-local-node-2 @@ -9,6 +9,6 @@ export NODE_ADDRESS=$ANVIL_ACC_2_ADDRESS dev/cli register-node \ --http-address=http://localhost:5051 \ - --owner-address=$NODE_ADDRESS \ + --node-owner-address=$NODE_ADDRESS \ --admin-private-key=$PRIVATE_KEY \ - --signing-key-pub=$XMTPD_SIGNER_PUBLIC_KEY \ No newline at end of file + --node-signing-key-pub=$XMTPD_SIGNER_PUBLIC_KEY \ No newline at end of file diff --git a/dev/up b/dev/up index a8bc0c88..81a2f4a0 100755 --- a/dev/up +++ b/dev/up @@ -13,7 +13,7 @@ if ! which sqlc &> /dev/null; then go install github.com/sqlc-dev/sqlc/cmd/sqlc; if ! which buf &> /dev/null; then go install github.com/bufbuild/buf/cmd/buf; fi if ! which golines &>/dev/null; then go install github.com/segmentio/golines@latest; fi if ! which abigen &>/dev/null; then go install github.com/ethereum/go-ethereum/cmd/abigen; fi - +if ! which jq &>/dev/null; then brew install jq; fi dev/docker/up dev/contracts/deploy-local diff --git a/doc/deploy.md b/doc/deploy.md index 671e8a90..048995af 100644 --- a/doc/deploy.md +++ b/doc/deploy.md @@ -26,9 +26,9 @@ export PRIVATE_KEY= dev/cli register-node \ --http-address= \ - --owner-address=0xd27FDB90A393Ce0E390120aeB58b326AbA910BE0 \ + --node-owner-address=0xd27FDB90A393Ce0E390120aeB58b326AbA910BE0 \ --admin-private-key=$PRIVATE_KEY \ - --signing-key-pub= + --node-signing-key-pub= ``` You need to register all (both) nodes with their correct DNS entries and public keys. diff --git a/doc/onboarding.md b/doc/onboarding.md index ea4fe98e..640f8c02 100644 --- a/doc/onboarding.md +++ b/doc/onboarding.md @@ -8,7 +8,7 @@ It is important that both `public key` and `address` are correct as they are imm An easy way to generate a new private key is to use our CLI: ```bash -$ XMTPD_LOG_ENCODING=json ./dev/cli generate-key | jq +./dev/cli generate-key | jq { "level": "INFO", "time": "2024-10-15T13:21:14.036-0400", @@ -21,7 +21,7 @@ $ XMTPD_LOG_ENCODING=json ./dev/cli generate-key | jq If you already have a private key, you can extract the relevant public details via: ```bash -$ XMTPD_LOG_ENCODING=json ./dev/cli get-pub-key --private-key 0xa9b48d687f450ea99a5faaae1be096ddb49487cb28393d3906d7359ede6ea460 | jq +./dev/cli get-pub-key --private-key 0xa9b48d687f450ea99a5faaae1be096ddb49487cb28393d3906d7359ede6ea460 | jq { "level": "INFO", "time": "2024-10-15T13:21:51.276-0400", @@ -48,9 +48,9 @@ export PRIVATE_KEY= dev/cli register-node \ --http-address= \ - --owner-address= \ + --node-owner-address= \ --admin-private-key=$PRIVATE_KEY \ - --signing-key-pub= + --node-signing-key-pub= ``` ## Step 4) Start the node diff --git a/go.mod b/go.mod index af2dcb22..c73dab13 100644 --- a/go.mod +++ b/go.mod @@ -336,15 +336,15 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect diff --git a/go.sum b/go.sum index eda7e622..38002d69 100644 --- a/go.sum +++ b/go.sum @@ -1217,8 +1217,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1335,8 +1335,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1413,14 +1413,14 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1433,8 +1433,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/api/payer/service.go b/pkg/api/payer/service.go index 71bc57f7..3a8d918a 100644 --- a/pkg/api/payer/service.go +++ b/pkg/api/payer/service.go @@ -82,6 +82,7 @@ func (s *Service) PublishClientEnvelopes( ) var originatorEnvelope *envelopesProto.OriginatorEnvelope if originatorEnvelope, err = s.publishToBlockchain(ctx, payload.payload); err != nil { + s.log.Error("error publishing payer envelopes", zap.Error(err)) return nil, status.Errorf(codes.Internal, "error publishing group message: %v", err) } out[payload.originalIndex] = originatorEnvelope diff --git a/pkg/api/server.go b/pkg/api/server.go index 3b388116..9bfc8396 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -2,6 +2,7 @@ package api import ( "context" + "github.com/xmtp/xmtpd/pkg/interceptors/server" "net" "strings" "sync" @@ -61,6 +62,11 @@ func NewAPIServer( prometheus.EnableHandlingTimeHistogram() }) + loggingInterceptor, err := server.NewLoggingInterceptor(log) + if err != nil { + return nil, err + } + unary := []grpc.UnaryServerInterceptor{prometheus.UnaryServerInterceptor} stream := []grpc.StreamServerInterceptor{prometheus.StreamServerInterceptor} @@ -75,6 +81,9 @@ func NewAPIServer( PermitWithoutStream: true, MinTime: 15 * time.Second, }), + grpc.ChainUnaryInterceptor(loggingInterceptor.Unary()), + grpc.ChainStreamInterceptor(loggingInterceptor.Stream()), + // grpc.MaxRecvMsgSize(s.Config.Options.MaxMsgSize), } diff --git a/pkg/blockchain/blockchainPublisher.go b/pkg/blockchain/blockchainPublisher.go index 63e17b8f..e29c8cfe 100644 --- a/pkg/blockchain/blockchainPublisher.go +++ b/pkg/blockchain/blockchainPublisher.go @@ -3,6 +3,10 @@ package blockchain import ( "context" "errors" + "fmt" + "math/big" + "sync" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -23,6 +27,8 @@ type BlockchainPublisher struct { messagesContract *abis.GroupMessages identityUpdateContract *abis.IdentityUpdates logger *zap.Logger + mutexNonce sync.Mutex + nonce *uint64 } func NewBlockchainPublisher( @@ -49,7 +55,6 @@ func NewBlockchainPublisher( if err != nil { return nil, err } - return &BlockchainPublisher{ signer: signer, logger: logger.Named("GroupBlockchainPublisher"). @@ -68,8 +73,15 @@ func (m *BlockchainPublisher) PublishGroupMessage( if len(message) == 0 { return nil, errors.New("message is empty") } + + nonce, err := m.fetchNonce(ctx) + if err != nil { + return nil, err + } + tx, err := m.messagesContract.AddMessage(&bind.TransactOpts{ Context: ctx, + Nonce: new(big.Int).SetUint64(nonce), From: m.signer.FromAddress(), Signer: m.signer.SignerFunc(), }, groupID, message) @@ -104,8 +116,15 @@ func (m *BlockchainPublisher) PublishIdentityUpdate( if len(identityUpdate) == 0 { return nil, errors.New("identity update is empty") } + + nonce, err := m.fetchNonce(ctx) + if err != nil { + return nil, err + } + tx, err := m.identityUpdateContract.AddIdentityUpdate(&bind.TransactOpts{ Context: ctx, + Nonce: new(big.Int).SetUint64(nonce), From: m.signer.FromAddress(), Signer: m.signer.SignerFunc(), }, inboxId, identityUpdate) @@ -135,6 +154,62 @@ func (m *BlockchainPublisher) PublishIdentityUpdate( ) } +func (m *BlockchainPublisher) fetchNonce(ctx context.Context) (uint64, error) { + // NOTE:since pendingNonce starts at 0, and we have to return that value exactly, + // we can't easily use Once with unsigned integers + if m.nonce == nil { + m.mutexNonce.Lock() + defer m.mutexNonce.Unlock() + if m.nonce == nil { + // PendingNonceAt gives the next nonce that should be used + // if we are the first thread to initialize the nonce, we want to return PendingNonce+0 + nonce, err := m.client.PendingNonceAt(ctx, m.signer.FromAddress()) + if err != nil { + return 0, err + } + m.nonce = &nonce + m.logger.Info(fmt.Sprintf("Starting server with blockchain nonce: %d", *m.nonce)) + return *m.nonce, nil + } + } + // Once the nonce has been initialized we can depend on Atomic to return the next value + next := atomic.AddUint64(m.nonce, 1) + + pending, err := m.client.PendingNonceAt(ctx, m.signer.FromAddress()) + if err != nil { + return 0, err + } + + m.logger.Debug( + "Generated nonce", + zap.Uint64("pending_nonce", pending), + zap.Uint64("atomic_nonce", next), + ) + + if next >= pending { + // normal case scenario + return next, nil + + } + + // in some cases the chain nonce jumps ahead, and we need to handle this case + // this won't catch all possible timing scenarios, but it should self-heal if the chain jumps + m.mutexNonce.Lock() + defer m.mutexNonce.Unlock() + currentNonce := atomic.LoadUint64(m.nonce) + if currentNonce < pending { + m.logger.Info( + "Nonce skew detected", + zap.Uint64("pending_nonce", pending), + zap.Uint64("current_nonce", currentNonce), + ) + atomic.StoreUint64(m.nonce, pending) + return pending, nil + } + + return atomic.AddUint64(m.nonce, 1), nil +} + func findLog[T any]( receipt *types.Receipt, parse func(types.Log) (*T, error), diff --git a/pkg/config/cliOptions.go b/pkg/config/cliOptions.go new file mode 100644 index 00000000..51d8fee7 --- /dev/null +++ b/pkg/config/cliOptions.go @@ -0,0 +1,32 @@ +package config + +type GlobalOptions struct { + Contracts ContractsOptions `group:"Contracts Options" namespace:"contracts"` + Log LogOptions `group:"Log Options" namespace:"log"` +} + +type GenerateKeyOptions struct{} + +type GetAllNodesOptions struct{} + +type UpdateHealthOptions struct { + AdminPrivateKey string `long:"admin-private-key" description:"Private key of the admin to administer the node"` + NodeId int64 `long:"node-id" description:"NodeId to update"` +} + +type UpdateAddressOptions struct { + PrivateKey string `long:"private-key" description:"Private key of node to be updated"` + NodeId int64 `long:"node-id" description:"NodeId to update"` + Address string `long:"address" description:"New HTTP address"` +} + +type GetPubKeyOptions struct { + PrivateKey string `long:"private-key" description:"Private key you want the public key for" required:"true"` +} + +type RegisterNodeOptions struct { + HttpAddress string `long:"http-address" description:"HTTP address to register for the node" required:"true"` + OwnerAddress string `long:"node-owner-address" description:"Blockchain address of the intended owner of the registration NFT" required:"true"` + AdminPrivateKey string `long:"admin-private-key" description:"Private key of the admin to register the node" required:"true"` + SigningKeyPub string `long:"node-signing-key-pub" description:"Signing key of the node to register" required:"true"` +} diff --git a/pkg/config/options.go b/pkg/config/options.go index ad928c84..c9885a8f 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -20,10 +20,10 @@ type ContractsOptions struct { type DbOptions struct { ReaderConnectionString string `long:"reader-connection-string" env:"XMTPD_DB_READER_CONNECTION_STRING" description:"Reader connection string"` - WriterConnectionString string `long:"writer-connection-string" env:"XMTPD_DB_WRITER_CONNECTION_STRING" description:"Writer connection string" required:"true"` - ReadTimeout time.Duration `long:"read-timeout" env:"XMTPD_DB_READ_TIMEOUT" description:"Timeout for reading from the database" default:"10s"` - WriteTimeout time.Duration `long:"write-timeout" env:"XMTPD_DB_WRITE_TIMEOUT" description:"Timeout for writing to the database" default:"10s"` - MaxOpenConns int `long:"max-open-conns" env:"XMTPD_DB_MAX_OPEN_CONNS" description:"Maximum number of open connections" default:"80"` + WriterConnectionString string `long:"writer-connection-string" env:"XMTPD_DB_WRITER_CONNECTION_STRING" description:"Writer connection string"` + ReadTimeout time.Duration `long:"read-timeout" env:"XMTPD_DB_READ_TIMEOUT" description:"Timeout for reading from the database" default:"10s"` + WriteTimeout time.Duration `long:"write-timeout" env:"XMTPD_DB_WRITE_TIMEOUT" description:"Timeout for writing to the database" default:"10s"` + MaxOpenConns int `long:"max-open-conns" env:"XMTPD_DB_MAX_OPEN_CONNS" description:"Maximum number of open connections" default:"80"` WaitForDB time.Duration `long:"wait-for" env:"XMTPD_DB_WAIT_FOR" description:"wait for DB on start, up to specified duration"` } @@ -39,7 +39,7 @@ type MetricsOptions struct { } type PayerOptions struct { - PrivateKey string `long:"private-key" env:"XMTPD_PAYER_PRIVATE_KEY" description:"Private key used to sign blockchain transactions" required:"true"` + PrivateKey string `long:"private-key" env:"XMTPD_PAYER_PRIVATE_KEY" description:"Private key used to sign blockchain transactions"` Enable bool `long:"enable" env:"XMTPD_PAYER_ENABLE" description:"Enable the payer API"` } @@ -71,33 +71,7 @@ type LogOptions struct { } type SignerOptions struct { - PrivateKey string `long:"private-key" env:"XMTPD_SIGNER_PRIVATE_KEY" description:"Private key used to sign messages" required:"true"` -} - -type GenerateKeyOptions struct{} - -type GetAllNodesOptions struct{} - -type UpdateHealthOptions struct { - AdminPrivateKey string `long:"admin-private-key" description:"Private key of the admin to administer the node"` - NodeId int64 `long:"node-id" description:"NodeId to update"` -} - -type UpdateAddressOptions struct { - PrivateKey string `long:"private-key" description:"Private key of node to be updated"` - NodeId int64 `long:"node-id" description:"NodeId to update"` - Address string `long:"address" description:"New HTTP address"` -} - -type GetPubKeyOptions struct { - PrivateKey string `long:"private-key" description:"Private key you want the public key for" required:"true"` -} - -type RegisterNodeOptions struct { - HttpAddress string `long:"http-address" description:"HTTP address to register for the node" required:"true"` - OwnerAddress string `long:"owner-address" description:"Blockchain address of the intended owner of the registration NFT" required:"true"` - AdminPrivateKey string `long:"admin-private-key" description:"Private key of the admin to register the node" required:"true"` - SigningKeyPub string `long:"signing-key-pub" description:"Signing key of the node to register" required:"true"` + PrivateKey string `long:"private-key" env:"XMTPD_SIGNER_PRIVATE_KEY" description:"Private key used to sign messages"` } type ServerOptions struct { diff --git a/pkg/config/validation.go b/pkg/config/validation.go new file mode 100644 index 00000000..9cb0b9e3 --- /dev/null +++ b/pkg/config/validation.go @@ -0,0 +1,90 @@ +package config + +import ( + "errors" + "fmt" + "strings" +) + +func ValidateServerOptions(options ServerOptions) error { + missingSet := make(map[string]struct{}) + customSet := make(map[string]struct{}) + + if options.Contracts.RpcUrl == "" { + missingSet["--contracts.rpc-url"] = struct{}{} + } + + if options.Contracts.NodesContractAddress == "" { + missingSet["--contracts.nodes-address"] = struct{}{} + } + + if options.Contracts.MessagesContractAddress == "" { + missingSet["--contracts.messages-address"] = struct{}{} + } + + if options.Contracts.IdentityUpdatesContractAddress == "" { + missingSet["--contracts.identity-updates-address"] = struct{}{} + } + + if options.Contracts.ChainID == 0 { + customSet["--contracts.chain-id must be greater than 0"] = struct{}{} + } + + if options.Contracts.RefreshInterval <= 0 { + customSet["--contracts.refresh-interval must be greater than 0"] = struct{}{} + } + + if options.Contracts.MaxChainDisconnectTime <= 0 { + customSet["--contracts.max-chain-disconnect-time must be greater than 0"] = struct{}{} + } + + if options.Payer.Enable { + if options.Payer.PrivateKey == "" { + missingSet["--payer.PrivateKey"] = struct{}{} + } + } + + if options.Replication.Enable { + if options.DB.WriterConnectionString == "" { + missingSet["--DB.WriterConnectionString"] = struct{}{} + } + if options.Signer.PrivateKey == "" { + missingSet["--Signer.PrivateKey"] = struct{}{} + } + } + + if options.Sync.Enable { + if options.DB.WriterConnectionString == "" { + missingSet["--DB.WriterConnectionString"] = struct{}{} + } + } + + if options.Indexer.Enable { + if options.DB.WriterConnectionString == "" { + missingSet["--DB.WriterConnectionString"] = struct{}{} + } + } + + if len(missingSet) > 0 || len(customSet) > 0 { + var errs []string + if len(missingSet) > 0 { + + var errorMessages []string + for err := range missingSet { + errorMessages = append(errorMessages, err) + } + errs = append( + errs, + fmt.Sprintf("Missing required arguments: %s", strings.Join(errorMessages, ", ")), + ) + } + if len(customSet) > 0 { + for err := range customSet { + errs = append(errs, err) + } + } + return errors.New(strings.Join(errs, "; ")) + } + + return nil +} diff --git a/pkg/indexer/storer/groupMessage.go b/pkg/indexer/storer/groupMessage.go index 581a815b..472bb226 100644 --- a/pkg/indexer/storer/groupMessage.go +++ b/pkg/indexer/storer/groupMessage.go @@ -62,7 +62,7 @@ func (s *GroupMessageStorer) StoreLog(ctx context.Context, event types.Log) LogS signedOriginatorEnvelope, err := buildSignedOriginatorEnvelope( buildOriginatorEnvelope(msgSent.SequenceId, msgSent.Message), - event.BlockHash, + event.TxHash, ) if err != nil { s.logger.Error("Error building signed originator envelope", zap.Error(err)) diff --git a/pkg/interceptors/server/logging.go b/pkg/interceptors/server/logging.go new file mode 100644 index 00000000..1ad16401 --- /dev/null +++ b/pkg/interceptors/server/logging.go @@ -0,0 +1,81 @@ +package server + +import ( + "context" + "fmt" + "go.uber.org/zap" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/status" +) + +// LoggingInterceptor logs errors for unary and stream RPCs. +type LoggingInterceptor struct { + logger *zap.Logger +} + +// NewLoggingInterceptor creates a new instance of LoggingInterceptor. +func NewLoggingInterceptor(logger *zap.Logger) (*LoggingInterceptor, error) { + if logger == nil { + return nil, fmt.Errorf("logger is required") + } + + return &LoggingInterceptor{ + logger: logger, + }, nil +} + +// Unary intercepts unary RPC calls to log errors. +func (i *LoggingInterceptor) Unary() grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (interface{}, error) { + start := time.Now() + resp, err := handler(ctx, req) // Call the actual RPC handler + duration := time.Since(start) + + if err != nil { + st, _ := status.FromError(err) + i.logger.Error( + "Client Unary RPC Error", + zap.String("method", info.FullMethod), + zap.Duration("duration", duration), + zap.Any("code", st.Code()), + zap.String("message", st.Message()), + ) + } + + return resp, err + } +} + +// Stream intercepts stream RPC calls to log errors. +func (i *LoggingInterceptor) Stream() grpc.StreamServerInterceptor { + return func( + srv interface{}, + ss grpc.ServerStream, + info *grpc.StreamServerInfo, + handler grpc.StreamHandler, + ) error { + start := time.Now() + err := handler(srv, ss) // Call the actual stream handler + duration := time.Since(start) + + if err != nil { + st, _ := status.FromError(err) + i.logger.Error( + "Stream Client RPC Error", + zap.String("method", info.FullMethod), + zap.Duration("duration", duration), + zap.Any("code", st.Code()), + zap.String("message", st.Message()), + ) + } + + return err + } +} diff --git a/pkg/interceptors/server/logging_test.go b/pkg/interceptors/server/logging_test.go new file mode 100644 index 00000000..d3ddee1b --- /dev/null +++ b/pkg/interceptors/server/logging_test.go @@ -0,0 +1,107 @@ +package server + +import ( + "context" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "testing" +) + +func createTestLogger() (*zap.Logger, *observer.ObservedLogs) { + core, observedLogs := observer.New(zapcore.DebugLevel) + logger := zap.New(core) + return logger, observedLogs +} + +type mockServerStream struct { + grpc.ServerStream +} + +func (m *mockServerStream) Context() context.Context { + return context.Background() +} + +func TestUnaryLoggingInterceptor(t *testing.T) { + logger, logs := createTestLogger() + + interceptor, err := NewLoggingInterceptor(logger) + require.NoError(t, err) + + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return nil, status.Errorf(codes.Internal, "mock internal error") + } + + ctx := context.Background() + info := &grpc.UnaryServerInfo{ + FullMethod: "/test.TestService/TestMethod", + } + req := struct{}{} + + interceptorUnary := interceptor.Unary() + _, err = interceptorUnary(ctx, req, info, handler) + + require.Error(t, err) + require.Equal(t, 1, logs.Len(), "expected one log entry but got none") + + logEntry := logs.All()[0] + + require.Equal(t, zapcore.ErrorLevel, logEntry.Level, "expected log level 'Error'") + require.Contains(t, logEntry.ContextMap(), "method") + require.Equal( + t, + "/test.TestService/TestMethod", + logEntry.ContextMap()["method"], + "expected log to contain correct method", + ) + require.Contains(t, logEntry.ContextMap(), "message") + require.Equal( + t, + "mock internal error", + logEntry.ContextMap()["message"], + "expected log to contain correct error message", + ) +} +func TestStreamLoggingInterceptor(t *testing.T) { + logger, logs := createTestLogger() + interceptor, err := NewLoggingInterceptor(logger) + require.NoError(t, err) + + handler := func(srv interface{}, ss grpc.ServerStream) error { + return status.Errorf(codes.NotFound, "mock stream error") + } + + info := &grpc.StreamServerInfo{ + FullMethod: "/test.TestService/TestStream", + } + + stream := &mockServerStream{} + + incerceptorStream := interceptor.Stream() + err = incerceptorStream(nil, stream, info, handler) + + require.Error(t, err) + require.Equal(t, 1, logs.Len(), "expected one log entry but got none") + + logEntry := logs.All()[0] + + require.Equal(t, zapcore.ErrorLevel, logEntry.Level, "expected log level 'Error'") + require.Contains(t, logEntry.ContextMap(), "method") + require.Equal( + t, + "/test.TestService/TestStream", + logEntry.ContextMap()["method"], + "expected log to contain correct method", + ) + require.Contains(t, logEntry.ContextMap(), "message") + require.Equal( + t, + "mock stream error", + logEntry.ContextMap()["message"], + "expected log to contain correct error message", + ) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index dec60ef5..b38749cb 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,6 +3,7 @@ package server import ( "context" "database/sql" + "github.com/xmtp/xmtpd/pkg/mlsvalidate" "net" "os" "os/signal" @@ -15,7 +16,6 @@ import ( "github.com/xmtp/xmtpd/pkg/blockchain" "github.com/xmtp/xmtpd/pkg/indexer" "github.com/xmtp/xmtpd/pkg/metrics" - "github.com/xmtp/xmtpd/pkg/mlsvalidate" "github.com/xmtp/xmtpd/pkg/proto/xmtpv4/message_api" "github.com/xmtp/xmtpd/pkg/proto/xmtpv4/payer_api" "github.com/xmtp/xmtpd/pkg/sync" @@ -42,8 +42,6 @@ type ReplicationServer struct { indx *indexer.Indexer options config.ServerOptions metrics *metrics.Server - writerDB *sql.DB - // Can add reader DB later if needed } func NewReplicationServer( @@ -80,31 +78,36 @@ func NewReplicationServer( options: options, log: log, nodeRegistry: nodeRegistry, - writerDB: writerDB, metrics: mtcs, } s.ctx, s.cancel = context.WithCancel(ctx) - s.registrant, err = registrant.NewRegistrant( - s.ctx, - log, - queries.New(s.writerDB), - nodeRegistry, - options.Signer.PrivateKey, - ) - if err != nil { - return nil, err - } - - validationService, err := mlsvalidate.NewMlsValidationService(ctx, log, options.MlsValidation) - if err != nil { - return nil, err + if options.Replication.Enable || options.Sync.Enable { + s.registrant, err = registrant.NewRegistrant( + s.ctx, + log, + queries.New(writerDB), + nodeRegistry, + options.Signer.PrivateKey, + ) + if err != nil { + return nil, err + } } if options.Indexer.Enable { + validationService, err := mlsvalidate.NewMlsValidationService( + ctx, + log, + options.MlsValidation, + ) + if err != nil { + return nil, err + } + s.indx = indexer.NewIndexer(ctx, log) err = s.indx.StartIndexer( - s.writerDB, + writerDB, options.Contracts, validationService, ) @@ -116,8 +119,52 @@ func NewReplicationServer( log.Info("Indexer service enabled") } - serviceRegistrationFunc := func(grpcServer *grpc.Server) error { + if options.Payer.Enable || options.Replication.Enable { + err = startAPIServer( + s.ctx, + log, + options, + s, + writerDB, + blockchainPublisher, + listenAddress) + if err != nil { + return nil, err + } + log.Info("API server started", zap.Int("port", options.API.Port)) + } + + if options.Sync.Enable { + s.syncServer, err = sync.NewSyncServer( + s.ctx, + log, + s.nodeRegistry, + s.registrant, + writerDB, + ) + if err != nil { + return nil, err + } + + log.Info("Sync service enabled") + } + + return s, nil +} + +func startAPIServer( + ctx context.Context, + log *zap.Logger, + options config.ServerOptions, + s *ReplicationServer, + writerDB *sql.DB, + blockchainPublisher blockchain.IBlockchainPublisher, + listenAddress string, +) error { + var err error + + serviceRegistrationFunc := func(grpcServer *grpc.Server) error { if options.Replication.Enable { replicationService, err := message.NewReplicationApiService( ctx, @@ -156,37 +203,18 @@ func NewReplicationServer( return nil } - if options.Payer.Enable || options.Replication.Enable { - s.apiServer, err = api.NewAPIServer( - s.ctx, - log, - listenAddress, - options.Reflection.Enable, - serviceRegistrationFunc, - ) - if err != nil { - return nil, err - } - - log.Info("API server started", zap.Int("port", options.API.Port)) - } - - if options.Sync.Enable { - s.syncServer, err = sync.NewSyncServer( - s.ctx, - log, - s.nodeRegistry, - s.registrant, - s.writerDB, - ) - if err != nil { - return nil, err - } - - log.Info("Sync service enabled") + s.apiServer, err = api.NewAPIServer( + s.ctx, + log, + listenAddress, + options.Reflection.Enable, + serviceRegistrationFunc, + ) + if err != nil { + return err } - return s, nil + return nil } func (s *ReplicationServer) Addr() net.Addr {