diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..191bd998 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @LeoHChen @AndyBoWu @jdubpark @edisonz0718 @ezreal1997 @limengformal @Narangde @leeren diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..d7329a76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Submit a bug ticket if you have an issue. If you're a user, check out the Story Protocol Discord server for faster assistance. +title: '' +labels: 'bug' +assignees: '' +--- + + +## Description and context + + + +## Steps to reproduce + + + +1. +2. +3. + +## Experienced behavior + + + +## Expected behavior + + + +## Solution recommendation + diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 00000000..3de509ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Story Protocol Official Discord + url: https://discord.gg/storyprotocol + about: If you're a user, this is the fastest way to get help. Do not give your wallet private key or mnemonic words to anyone. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..0296a0bb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,20 @@ +--- +name: Feature Request +about: Create a feature request item to be picked up by a contributor. +title: '' +labels: 'enhancement' +assignees: '' + +--- + +## Description and context + + + + + +## Suggested solution + + +## Definition of done + diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 00000000..27b5b343 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,20 @@ +--- +name: Task +about: Create a regular work item to be picked up by a contributor. +title: '' +labels: '' +assignees: '' + +--- + +## Description and context + + + + + +## Suggested solution + + +## Definition of done + diff --git a/.github/ISSUE_TEMPLATE/tracking_issue.md b/.github/ISSUE_TEMPLATE/tracking_issue.md new file mode 100644 index 00000000..efa993f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tracking_issue.md @@ -0,0 +1,25 @@ +--- +name: Tracking issue +about: Tracking issues are task lists used to better organize regular work items. +title: 'Tracking issue for *ADD_PROJECT* - *ADD_COMPONENT*' +labels: 'epic' +assignees: '' + +--- + +This issue is for grouping *ADD_COMPONENT* related tasks that are necessary for *ADD_PROJECT*. + +### Other tracking issues for the same project: + + + +- #XXXX +- #XXXX +- #XXXX + + +```[tasklist] +### Task list +- [ ] XXXX +- [ ] XXXX +``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c2bd5e44 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md new file mode 100644 index 00000000..36e7fd9b --- /dev/null +++ b/.github/pull-request-template.md @@ -0,0 +1,3 @@ + + +issue: diff --git a/.github/workflows/ci-docs.yml.disabled b/.github/workflows/ci-docs.yml.disabled new file mode 100644 index 00000000..ac44cdcb --- /dev/null +++ b/.github/workflows/ci-docs.yml.disabled @@ -0,0 +1,29 @@ +name: ci docs +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_DOCS_PROJECT_ID }} +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'docs/site/**' +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install PNPM + uses: pnpm/action-setup@v3 + with: + version: 8 + - name: Install Vercel CLI + run: pnpm install --global vercel@latest + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/ci-iliad-s3.yml b/.github/workflows/ci-iliad-s3.yml new file mode 100644 index 00000000..5bbf8532 --- /dev/null +++ b/.github/workflows/ci-iliad-s3.yml @@ -0,0 +1,150 @@ +name: Build and Upload iliad Binary + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + id-token: write + contents: write + pull-requests: write + actions: write + +env: + NUM_INTERNAL_BINARIES_TO_KEEP: 50 + NUM_PUBLIC_BINARIES_TO_KEEP: 400 + +jobs: + # Add timestamp + Timestamp: + uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main + + # Build and upload the iliad binary + build_and_push: + needs: Timestamp + runs-on: ubuntu-latest + strategy: + matrix: + platform: [linux-386, linux-amd64, linux-arm, linux-arm64, darwin-amd64, darwin-arm64, windows-amd64, windows-386] + + steps: + - name: Checkout code + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: arn:aws:iam::478656756051:role/iac-max-role + aws-region: us-west-1 + role-session-name: github-actions + + - name: Extract the version + run: | + PARAMS_FILE="./lib/buildinfo/buildinfo.go" + VERSION_MAJOR=$(awk -F= '/VersionMajor/ {gsub(/[^0-9]/, "", $2); printf "%s", $2}' $PARAMS_FILE) + VERSION_MINOR=$(awk -F= '/VersionMinor/ {gsub(/[^0-9]/, "", $2); printf "%s", $2}' $PARAMS_FILE) + VERSION_PATCH=$(awk -F= '/VersionPatch/ {gsub(/[^0-9]/, "", $2); printf "%s", $2}' $PARAMS_FILE) + VERSION_META=$(awk -F\" '/VersionMeta/ {print $2; exit}' $PARAMS_FILE) + + VERSION="$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" + if [ "$VERSION_META" != "stable" ]; then + VERSION+="-${VERSION_META}" + fi + + echo "Version extracted: $VERSION" + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Build the iliad binary + run: | + IFS="-" read -r GOOS GOARCH <<< "${{ matrix.platform }}" + bin_name=iliad + if [ "$GOOS" = "windows" ]; then + bin_name+='.exe' + fi + + echo "Building for $GOOS/$GOARCH..." + cd client + env GOOS=$GOOS GOARCH=$GOARCH go build -o $bin_name > /dev/null 2>&1 + chmod +x $bin_name + + - name: Upload the iliad binary to S3 + run: | + export TZ=America/Los_Angeles + IFS="-" read -r GOOS GOARCH <<< "${{ matrix.platform }}" + TIMESTAMP=$(date +%Y%m%d%H%M%S) + HUMAN_READABLE_VERSION=$(date) + COMMIT_HASH=$(git rev-parse --short HEAD) + FOLDER_NAME="iliad-${{ matrix.platform }}-${VERSION}-${COMMIT_HASH}" + ARCHIVE_NAME="${FOLDER_NAME}.tar.gz" + + bin_name=./client/iliad + if [ "$GOOS" = "windows" ]; then + bin_name+='.exe' + fi + + mkdir $FOLDER_NAME + mv $bin_name $FOLDER_NAME/ + + echo "Archiving the iliad binary..." + tar -czvf $ARCHIVE_NAME $FOLDER_NAME + + if [ $? -ne 0 ]; then + echo "Failed to create the archive: $ARCHIVE_NAME" + exit 1 + fi + + aws s3 cp $ARCHIVE_NAME s3://iliad-geth-binaries/iliad-public/$ARCHIVE_NAME --quiet + + if [ "${{ matrix.platform }}" = "linux-amd64" ]; then + + echo "Uploading binary for internal use..." + aws s3 cp $ARCHIVE_NAME s3://iliad-geth-binaries/iliad/iliad-$TIMESTAMP --quiet + + # Update manifest file + aws s3 cp s3://iliad-geth-binaries/iliad/manifest.txt manifest.txt --quiet || touch manifest.txt + echo "$TIMESTAMP" >> manifest.txt + aws s3 cp manifest.txt s3://iliad-geth-binaries/iliad/manifest.txt --quiet + fi + + cleanup: + runs-on: ubuntu-latest + needs: build_and_push + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: arn:aws:iam::478656756051:role/iac-max-role + aws-region: us-west-1 + role-session-name: github-actions + + - name: Cleanup internal binaries + run: | + cleanup_s3() { + PREFIX=$1 + KEEP=$2 + + echo "Cleaning up in bucket iliad-geth-binaries with prefix: $PREFIX, keeping latest $KEEP binaries" + + aws s3api list-objects-v2 --bucket iliad-geth-binaries --prefix $PREFIX --query "sort_by(Contents,&LastModified)[*].Key" > all_binaries.json + + # Extract the list of keys, remove the latest $KEEP binaries + BINARIES_TO_DELETE=$(jq -r ".[0:-${KEEP}][]" all_binaries.json) + + if [ -n "$BINARIES_TO_DELETE" ]; then + # Delete old binaries + for key in $BINARIES_TO_DELETE; do + aws s3 rm s3://iliad-geth-binaries/$key --quiet + done + echo "Deleted old binaries: $BINARIES_TO_DELETE" + else + echo "No old binaries to delete." + fi + } + + # Cleanup internal binaries + cleanup_s3 "iliad/" "${NUM_INTERNAL_BINARIES_TO_KEEP}" + + # Cleanup public binaries + cleanup_s3 "iliad-public/" "${NUM_PUBLIC_BINARIES_TO_KEEP}" diff --git a/.github/workflows/ci-main-yml-disabled b/.github/workflows/ci-main-yml-disabled new file mode 100644 index 00000000..98ab01fa --- /dev/null +++ b/.github/workflows/ci-main-yml-disabled @@ -0,0 +1,28 @@ +name: ci main +# continuous integration on push to main + +on: + push: + branches: + - main + +permissions: + contents: read + pull-requests: read + +jobs: + pre-commit: + uses: ./.github/workflows/pre-commit.yml + go-tests: + uses: ./.github/workflows/gotest.yml + go-lint: + uses: ./.github/workflows/golangci-lint.yml + sol-tests: + uses: ./.github/workflows/soltest.yml + release-snapshot: + uses: ./.github/workflows/release-snapshot.yml + needs: [pre-commit, go-tests, go-lint, sol-tests] + secrets: inherit + e2e: + uses: ./.github/workflows/e2etest.yml + needs: [release-snapshot] diff --git a/.github/workflows/ci-notify.yml.disabled b/.github/workflows/ci-notify.yml.disabled new file mode 100644 index 00000000..1cf7fab3 --- /dev/null +++ b/.github/workflows/ci-notify.yml.disabled @@ -0,0 +1,46 @@ +name: ci notify + +# run after all completed workflows on main +on: + workflow_run: + workflows: ["*"] + branches: [main] + types: [completed] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Notify Slack + uses: slackapi/slack-github-action@v1.24.0 + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "šŸšØ CI failed on `${{ github.event.repository.name }}:${{ github.event.workflow_run.head_branch }}`." + } + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/4/43/Minimalist_info_Icon.png", + "alt_text": "images" + }, + { + "type": "mrkdwn", + "text": "url: ${{ github.event.workflow_run.html_url }}" + } + ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK diff --git a/.github/workflows/ci-pr-docs.yml.disabled b/.github/workflows/ci-pr-docs.yml.disabled new file mode 100644 index 00000000..b9312763 --- /dev/null +++ b/.github/workflows/ci-pr-docs.yml.disabled @@ -0,0 +1,52 @@ +name: ci pr docs +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_DOCS_PROJECT_ID }} +on: + pull_request: + paths: + - 'docs/site/**' +jobs: + deploy: + runs-on: ubuntu-latest + outputs: + url: ${{ steps.deploy.outputs.deployment_url }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install PNPM + uses: pnpm/action-setup@v3 + with: + version: 8 + - name: Install Vercel CLI + run: pnpm install --global vercel@latest + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + id: deploy + run: echo "deployment_url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})" >> $GITHUB_OUTPUT + comment: + needs: deploy + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Comment PR with Deployment URL + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const deployment_url = '${{ needs.deploy.outputs.url }}'; + const pr_number = context.payload.pull_request.number; + const repository = context.repo.repo; + const owner = context.repo.owner; + github.rest.issues.createComment({ + owner: owner, + repo: repository, + issue_number: pr_number, + body: `Docs preview complete šŸš€ see it here: ${deployment_url}` + }); diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 00000000..6579aebf --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,21 @@ +name: ci pr +# continuous integration on pull requests to main + +on: + pull_request: + branches: + - main + +permissions: + contents: read + pull-requests: read + +jobs: + pre-commit: + uses: ./.github/workflows/pre-commit.yml + go-tests: + uses: ./.github/workflows/gotest.yml + go-lint: + uses: ./.github/workflows/golangci-lint.yml + sol-tests: + uses: ./.github/workflows/soltest.yml diff --git a/.github/workflows/ci-release.yml.disabled b/.github/workflows/ci-release.yml.disabled new file mode 100644 index 00000000..87f49086 --- /dev/null +++ b/.github/workflows/ci-release.yml.disabled @@ -0,0 +1,15 @@ +name: ci release +# continuous integration on git tagged release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release-official: + uses: ./.github/workflows/release-official.yml + secrets: inherit diff --git a/.github/workflows/ci-verifypr.yml b/.github/workflows/ci-verifypr.yml new file mode 100644 index 00000000..30e743d4 --- /dev/null +++ b/.github/workflows/ci-verifypr.yml @@ -0,0 +1,17 @@ +name: verifypr +on: + pull_request: + types: [opened, reopened, edited, synchronize] + +jobs: + verify: + runs-on: ubuntu-latest + env: + GITHUB_PR: ${{ toJSON(github.event.pull_request) }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + - name: "Verify PR" + run: go run github.com/piplabs/story/scripts/verifypr diff --git a/.github/workflows/clitest.yml.disabled b/.github/workflows/clitest.yml.disabled new file mode 100644 index 00000000..de398dc7 --- /dev/null +++ b/.github/workflows/clitest.yml.disabled @@ -0,0 +1,60 @@ +name: cli install test + +on: + workflow_call: + +jobs: + test-install: + name: ${{ matrix.os }} ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + arch: x86_64 + - os: ubuntu-latest + arch: arm64 + - os: macos-latest + arch: x86_64 + - os: macos-latest + arch: arm64 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Install Command + run: bash scripts/install_iliad_cli.sh + shell: bash + + - name: Confirm Installation Location + run: ls -la ~/bin + + - name: Check Installation + run: | + export PATH=$PATH:~/bin + iliad version + shell: bash + + test-prompt: + name: windows-latest + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Install Command + run: bash scripts/install_iliad_cli.sh || true + shell: bash + + - name: Check for Windows Prompt + run: | + OUTPUT=$(bash scripts/install_iliad_cli.sh || true) + echo "$OUTPUT" + if [[ "$OUTPUT" == *"Please use Ubuntu or macOS to run this script"* ]]; then + echo "Correct prompt detected for Windows." + else + echo "Correct prompt NOT detected for Windows." + exit 1 + fi + shell: bash diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..f5512403 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,225 @@ +name: Install required dependencies and deploy + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: write + pull-requests: write + actions: write + +jobs: + # Add timestamp + Timestamp: + uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main + + # Install required dependencies + deploy: + needs: Timestamp + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: arn:aws:iam::478656756051:role/iac-max-role + aws-region: us-west-1 + role-session-name: github-actions + + - name: Install jq + run: | + sudo apt-get install -y jq + + - name: Fetch the list of ec2 public IP addresses - validators + run: | + # Define the instance names in the desired order + instance_names=("validator1" "validator2" "validator3" "validator4" "validator5" "validator6" "validator7" "validator8" "validator9" "validator10" "validator11" "validator12" "validator13" "validator14" "validator15") + + # Initialize an array to hold the sorted IPs + sorted_ips=() + + # Loop through each instance name and fetch its public IP address + for name in "${instance_names[@]}"; do + echo "Fetching IP address for instance with name: $name" + ip=$(aws ec2 describe-instances \ + --region us-west-1 \ + --filters "Name=tag:Name,Values=$name" "Name=instance-state-name,Values=running" \ + --query "Reservations[*].Instances[*].PublicIpAddress" \ + --output text) + + if [ -n "$ip" ]; then + sorted_ips+=("$ip") + else + echo "No IP address found for instance with name: $name" + fi + done + + # Print the sorted IPs + echo "Sorted IP addresses:" + for ip in "${sorted_ips[@]}"; do + echo "$ip" + done + + # Save the sorted IPs to a file + printf "%s\n" "${sorted_ips[@]}" > sorted_ips.txt + + - name: Fetch the list of ec2 public IP addresses - bootnodes + run: | + aws ec2 describe-instances \ + --region us-west-1 \ + --filters "Name=tag:Name,Values=iliad-bootnode" \ + --query "Reservations[*].Instances[*].PublicIpAddress" \ + --output text > bootnode_ips.txt + + echo "Bootnode IPs:" + cat bootnode_ips.txt + + - name: Install ansible + run: | + sudo apt update + sudo apt install -y ansible + + - name: Add SSH key + uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.DEVNET_AWS_STG }} + + - name: Prepare the ansible inventory file + run: | + # Add the bootnode IPs to the inventory file + echo "[bootnodes]" > inventory.ini + tr '\t' '\n' < bootnode_ips.txt | awk '{print $0 " ansible_user=ec2-user"}' >> inventory.ini + echo "[validators]" >> inventory.ini + tr '\t' '\n' < sorted_ips.txt | awk '{print $0 " ansible_user=ec2-user"}' >> inventory.ini + + # Print the inventory file + cat inventory.ini + + - name: Add EC2 instances to known_hosts + run: | + for ip in $(cat sorted_ips.txt); do + ssh-keyscan -H $ip >> ~/.ssh/known_hosts + done + + for ip in $(cat bootnode_ips.txt); do + ssh-keyscan -H $ip >> ~/.ssh/known_hosts + done + + cat ~/.ssh/known_hosts + + - name: Download iliad binary from s3://iliad-geth-binaries/iliad + run: | + aws s3 cp s3://iliad-geth-binaries/iliad ./client/iliad --quiet + chmod +x ./client/iliad + + - name: Download geth client from s3://iliad-geth-binaries/geth + run: | + aws s3 cp s3://iliad-geth-binaries/geth ./client/geth --quiet + chmod +x ./client/geth + + # # - name: Install Tendermint 0.37.0 on the GHA runner + # # run: | + # # curl -L -o tendermint_0.37.0-rc2_linux_amd64.tar.gz https://github.com/tendermint/tendermint/releases/download/v0.37.0-rc2/tendermint_0.37.0-rc2_linux_amd64.tar.gz + # # tar -xzf tendermint_0.37.0-rc2_linux_amd64.tar.gz + # # sudo mv tendermint /usr/local/bin/ + # # sudo chmod +x /usr/local/bin/tendermint + + - name: Run the ansible playbook + env: + ANSIBLE_HOST_KEY_CHECKING: "False" + run: | + ansible-playbook -i inventory.ini ./scripts/ansible/deploy.yml + + - name: Copy validator1 files to EC2 instance 1 + run: | + # sorted_ips is an array of the public IP addresses of the EC2 instances + scp -r ./validators/validator1/* ec2-user@$(sed -n '1p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator2 files to EC2 instance 2 + run: | + scp -r ./validators/validator2/* ec2-user@$(sed -n '2p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator3 files to EC2 instance 3 + run: | + scp -r ./validators/validator3/* ec2-user@$(sed -n '3p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator4 files to EC2 instance 4 + run: | + scp -r ./validators/validator4/* ec2-user@$(sed -n '4p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator5 files to EC2 instance 5 + run: | + # sorted_ips is an array of the public IP addresses of the EC2 instances + scp -r ./validators/validator5/* ec2-user@$(sed -n '5p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator6 files to EC2 instance 6 + run: | + scp -r ./validators/validator6/* ec2-user@$(sed -n '6p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator7 files to EC2 instance 7 + run: | + scp -r ./validators/validator7/* ec2-user@$(sed -n '7p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator8 files to EC2 instance 8 + run: | + scp -r ./validators/validator8/* ec2-user@$(sed -n '8p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator9 files to EC2 instance 9 + run: | + scp -r ./validators/validator9/* ec2-user@$(sed -n '9p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator10 files to EC2 instance 10 + run: | + scp -r ./validators/validator10/* ec2-user@$(sed -n '10p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator11 files to EC2 instance 11 + run: | + scp -r ./validators/validator11/* ec2-user@$(sed -n '11p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator12 files to EC2 instance 12 + run: | + scp -r ./validators/validator12/* ec2-user@$(sed -n '12p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator13 files to EC2 instance 13 + run: | + scp -r ./validators/validator13/* ec2-user@$(sed -n '13p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator14 files to EC2 instance 14 + run: | + scp -r ./validators/validator14/* ec2-user@$(sed -n '14p' sorted_ips.txt):/home/ec2-user/ + + - name: Copy validator15 files to EC2 instance 15 + run: | + scp -r ./validators/validator15/* ec2-user@$(sed -n '15p' sorted_ips.txt):/home/ec2-user/ + + - name: Launch the GETH process on the bootnodes + run: | + echo "Launching geth process on the bootnodes" + for ip in $(cat bootnode_ips.txt); do + ssh -o StrictHostKeyChecking=no ec2-user@$ip "nohup geth --config /home/ec2-user/devnet/geth/config/geth.toml > /home/ec2-user/geth.log 2>&1 &" + done + + - name: Launch the GETH process on the validators + run: | + echo "Launching geth process on the validators" + for ip in $(cat sorted_ips.txt); do + ssh -o StrictHostKeyChecking=no ec2-user@$ip "nohup geth --config /home/ec2-user/geth/config/geth.toml > /home/ec2-user/geth.log 2>&1 &" + done + + - name: Launch the ILIAD process on the bootnodes + run: | + echo "Launching iliad process on the bootnodes" + for ip in $(cat bootnode_ips.txt); do + ssh -o StrictHostKeyChecking=no ec2-user@$ip "nohup iliad run --home /home/ec2-user/devnet/iliad > /home/ec2-user/iliad.log 2>&1 &" + done + + - name: Launch the ILIAD process on the validators + run: | + echo "Launching iliad process on the validators" + for ip in $(cat sorted_ips.txt); do + ssh -o StrictHostKeyChecking=no ec2-user@$ip "nohup iliad run > /home/ec2-user/iliad.log 2>&1 &" + done diff --git a/.github/workflows/e2etest.yml b/.github/workflows/e2etest.yml new file mode 100644 index 00000000..9426be10 --- /dev/null +++ b/.github/workflows/e2etest.yml @@ -0,0 +1,33 @@ +name: e2e tests + +on: + workflow_call: + +jobs: + e2e_tests: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + - name: Run e2e tests + run: make e2e-ci + - name: Upload failed logs + uses: actions/upload-artifact@v4 + if: failure() + with: + name: failed-logs + path: e2e/failed-logs.txt + retention-days: 3 + + - name: e2e-logs + if: failure() + uses: metrico/loki-action@V3.1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + endpoint: ${{ secrets.LOGQL_ENDPOINT }} + username: ${{ secrets.LOGQL_USER }} + password: ${{ secrets.LOGQL_PASS }} diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 00000000..e3f5c388 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,22 @@ +name: go lint +# Run this separately from pre-commit since then we get nice inline messages in PRs. + +on: + workflow_call: + +jobs: + golangci: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.0' + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.59.1 + # Verbose with color. Just fail, don't fix issues. Use both annotations and normal output. + args: -v --color=always --fix=false --timeout=10m --out-format=colored-line-number,github-actions diff --git a/.github/workflows/gotest.yml b/.github/workflows/gotest.yml new file mode 100644 index 00000000..06c47aad --- /dev/null +++ b/.github/workflows/gotest.yml @@ -0,0 +1,18 @@ +name: go tests +# Run this separately from pre-commit for nice visual coverage. + +on: + workflow_call: + +jobs: + unit_tests: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.0' + # TODO: add coverage + - run: go test -timeout=5m -race -tags=verify_logs ./... diff --git a/.github/workflows/pr-e2etest.yml b/.github/workflows/pr-e2etest.yml new file mode 100644 index 00000000..044ce9a1 --- /dev/null +++ b/.github/workflows/pr-e2etest.yml @@ -0,0 +1,41 @@ +name: PR e2e test + +on: + workflow_call: + +jobs: + pr_e2e_tests: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: Build binaries + uses: goreleaser/goreleaser-action@v5 + env: + GOOS: linux + with: + version: latest + args: build --single-target --snapshot --clean --id=iliad + + - name: Build iliad image + run: | + cd dist/iliad_linux_amd64_v1 + docker build -f "../../client/Dockerfile" . -t "iliadops/iliad:${GITHUB_SHA::7}" + + - name: Run devnet1 e2e test + run: | + go install github.com/piplabs/story/e2e + cd e2e && ./run-multiple.sh manifests/devnet1.toml + + - name: Upload failed logs + uses: actions/upload-artifact@v4 + if: failure() + with: + name: failed-logs + path: e2e/failed-logs.txt + retention-days: 3 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..4659d26f --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,22 @@ +name: pre-commit hooks + +on: + workflow_call: + +jobs: + pre-commit: + runs-on: ubuntu-latest + env: + # Skip a few hooks: Golangci-lint and go-tests have their own actions. Allow lints against main commits. + SKIP: golangci-lint,run-go-tests,no-commit-to-branch,run-forge-tests,run-solidity-lint + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.0' + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/release-official.yml b/.github/workflows/release-official.yml new file mode 100644 index 00000000..ea0780e8 --- /dev/null +++ b/.github/workflows/release-official.yml @@ -0,0 +1,29 @@ +name: release official +# Official releases on tagged commits. + +on: + workflow_call: + +jobs: + release-official: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: Login to Dockerhub container registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build docker images + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Calculate git short sha + id: git_ref + run: echo "short_sha=`echo ${GITHUB_SHA::7}`" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml new file mode 100644 index 00000000..1c55d3e3 --- /dev/null +++ b/.github/workflows/release-snapshot.yml @@ -0,0 +1,36 @@ +name: release snapshot +# Snapshot releases on push to main. + +on: + workflow_call: + +jobs: + release-snapshot: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: Login to Dockerhub container registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build docker images + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + # Use --snapshot to build current HEAD commit (this doesn't publish images) + args: release --snapshot --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Calculate git short sha + id: git_ref + run: echo "short_sha=`echo ${GITHUB_SHA::7}`" >> "$GITHUB_OUTPUT" + + - name: Push Iliad to Dockerhub + run: | + docker push iliadops/iliad:${GITHUB_SHA::7} + docker push iliadops/iliad:main diff --git a/.github/workflows/soltest.yml b/.github/workflows/soltest.yml new file mode 100644 index 00000000..d0dff318 --- /dev/null +++ b/.github/workflows/soltest.yml @@ -0,0 +1,23 @@ +name: solidity tests +# Run this separately from pre-commit for nice visual coverage. + +on: + workflow_call: + +jobs: + sol_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: foundry-rs/foundry-toolchain@v1 + - uses: pnpm/action-setup@v2 + with: + package_json_file: contracts/package.json + - run: pnpm install + working-directory: contracts + - name: Check lint + run: pnpm lint-check + working-directory: contracts + - name: Run tests + run: forge test -vvv + working-directory: contracts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c4cf52e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile ~/.gitignore_global + + +# IdeaIDE +.idea + +# VS Code +.vscode + +# used by the Makefile +build +dist + +# MacOS specific file +.DS_Store + +# Vercel deployment vars (docs) +.vercel + +# Environment vars +.env + +# ignore iliad binary in ./iliad directory +client/client +client/build___run_iliad_client +client/go_build_iliad_client diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..1d1b57a1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,161 @@ +run: + timeout: 5m + go: "1.22" +linters-settings: + exhaustive: + default-signifies-exhaustive: true + forbidigo: + forbid: + - 'fmt\.Print.*(# Avoid debug logging)?' + - 'fmt\.Errorf.*(# Prefer lib/errors.Wrap)?' + - 'prometheus\.New.*(# Prefer promauto)?' + gci: # Auto-format imports + sections: + - standard # Go stdlib + - prefix(golang.org) # Golang pkg import + - prefix(cosmossdk.io) # Cosmos SDK import + - prefix(github.com) # Catch-all Github direct imports + - prefix(github.com/storyprotocol) # Story + - prefix(github.com/piplabs/story) # Iliad + - default # All other imports not matched to another section type. + - blank # Blank imports + custom-order: true + govet: + enable-all: true + importas: + no-unaliased: true + alias: + - pkg: github.com/cometbft/cometbft/crypto/secp256k1 + alias: k1 + # TODO: Add our own import aliases here + misspell: + locale: US + nlreturn: + block-size: 2 + nolintlint: + require-explanation: true + require-specific: true + revive: + enable-all-rules: true + severity: warning + ignore-generated-header: true + rules: + # Disabled revive rules + - name: file-header # Doesn't support auto fix + disabled: true + - name: max-public-structs # Too strict + disabled: true + - name: banned-characters # Not applicable + disabled: true + - name: cognitive-complexity # False positives, address in code reviews + disabled: true + - name: function-length # False positives, address in code reviews + disabled: true + - name: function-result-limit # False positives, address in code reviews + disabled: true + - name: cyclomatic # False positives, address in code reviews + disabled: true + - name: argument-limit # False positives, address in code reviews + disabled: true + - name: line-length-limit # Requires us to cause indentation confusion: https://google.github.io/styleguide/go/decisions#indentation-confusion + disabled: true + - name: comment-spacings # Relax revive rules for prefix comment spacing + disabled: true + # Some configured revive rules + - name: imports-blocklist + arguments: + - "log" # Prefer ./lib/log + - "github.com/gogo/protobuf/proto" # Prefer google.golang.org/protobuf + - "github.com/stretchr/testify/assert" # Prefer github.com/stretchr/testify/require + - "golang.org/x/exp/slices" # Prefer slices + - name: unhandled-error + arguments: + - 'fmt.Printf' + - 'fmt.Println' + staticcheck: + checks: + - "all" + - "-SA1019" + testpackage: + skip-regexp: internal_test\.go # Allow internal tests + wrapcheck: + ignoreSigs: + - github.com/piplabs/story/ + - google.golang.org/grpc/status # No point wrapping gRPC/network errors. + - github.com/ethereum/go-ethereum # We wrap these automatically in lib/ethclient + - "Errorf" + - ".Errorf(" + - "errors.New(" + - "errors.Unwrap(" + - ".Wrap(" + - ".Wrapf(" + - ".WithMessage(" + - ".WithMessagef(" + - ".WithStack(" + +issues: + fix: true + exclude-files: + - ".*\\.pb\\.go$" # Ignore generated protobuf files + - "contracts/bindings/*" # Ignore generated contract bindings + exclude-rules: + - path: '(.*)(_test|tutil|scripts)(.*)' + linters: # Relax linters for both tests/scripts (non-production code) + - gosec # Security not required + - revive # Relax revive rules + - wrapcheck # Wrapping not required + - perfsprint # Performance not an issue here + - contextcheck # Context not an issue here + - maintidx # Relax linter for table driven tests + - dupl # Many tests share similar testing logic but call different methods with different args + - path: '(.*)(e2e)(.*)' + linters: # Relax linters for both e2e (performance not required) + - perfsprint # Performance not an issue here + - path: '(.*)(scripts|cli)(.*)' + linters: # Relax linters for scripts and clis + - forbidigo # Allow debug printing + exclude: + - add-constant # Ignore "add-constant: avoid magic numbers like" since it is too strict + - fieldalignment # Ignore "fieldalignment: struct with XXX pointer bytes could be YYY" + - "shadow: declaration of" # Relax govet + - "shadows an import name" # Relax revive + - "ifElseChain: rewrite if-else to switch statement" # IfElseChain actually preferred to switches + - "nested-structs: no nested structs are allowed" # Relax revive + - "confusing-naming" # Relax revive, we often use Foo and foo function names. + - "flag-parameter" # Relax revive, flag parameters are ok if used sparingly. + - "G306: Expect WriteFile permissions to be 0600 or less" # We write a lot of files that need to be editable. + - "exported: type name will be used as module.Module" # Cosmos style + - "defer: prefer not to defer chains of function calls" # We use this for defer latency()() + + # Loop variable issues have been fixed in Go 1.22, so can be ignored + - "G601: Implicit memory aliasing in for loop" + - "loopclosure: loop variable" + - "Range statement for test" + - "range-val-address: suspicious assignment of" + - "exporting a pointer for the loop variable snapshot" + +linters: + enable-all: true + disable: + # Disable some linters (alphabetical order) + - cyclop # False positives, address in code reviews + - depguard # Dependency guard is for enterprise users + - err113 # Too strict (goerr113) + - exhaustruct # Exhaustive structs results in super verbose go code + - funlen # Some functions will be long + - gocognit # We tend to write long multi-step functions + - gochecknoglobals # We use globals in many places + - godox # Allow TODOs + - goimports # Handled by gci + - gofumpt # Not compatible with gci, see https://github.com/golangci/golangci-lint/issues/1490. + - gomnd + - gomoddirectives # We have a replace directive + - interfacebloat # For pragmatic expected_keepers + - ireturn # Too many false positives + - mnd # Magic numbers + - nonamedreturns # Allow named returns + - prealloc # Too many false positives + - tagliatelle # Too strict + - varnamelen # False positives + - wsl # Way to strict and opinionated + - lll # Disable rigid line length limit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..0358ba4e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,93 @@ +repos: + # First check for secrets + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + exclude: "(pnpm-lock.yaml|testdata|static|config.json|genesis.json)" + + # Then run code formatters + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files # No large files + exclude: "lib/netconf/.*/.*" + - id: trailing-whitespace # trims trailing whitespace + exclude: "testdata" + - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline + exclude_types: ["proto"] + exclude: "(testdata|.gas-snapshot|buf.gen.yaml|lib/netconf/.*/.*)" + - id: mixed-line-ending # ensures that a file doesn't contain a mix of LF and CRLF + - id: no-commit-to-branch # Protect specific branches (default: main/master) from direct checkins + + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-mod-tidy # Run go mod tidy when go.mod changes + files: go.mod + - id: go-fmt + args: [ -w, -s ] # simplify code and write result to (source) file instead of stdout + + - repo: local + hooks: + - id: run-solidity-lint + name: run-solidity-lint + language: script + entry: .pre-commit/run_solidity_lint.sh + types: [ file, solidity ] + require_serial: true + + # Then run code validators (on the formatted code) + + - repo: https://github.com/golangci/golangci-lint # See .golangci.yml for config + rev: v1.59.1 + hooks: + - id: golangci-lint + require_serial: true # Don't run this in parallel + # Lint all go files in the repo, since this aligns with github actions. + entry: golangci-lint run --fix + + - repo: local + hooks: + - id: run-buf + name: run-buf + language: script + entry: .pre-commit/run_buf.sh + types: [ file, proto ] + pass_filenames: false + + - id: run-go-tests + name: run-go-tests + language: script + require_serial: true # Don't run this in parallel + entry: .pre-commit/run_go_tests.sh + types: [ file, go ] + + - id: run-forge-tests + name: run-forge-tests + language: script + entry: .pre-commit/run_forge_tests.sh + types: [ file, solidity ] + require_serial: true + + - id: run-regexp + name: run-regexp + language: script + entry: .pre-commit/run_regexp.sh + types: [ file, go ] + exclude: "(_test.go|contracts/bindings/.*|scripts/)" + + - id: run-goversion + name: run-goversion + language: script + entry: .pre-commit/run_goversion.sh + pass_filenames: false + types: [ file, go ] + + - id: run-solhint + name: run-solhint + language: script + entry: .pre-commit/run_solhint.sh + types: [ file, solidity ] + require_serial: true diff --git a/.pre-commit/foundry_utils.sh b/.pre-commit/foundry_utils.sh new file mode 100644 index 00000000..00236e97 --- /dev/null +++ b/.pre-commit/foundry_utils.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Set of foundry shell utils + + +# Get foundry root of a given filepath. Searches upwards from filepath for a +# directory containing foundry.toml +foundryroot() { + dir=$(dirname $1) + + while [[ "$dir" != "." && "$dir" != "/" ]]; do + if [ -f "$dir/foundry.toml" ]; then + echo "$dir" + return + fi + + dir=$(dirname $dir) + done +} + +# Get a list of unique foundry roots for a list of files +foundryroots() { + for file in $@; do + foundryroot $file + done | sort -u +} diff --git a/.pre-commit/run_buf.sh b/.pre-commit/run_buf.sh new file mode 100755 index 00000000..57631b6d --- /dev/null +++ b/.pre-commit/run_buf.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +if ! which buf 1>/dev/null; then + echo "Installing buf" + go generate scripts/tools.go +fi + +EXPECT=$(go list -f "{{.Module.Version}}" github.com/bufbuild/buf/cmd/buf) +ACTUAL="v$(buf --version)" +if [[ "${EXPECT}" != "${ACTUAL}" ]]; then + echo "Updating buf" + go generate scripts/tools.go +fi + +./scripts/buf_generate.sh +buf lint diff --git a/.pre-commit/run_forge_tests.sh b/.pre-commit/run_forge_tests.sh new file mode 100755 index 00000000..013566d3 --- /dev/null +++ b/.pre-commit/run_forge_tests.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Runs `forge test` for every unique foundry project derived from the list +# of files provided as arguments by pre-commit. + +source scripts/install_foundry.sh +source scripts/install_pnpm.sh + +# import foundryroots +source .pre-commit/foundry_utils.sh + +for dir in $(foundryroots $@); do + echo "Running 'forge test' in ./$dir" + (cd $dir && pnpm install && forge test) +done diff --git a/.pre-commit/run_go_tests.sh b/.pre-commit/run_go_tests.sh new file mode 100755 index 00000000..3e37766c --- /dev/null +++ b/.pre-commit/run_go_tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Runs go test for all touched packages +MOD=$(go list -m) +PKGS=$(echo "$@"| xargs -n1 dirname | sort -u | sed -e "s#^#${MOD}/#") + +# TODO: fix tests and enable +# go test -tags=verify_logs -failfast -race -timeout=2m $PKGS diff --git a/.pre-commit/run_goversion.sh b/.pre-commit/run_goversion.sh new file mode 100755 index 00000000..40589007 --- /dev/null +++ b/.pre-commit/run_goversion.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# run_goversion.sh ensures that the local go version matches the go.mod minor version. +# Minor version matching is fine since local go isn't used to build production binaries, that is done by CI. + +# minor returns the minor version from the provided string. +# e.g. go1.14.2 -> 1.14 +function minor() ( + REGEX="([0-9]+\.[0-9]+)\.[0-9]+" + if [[ ! $1 =~ $REGEX ]]; then + echo "Failed parsing minor version from $1" + exit 1 + fi + + echo "${BASH_REMATCH[1]}" +) + +ACTUAL=$(minor "$(go version)") +EXPECTED=$(minor "$(go list -m -f '{{.GoVersion}}')") + +if [[ "$ACTUAL" != "$EXPECTED" ]]; then + echo "Go version mismatch; $EXPECTED vs $ACTUAL" + echo "Please update local go installation to match go.mod minor version" + exit 1 +fi diff --git a/.pre-commit/run_regexp.sh b/.pre-commit/run_regexp.sh new file mode 100755 index 00000000..4cc821d1 --- /dev/null +++ b/.pre-commit/run_regexp.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +FILES=$@ + +function check() { + grep -HnE "$2" $FILES && printf "\nāŒ Regexp check failed: %s\n\n" "$1" +} + +check 'Log messages must be capitalised' 'log\.(Error|Warn|Info|Debug)\(ctx, "[[:lower:]]' && exit 1 +check 'Error messages must not be capitalised' 'errors\.(New|Wrap)\((err, )?"[[:upper:]]' && exit 1 +check 'Rather add secrets to baseline with "make secrets-baseline"' 'pragma: allowlist secret' && exit 1 +check 'See Go Guidelines for correct error wrapping' '%w' && exit 1 + +true diff --git a/.pre-commit/run_solhint.sh b/.pre-commit/run_solhint.sh new file mode 100755 index 00000000..8fbc1924 --- /dev/null +++ b/.pre-commit/run_solhint.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Solhint's repo doesn't support pre-commit out-of-the-box, so this script is the workaround. + +VERSION="4.0.0" + +if ! which solhint 1>/dev/null || [[ $(solhint --version) != "$VERSION" ]]; then + echo "Installing solhint@$VERSION" + npm install -g solhint@$VERSION +fi + +solhint $@ diff --git a/.pre-commit/run_solidity_lint.sh b/.pre-commit/run_solidity_lint.sh new file mode 100755 index 00000000..b83d0570 --- /dev/null +++ b/.pre-commit/run_solidity_lint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Runs `pnpm lint-check` for every unique foundry project derived from the list +# of files provided as arguments by pre-commit. + +source scripts/install_foundry.sh + +# import foundryroots +source .pre-commit/foundry_utils.sh + +for dir in $(foundryroots $@); do + echo "Running 'lint-check' in ./$dir" + (cd $dir && pnpm lint-check) +done diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 00000000..7136a971 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,594 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + }, + { + "path": "detect_secrets.filters.regex.should_exclude_file", + "pattern": [ + "pnpm-lock.yaml" + ] + } + ], + "results": { + ".github/workflows/ci-main.yml": [ + { + "type": "Secret Keyword", + "filename": ".github/workflows/ci-main.yml", + "hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0", + "is_verified": false, + "line_number": 25 + } + ], + "client/genutil/evm/testdata/TestMakeGenesis.golden": [ + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "ac3758d662cbdd3b3067756b1979a60d0e654a19", + "is_verified": false, + "line_number": 39 + }, + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "a3a8c0b3bd304e1527669b9c986cc5fa5ac41a30", + "is_verified": false, + "line_number": 40 + }, + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "7207ae767d4cca593a7f62869ac682a19e1f02ad", + "is_verified": false, + "line_number": 41 + }, + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "ba7a46db2bcce63c5808782aa7785c1debcddd9b", + "is_verified": false, + "line_number": 42 + }, + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "189d2eed2a609e5459e1592d3254a3a0c0467e81", + "is_verified": false, + "line_number": 43 + }, + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "7ded2ebd67f6cbd9da14253b4237b1716182cb9a", + "is_verified": false, + "line_number": 44 + }, + { + "type": "Hex High Entropy String", + "filename": "client/genutil/evm/testdata/TestMakeGenesis.golden", + "hashed_secret": "25e3b8bdce3ad76cf8028c17bdbd01057bade6c3", + "is_verified": false, + "line_number": 45 + } + ], + "client/genutil/testdata/TestMakeGenesis.golden": [ + { + "type": "Base64 High Entropy String", + "filename": "client/genutil/testdata/TestMakeGenesis.golden", + "hashed_secret": "4aaa28befbfbe108123482f4cd39eefa10804b83", + "is_verified": false, + "line_number": 20 + }, + { + "type": "Base64 High Entropy String", + "filename": "client/genutil/testdata/TestMakeGenesis.golden", + "hashed_secret": "0019803e0cfe9a6a96d766b2cf9b20c8d6f4aff7", + "is_verified": false, + "line_number": 111 + }, + { + "type": "Base64 High Entropy String", + "filename": "client/genutil/testdata/TestMakeGenesis.golden", + "hashed_secret": "77a539d98dfa381df1bc489650aef852c0ebcda6", + "is_verified": false, + "line_number": 114 + }, + { + "type": "Base64 High Entropy String", + "filename": "client/genutil/testdata/TestMakeGenesis.golden", + "hashed_secret": "7f171266f22732c3c265ac01943db64a68ecde9c", + "is_verified": false, + "line_number": 161 + } + ], + "e2e/app/agent/prometheus_internal_test.go": [ + { + "type": "Secret Keyword", + "filename": "e2e/app/agent/prometheus_internal_test.go", + "hashed_secret": "858b02bf93798fdd02736ef7ec278319018d1272", + "is_verified": false, + "line_number": 92 + } + ], + "e2e/app/geth/testdata/TestWriteConfigTOML_archive.golden": [ + { + "type": "Secret Keyword", + "filename": "e2e/app/geth/testdata/TestWriteConfigTOML_archive.golden", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "is_verified": false, + "line_number": 101 + } + ], + "e2e/app/geth/testdata/TestWriteConfigTOML_full.golden": [ + { + "type": "Secret Keyword", + "filename": "e2e/app/geth/testdata/TestWriteConfigTOML_full.golden", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "is_verified": false, + "line_number": 101 + } + ], + "e2e/app/key/key_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "e2e/app/key/key_test.go", + "hashed_secret": "7f206c47631fb0c250283516cc47fd85c370ce6f", + "is_verified": false, + "line_number": 96 + }, + { + "type": "Hex High Entropy String", + "filename": "e2e/app/key/key_test.go", + "hashed_secret": "480c5f9eeb3033bf2dfe86627210969af5e1a665", + "is_verified": false, + "line_number": 99 + } + ], + "e2e/manifests/staging.toml": [ + { + "type": "Hex High Entropy String", + "filename": "e2e/manifests/staging.toml", + "hashed_secret": "f2400ac6800113efbeede5e2b7942549147600b3", + "is_verified": false, + "line_number": 19 + } + ], + "e2e/manifests/testnet.toml": [ + { + "type": "Hex High Entropy String", + "filename": "e2e/manifests/testnet.toml", + "hashed_secret": "70c787f6b2cb3bfce2a249c364dda0fc70b2f6b1", + "is_verified": false, + "line_number": 45 + }, + { + "type": "Hex High Entropy String", + "filename": "e2e/manifests/testnet.toml", + "hashed_secret": "7d389016386dad7957488ad08e577a7f7fef8a26", + "is_verified": false, + "line_number": 51 + } + ], + "lib/create3/create3_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "lib/create3/create3_test.go", + "hashed_secret": "87d1b1cb78352bf048e630bd5cbe69d35bdc313d", + "is_verified": false, + "line_number": 22 + } + ], + "lib/ethclient/client_test.go": [ + { + "type": "Secret Keyword", + "filename": "lib/ethclient/client_test.go", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 141 + } + ], + "lib/ethclient/ethbackend/backend.go": [ + { + "type": "Secret Keyword", + "filename": "lib/ethclient/ethbackend/backend.go", + "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", + "is_verified": false, + "line_number": 131 + } + ], + "lib/fireblocks/client.go": [ + { + "type": "Secret Keyword", + "filename": "lib/fireblocks/client.go", + "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", + "is_verified": false, + "line_number": 44 + } + ], + "lib/k1util/k1util_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "lib/k1util/k1util_test.go", + "hashed_secret": "958d1c2444d6fcb271801332187e50792b047851", + "is_verified": false, + "line_number": 26 + }, + { + "type": "Secret Keyword", + "filename": "lib/k1util/k1util_test.go", + "hashed_secret": "958d1c2444d6fcb271801332187e50792b047851", + "is_verified": false, + "line_number": 26 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/k1util/k1util_test.go", + "hashed_secret": "f0b3a47d95d2d2ee98e97971760e5a3d88540d3d", + "is_verified": false, + "line_number": 27 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/k1util/k1util_test.go", + "hashed_secret": "a255755528bd4eff06e5abe7cc5b4a0ea0ab7e0b", + "is_verified": false, + "line_number": 28 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/k1util/k1util_test.go", + "hashed_secret": "d56a456dcbdc7663fe8d4b5af709b2f97d35f2a4", + "is_verified": false, + "line_number": 29 + } + ], + "lib/netconf/testnet/consensus-genesis.json": [ + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "78485ad758eb7f54eacf642b7d44e59973b14210", + "is_verified": false, + "line_number": 61 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "079ce31142c73b41232804b2ce75219c2eaadfef", + "is_verified": false, + "line_number": 160 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "5ddd709a757e109fc9bb1cfd5d31be465aed4b1b", + "is_verified": false, + "line_number": 163 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "8e0c256cabc5dd532535b0ba6880e182e9b84734", + "is_verified": false, + "line_number": 207 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "3d355ec69568c554afeb8f48ce334bdc9a11f5bf", + "is_verified": false, + "line_number": 210 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "a4329c8b1f80ed1bc79dd9486c9067efea573a36", + "is_verified": false, + "line_number": 254 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "676eda12004d7f562dcb73fd8be616a0e5e2b772", + "is_verified": false, + "line_number": 257 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/netconf/testnet/consensus-genesis.json", + "hashed_secret": "f1be26cd282bedb59a16f423b05f15dd7680d105", + "is_verified": false, + "line_number": 304 + } + ], + "lib/netconf/testnet/execution-genesis.json": [ + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "ac3758d662cbdd3b3067756b1979a60d0e654a19", + "is_verified": false, + "line_number": 39 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "a3a8c0b3bd304e1527669b9c986cc5fa5ac41a30", + "is_verified": false, + "line_number": 40 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "7207ae767d4cca593a7f62869ac682a19e1f02ad", + "is_verified": false, + "line_number": 41 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "ba7a46db2bcce63c5808782aa7785c1debcddd9b", + "is_verified": false, + "line_number": 42 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "189d2eed2a609e5459e1592d3254a3a0c0467e81", + "is_verified": false, + "line_number": 43 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "7ded2ebd67f6cbd9da14253b4237b1716182cb9a", + "is_verified": false, + "line_number": 44 + }, + { + "type": "Hex High Entropy String", + "filename": "lib/netconf/testnet/execution-genesis.json", + "hashed_secret": "25e3b8bdce3ad76cf8028c17bdbd01057bade6c3", + "is_verified": false, + "line_number": 45 + } + ], + "lib/tutil/testdata/genesis.json": [ + { + "type": "Base64 High Entropy String", + "filename": "lib/tutil/testdata/genesis.json", + "hashed_secret": "a25a547f82b0821fe2a149195674547583ffd9c0", + "is_verified": false, + "line_number": 30 + } + ], + "lib/tutil/testdata/priv-validator-key.json": [ + { + "type": "Hex High Entropy String", + "filename": "lib/tutil/testdata/priv-validator-key.json", + "hashed_secret": "e22b9130ca36904f81d6c4781e06afa9ceb2c093", + "is_verified": false, + "line_number": 2 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/tutil/testdata/priv-validator-key.json", + "hashed_secret": "a25a547f82b0821fe2a149195674547583ffd9c0", + "is_verified": false, + "line_number": 5 + }, + { + "type": "Base64 High Entropy String", + "filename": "lib/tutil/testdata/priv-validator-key.json", + "hashed_secret": "1f7e4e9473c8c12828910d0dfaca31b116f0bfe7", + "is_verified": false, + "line_number": 9 + } + ], + "scripts/gethdevnet/execution/genesis.json": [ + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "ac3758d662cbdd3b3067756b1979a60d0e654a19", + "is_verified": false, + "line_number": 39 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "a3a8c0b3bd304e1527669b9c986cc5fa5ac41a30", + "is_verified": false, + "line_number": 40 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "7207ae767d4cca593a7f62869ac682a19e1f02ad", + "is_verified": false, + "line_number": 41 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "ba7a46db2bcce63c5808782aa7785c1debcddd9b", + "is_verified": false, + "line_number": 42 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "189d2eed2a609e5459e1592d3254a3a0c0467e81", + "is_verified": false, + "line_number": 43 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "7ded2ebd67f6cbd9da14253b4237b1716182cb9a", + "is_verified": false, + "line_number": 44 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/genesis.json", + "hashed_secret": "25e3b8bdce3ad76cf8028c17bdbd01057bade6c3", + "is_verified": false, + "line_number": 45 + } + ], + "scripts/gethdevnet/execution/keystore/UTC--2022-08-19T17-38-31.257380510Z--123463a4b065722e99115d6c222f267d9cabb524": [ + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/keystore/UTC--2022-08-19T17-38-31.257380510Z--123463a4b065722e99115d6c222f267d9cabb524", + "hashed_secret": "0c067d3944d8dd16687a1fdc416d92b0d78a523c", + "is_verified": false, + "line_number": 1 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/keystore/UTC--2022-08-19T17-38-31.257380510Z--123463a4b065722e99115d6c222f267d9cabb524", + "hashed_secret": "a6ba42fe5667d5fdfa788c219dd88a99031566a3", + "is_verified": false, + "line_number": 1 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/keystore/UTC--2022-08-19T17-38-31.257380510Z--123463a4b065722e99115d6c222f267d9cabb524", + "hashed_secret": "ba1aecf13c1fc45aa1867beb65afbebcf594026d", + "is_verified": false, + "line_number": 1 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/keystore/UTC--2022-08-19T17-38-31.257380510Z--123463a4b065722e99115d6c222f267d9cabb524", + "hashed_secret": "cd78c461e40612bf67425e745c3c03df6071d98c", + "is_verified": false, + "line_number": 1 + }, + { + "type": "Hex High Entropy String", + "filename": "scripts/gethdevnet/execution/keystore/UTC--2022-08-19T17-38-31.257380510Z--123463a4b065722e99115d6c222f267d9cabb524", + "hashed_secret": "f0a9052734814c9b196f44eb9feda62efdf931c5", + "is_verified": false, + "line_number": 1 + } + ] + }, + "generated_at": "2024-08-04T14:14:14Z" +} diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 00000000..05844850 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,12 @@ +{ + "extends": "solhint:recommended", + "rules": { + "const-name-snakecase": "off", + "custom-errors": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "func-name-mixedcase": "off", + "immutable-vars-naming": "off", + "no-unused-import": "error", + "no-unused-vars": "error" + } +} diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 00000000..e9e1f54e --- /dev/null +++ b/.solhintignore @@ -0,0 +1 @@ +**/test/**/*.sol diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..0e6111d8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +contact@storyprotocol.xyz. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e0986b94 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing to Story +Thank you for considering making contributions to Story and related repositories! Below you can find what's the best way that you can help. + +## Coding Guideline + +[Go coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments) + +[Good commit messages](https://chris.beams.io/posts/git-commit/) + +## Bug Reports + +* Ensure your issue [has not already been reported][1]. It may already be fixed! +* Include the steps you carried out to produce the problem. +* Include the behavior you observed along with the behavior you expected, and + why you expected it. +* Include any relevant stack traces or debugging output. + +## Feature Requests + +We welcome feedback with or without pull requests. If you have an idea for how +to improve the project, great! All we ask is that you take the time to write a +clear and concise explanation of what need you are trying to solve. If you have +thoughts on _how_ it can be solved, include those too! + +The best way to see a feature added, however, is to submit a pull request. + +## Pull Requests + +* Before creating your pull request, it's usually worth asking if the code + you're planning on writing will actually be considered for merging. You can + do this by [opening an issue][1] and asking. It may also help give the + maintainers context for when the time comes to review your code. + +* Ensure your [commit messages are well-written][2]. This can double as your + pull request message, so it pays to take the time to write a clear message. + +* Add tests for your feature. You should be able to look at other tests for + examples. If you're unsure, don't hesitate to [open an issue][1] and ask! + +* Submit your pull request! + - Fork the repository on GitHub. + - Make your changes on your fork repository. + - Submit a PR. + +* Each PR needs to link to a github issue. Please open a github issue first to describe the code changes you plan to make and the problem it solves. Then link the issue in your PR. + +* PR title and body follows [conventional commit][3]. + - Title template: `type(app/pkg): concise description` + - See [allowed types][4] + - Description must be concise: lower case, no punctuation, no more than 50 characters. + - Scope must be concise: only a one or two folders; e.g. 'client/cmd' or 'github' or '*' + +## Find something to work on + +We are always in need of help, be it fixing documentation, reporting bugs or writing some code. +Look at places where you feel best coding practices aren't followed, code refactoring is needed or tests are missing. + +If you have questions about the development process, +feel free to [file an issue](https://github.com/piplabs/story/issues/new). + +## Code Review + +To make it easier for your PR to receive reviews, consider the reviewers will need you to: + +* follow [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments). +* write [good commit messages](https://chris.beams.io/posts/git-commit/). +* break large changes into a logical series of smaller patches which individually make easily understandable changes, and in aggregate solve a broader issue. + +[1]: https://github.com/piplabs/story/issues +[2]: https://chris.beams.io/posts/git-commit/#seven-rules +[3]: https://www.conventionalcommits.org/en/v1.0.0 +[4]: https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#type-enum diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..2894a221 --- /dev/null +++ b/LICENSE @@ -0,0 +1,696 @@ +----------- + +GNU GENERAL PUBLIC LICENSE v3 PLUS OPEN INTEROPERABILITY REQUIREMENT + +----------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +----------- + +OPEN INTEROPERABILITY REQUIREMENT + +To ensure the adoption of this Program promotes the health of the wider +blockchain ecosystem, the following additional open interoperability +requirement applies to your use of the Program: + +Within the first 90 days of your use of the source code for the Program to +deploy a production mainnet network, you must establish and maintain an +Inter-Blockchain Communication Protocol (IBC) connection to the Omni Network +(for more details, see https://omni.network). To be clear, there are no +restrictions on your use of the source code for the Program to also connect +to other networks besides the Omni Network, we actually promote that to further +interoperability. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7a2bef41 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +help: ## Display this help message + @egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-30s\033[0m %s\n", $$1, $$2}' + + +############################################################################### +### Contracts ### +############################################################################### + +.PHONY: contracts-bindings +contract-bindings: ## Generate golang contract bindings. + make -C ./contracts bindings + +############################################################################### +### Utils ### +############################################################################### + +.PHONY: ensure-detect-secrets +ensure-detect-secrets: ## Checks if detect-secrets is installed. + @which detect-secrets > /dev/null || echo "detect-secrets not installed, see https://github.com/Yelp/detect-secrets?tab=readme-ov-file#installation" + +.PHONY: install-pre-commit +install-pre-commit: ## Installs the pre-commit tool as the git pre-commit hook for this repo. + @which pre-commit > /dev/null || echo "pre-commit not installed, see https://pre-commit.com/#install" + @pre-commit install --install-hooks + +.PHONY: install-go-tools +install-go-tools: ## Installs the go-dev-tools, like buf. + @go generate scripts/tools.go + +.PHONY: lint +lint: ## Runs linters via pre-commit. + @pre-commit run -v --all-files + +.PHONY: bufgen +bufgen: ## Generates protobufs using buf generate. + @./scripts/buf_generate.sh + +.PHONY: +secrets-baseline: ensure-detect-secrets ## Update secrets baseline. + @detect-secrets scan --exclude-file pnpm-lock.yaml > .secrets.baseline + +.PHONY: fix-golden +fix-golden: ## Fixes golden test fixtures. + @./scripts/fix_golden_tests.sh + +############################################################################### +### Testing ### +############################################################################### diff --git a/README.md b/README.md new file mode 100644 index 00000000..c227dac9 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +## Installation + +Building the node requires a working Go (version 1.22 or higher, see `go.mod`) and `goreleaser` ([see installation guide here](https://goreleaser.com/install/) or install with `make ensure-go-releaser`). You can install them using your favorite package manager. Once the dependencies are installed, run: + +```bash +make build-docker +``` + +## Usage + +### Testing + +To run the end-to-end tests, run: + +```bash +MANIFEST=simple make e2e-run +``` + +### Starting a devnet + +To start a devnet, run: + +```bash +make devnet-deploy +``` + +To stop it, run: + +```bash +make devnet-clean +``` + +### Setup + +To setup the environment for development, run: +```bash +go mod download +make ensure-go-releaser +make install-go-tools +make build-docker +make build-iliad +make contracts-bindings +``` + +## Directory Structure + +
+ā”œā”€ā”€ contracts: Solidity contracts, bindings, and testing for the Iliad protocol.
+ā”‚ ā”œā”€ā”€ bindings: Go bindings for smart contracts.
+ā”‚ ā”œā”€ā”€ src: Solidity source files for the protocol's smart contracts.
+ā”‚ ā””ā”€ā”€ test: Tests for smart contracts.
+ā”œā”€ā”€ docs: Documentation resources, including images and diagrams.
+ā”œā”€ā”€ iliad: The Iliad instance, including application logic mechanisms.
+ā”‚ ā”œā”€ā”€ app: Application logic for Iliad.
+ā”‚ ā””ā”€ā”€ cmd: Command-line tools and utilities.
+ā”œā”€ā”€ lib: Core libraries for various protocol functionalities.
+ā”œā”€ā”€ scripts: Utility scripts for development and operational tasks.
+ā””ā”€ā”€ test: Testing suite for end-to-end, smoke, and utility testing.
+
+ +## Contributing + +For detailed instructions on how to contribute, including our coding standards, testing practices, and how to submit pull requests, please see [the contribution guidelines](./docs/contributing.md). + +## Acknowledgements + +The development of Iliad has been a journey of learning, adaptation, and innovation. As we built Iliad, we drew inspiration and knowledge from the work of several remarkable teams within the blockchain and Ethereum ecosystem. + +We extend our gratitude to the following teams for their pioneering work and the open-source resources they've provided, which have significantly influenced our development process: + +- [**CometBFT**](https://github.com/cometbft/cometbft): Our heartfelt thanks go to the CometBFT team for their groundbreaking work in consensus algorithms. + +- [**Geth**](https://github.com/ethereum/go-ethereum): The go-ethereum team's relentless dedication to the Ethereum ecosystem has been nothing short of inspiring. Their comprehensive and robust implementation of the Ethereum protocol has provided us with a solid foundation to build upon. + +- [**Erigon**](https://github.com/ledgerwatch/erigon): We are grateful to Erigon for their novel work in maximizing EVM performance. + +- [**Optimism**](https://github.com/ethereum-optimism/optimism): We thank the Optimism team for their dedication to open source work within the Ethereum ecosystem. + +Acknowledging these teams is not only a gesture of gratitude but also a reflection of our commitment to collaborative progress and the open-source ethos. The path they've paved has enabled us to contribute our innovations back to the community, and we look forward to continuing this tradition of mutual growth and support. + +## Security + + + +Please refer to [SECURITY.md](./SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..76f168b6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policy + +## Reporting a Vulnerability + +The security of Story is critical. If you discover any security vulnerabilities, we appreciate your help in responsibly disclosing them to us. + +To report a vulnerability, please send an email to **security@storyprotocol.xyz**. We kindly request that you provide us with the following details: + +- A clear description of the vulnerability and its potential impact. +- Steps to reproduce the vulnerability. +- Any additional information or proof of concept that can help us understand and address the issue. + +Upon receiving your report, the Story security team will create a confidential GitHub Security Advisory within our repository. This advisory will serve as a private discussion forum where you can communicate directly with our project maintainers about the vulnerability. This process ensures that sensitive information about security vulnerabilities is handled securely and responsibly until an appropriate fix can be developed and deployed. + +## Responsible Disclosure + +We believe in responsible disclosure and request that you refrain from publicly disclosing any vulnerabilities until we have had sufficient time to investigate and address them. We appreciate your cooperation in helping us maintain the security and integrity of our blockchain network. + +## Bug Bounty Program + +At this time, we do not have a bug bounty program in place. However, we greatly value the contributions of security researchers and may consider offering rewards on a case-by-case basis for significant vulnerabilities. + +## Disclaimer + +Please note that this document is subject to change and may be updated as our security practices evolve. We encourage you to check back regularly for any updates or changes. diff --git a/buf.lock b/buf.lock new file mode 100644 index 00000000..939709dc --- /dev/null +++ b/buf.lock @@ -0,0 +1,28 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: cosmos + repository: cosmos-proto + commit: 1935555c206d4afb9e94615dfd0fad31 + digest: shake256:c74d91a3ac7ae07d579e90eee33abf9b29664047ac8816500cf22c081fec0d72d62c89ce0bebafc1f6fec7aa5315be72606717740ca95007248425102c365377 + - remote: buf.build + owner: cosmos + repository: cosmos-sdk + commit: 65bf8005287c4fe7805789327d649f4c + digest: shake256:1fa3aacaeb1a254eca2e8d17f560443823bdb0e12592cded7c12fdb74e61c7522ce73632fe51efb347b12f08d313475c5832004ab161fad2ea3e17ae042990ea + - remote: buf.build + owner: cosmos + repository: gogo-proto + commit: 88ef6483f90f478fb938c37dde52ece3 + digest: shake256:89c45df2aa11e0cff97b0d695436713db3d993d76792e9f8dc1ae90e6ab9a9bec55503d48ceedd6b86069ab07d3041b32001b2bfe0227fa725dd515ff381e5ba + - remote: buf.build + owner: googleapis + repository: googleapis + commit: 28151c0d0a1641bf938a7672c500e01d + digest: shake256:49215edf8ef57f7863004539deff8834cfb2195113f0b890dd1f67815d9353e28e668019165b9d872395871eeafcbab3ccfdb2b5f11734d3cca95be9e8d139de + - remote: buf.build + owner: protocolbuffers + repository: wellknowntypes + commit: 11912caccc894a0e8bfa509e433b10c0 + digest: shake256:c14a5a03f8db59c57b701c95f6e524b42f3f3be251e60ccf4108d91c068a5c31b123ec402e62f8171734a663a8c8a940a1ec48c09448c88033112b45f5ae4c7f diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 00000000..24ea8502 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,34 @@ +version: v1 +name: buf.build/piplabs/story +deps: + - buf.build/protocolbuffers/wellknowntypes + - buf.build/cosmos/cosmos-sdk + - buf.build/cosmos/cosmos-proto + - buf.build/cosmos/gogo-proto + - buf.build/googleapis/googleapis +breaking: + use: + - FILE +lint: + use: + - DEFAULT + - COMMENTS + - FILE_LOWER_SNAKE_CASE + except: + - UNARY_RPC + - COMMENT_FIELD + - COMMENT_MESSAGE + - COMMENT_SERVICE + - COMMENT_RPC + - SERVICE_SUFFIX + - PACKAGE_VERSION_SUFFIX + - RPC_REQUEST_STANDARD_NAME + - PACKAGE_SAME_GO_PACKAGE + - PACKAGE_SAME_DIRECTORY + - PACKAGE_DIRECTORY_MATCH + - RPC_RESPONSE_STANDARD_NAME + - COMMENT_ENUM_VALUE + - COMMENT_ENUM + - ENUM_ZERO_VALUE_SUFFIX + ignore: + - tendermint diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 00000000..d99b0dd2 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,15 @@ +FROM scratch + +# Install ca-certificates (for https to EVMs) +COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Create /tmp directory (default cometBFT --temp-dir) +COPY --from=alpine:latest /tmp /tmp + +COPY iliad /app + +# Mount home directory at /iliad +VOLUME ["/iliad"] + +ENTRYPOINT ["/app"] +CMD ["run"] diff --git a/client/app/abci.go b/client/app/abci.go new file mode 100644 index 00000000..3a8c1d2e --- /dev/null +++ b/client/app/abci.go @@ -0,0 +1,184 @@ +//nolint:wrapcheck // The abci.Application is our application, so we don't need to wrap it. Long lines are fine here. +package app + +import ( + "context" + "fmt" + + storetypes "cosmossdk.io/store/types" + + abci "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/lib/log" +) + +type postFinalizeCallback func(sdk.Context) error +type multiStoreProvider func() storetypes.CacheMultiStore + +type abciWrapper struct { + abci.Application + postFinalize postFinalizeCallback + multiStoreProvider multiStoreProvider +} + +func newABCIWrapper( + app abci.Application, + finalizeCallback postFinalizeCallback, + multiStoreProvider multiStoreProvider, +) *abciWrapper { + return &abciWrapper{ + Application: app, + postFinalize: finalizeCallback, + multiStoreProvider: multiStoreProvider, + } +} + +func (l abciWrapper) Info(ctx context.Context, info *abci.RequestInfo) (*abci.ResponseInfo, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: Info") + resp, err := l.Application.Info(ctx, info) + if err != nil { + log.Error(ctx, "Info failed [BUG]", err) + } + + return resp, err +} + +func (l abciWrapper) Query(ctx context.Context, query *abci.RequestQuery) (*abci.ResponseQuery, error) { + return l.Application.Query(ctx, query) // No log here since this can be very noisy +} + +func (l abciWrapper) CheckTx(ctx context.Context, tx *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: CheckTx") + return l.Application.CheckTx(ctx, tx) +} + +func (l abciWrapper) InitChain(ctx context.Context, chain *abci.RequestInitChain) (*abci.ResponseInitChain, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: InitChain") + resp, err := l.Application.InitChain(ctx, chain) + if err != nil { + log.Error(ctx, "InitChain failed [BUG]", err) + } + + return resp, err +} + +func (l abciWrapper) PrepareProposal(ctx context.Context, proposal *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: PrepareProposal", + "height", proposal.Height, + log.Hex7("proposer", proposal.ProposerAddress), + ) + resp, err := l.Application.PrepareProposal(ctx, proposal) + if err != nil { + log.Error(ctx, "PrepareProposal failed [BUG]", err) + } + + return resp, err +} + +func (l abciWrapper) ProcessProposal(ctx context.Context, proposal *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: ProcessProposal", + "height", proposal.Height, + log.Hex7("proposer", proposal.ProposerAddress), + ) + resp, err := l.Application.ProcessProposal(ctx, proposal) + if err != nil { + log.Error(ctx, "ProcessProposal failed [BUG]", err) + } + + return resp, err +} + +// FinalizeBlock calls BeginBlock -> DeliverTx (for all txs) -> EndBlock. +func (l abciWrapper) FinalizeBlock(ctx context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { + resp, err := l.Application.FinalizeBlock(ctx, req) + if err != nil { + log.Error(ctx, "Finalize req failed [BUG]", err, "height", req.Height) + return resp, err + } + + // Call custom `PostFinalize` callback after the block is finalized. + header := cmtproto.Header{ + Height: req.Height, + Time: req.Time, + ProposerAddress: req.ProposerAddress, + NextValidatorsHash: req.NextValidatorsHash, + AppHash: resp.AppHash, // The app hash after the block is finalized. + } + sdkCtx := sdk.NewContext(l.multiStoreProvider(), header, false, nil) + if err := l.postFinalize(sdkCtx); err != nil { + log.Error(ctx, "PostFinalize callback failed [BUG]", err, "height", req.Height) + return resp, err + } + + attrs := []any{ + "val_updates", len(resp.ValidatorUpdates), + "height", req.Height, + } + for i, update := range resp.ValidatorUpdates { + attrs = append(attrs, log.Hex7(fmt.Sprintf("pubkey_%d", i), update.PubKey.GetSecp256K1())) + attrs = append(attrs, fmt.Sprintf("power_%d", i), update.Power) + } + log.Debug(ctx, "šŸ‘¾ ABCI response: FinalizeBlock", attrs...) + + for i, res := range resp.TxResults { + if res.Code == 0 { + continue + } + log.Error(ctx, "FinalizeBlock contains unexpected failed transaction [BUG]", nil, + "info", res.Info, "code", res.Code, "log", res.Log, + "code_space", res.Codespace, "index", i, "height", req.Height) + } + + return resp, err +} + +func (l abciWrapper) ExtendVote(ctx context.Context, vote *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: ExtendVote", + "height", vote.Height, + ) + resp, err := l.Application.ExtendVote(ctx, vote) + if err != nil { + log.Error(ctx, "ExtendVote failed [BUG]", err) + } + + return resp, err +} + +func (l abciWrapper) VerifyVoteExtension(ctx context.Context, extension *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: VerifyVoteExtension", + "height", extension.Height, + ) + resp, err := l.Application.VerifyVoteExtension(ctx, extension) + if err != nil { + log.Error(ctx, "VerifyVoteExtension failed [BUG]", err) + } + + return resp, err +} + +func (l abciWrapper) Commit(ctx context.Context, commit *abci.RequestCommit) (*abci.ResponseCommit, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: Commit") + return l.Application.Commit(ctx, commit) +} + +func (l abciWrapper) ListSnapshots(ctx context.Context, listSnapshots *abci.RequestListSnapshots) (*abci.ResponseListSnapshots, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: ListSnapshots") + return l.Application.ListSnapshots(ctx, listSnapshots) +} + +func (l abciWrapper) OfferSnapshot(ctx context.Context, snapshot *abci.RequestOfferSnapshot) (*abci.ResponseOfferSnapshot, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: OfferSnapshot") + return l.Application.OfferSnapshot(ctx, snapshot) +} + +func (l abciWrapper) LoadSnapshotChunk(ctx context.Context, chunk *abci.RequestLoadSnapshotChunk) (*abci.ResponseLoadSnapshotChunk, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: LoadSnapshotChunk") + return l.Application.LoadSnapshotChunk(ctx, chunk) +} + +func (l abciWrapper) ApplySnapshotChunk(ctx context.Context, chunk *abci.RequestApplySnapshotChunk) (*abci.ResponseApplySnapshotChunk, error) { + log.Debug(ctx, "šŸ‘¾ ABCI call: ApplySnapshotChunk") + return l.Application.ApplySnapshotChunk(ctx, chunk) +} diff --git a/client/app/app.go b/client/app/app.go new file mode 100644 index 00000000..e09622f0 --- /dev/null +++ b/client/app/app.go @@ -0,0 +1,198 @@ +package app + +import ( + "cosmossdk.io/depinject" + "cosmossdk.io/log" + upgradekeeper "cosmossdk.io/x/upgrade/keeper" + + abci "github.com/cometbft/cometbft/abci/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/runtime" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + "github.com/piplabs/story/client/app/keepers" + "github.com/piplabs/story/client/comet" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + + _ "cosmossdk.io/api/cosmos/tx/config/v1" // import for side-effects + _ "cosmossdk.io/x/upgrade" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/auth" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/auth/tx/config" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/bank" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/consensus" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/distribution" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/genutil" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/gov" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/mint" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/slashing" // import for side-effects + _ "github.com/cosmos/cosmos-sdk/x/staking" // import for side-effects +) + +const Name = "iliad" + +var ( + _ runtime.AppI = (*App)(nil) + _ servertypes.Application = (*App)(nil) +) + +// App extends an ABCI application, but with most of its parameters exported. +// They are exported for convenience in creating helper functions, as object +// capabilities aren't needed for testing. +type App struct { + *runtime.App + + appCodec codec.Codec + txConfig client.TxConfig + interfaceRegistry codectypes.InterfaceRegistry + + Keepers keepers.Keepers +} + +// newApp returns a reference to an initialized App. +func newApp( + logger log.Logger, + db dbm.DB, + engineCl ethclient.EngineClient, + baseAppOpts ...func(*baseapp.BaseApp), +) (*App, error) { + depCfg := depinject.Configs( + DepConfig(), + depinject.Supply( + logger, engineCl, + ), + ) + + var ( + app = new(App) + appBuilder = new(runtime.AppBuilder) + ) + if err := depinject.Inject(depCfg, + &appBuilder, + &app.appCodec, + &app.txConfig, + &app.interfaceRegistry, + &app.Keepers.AccountKeeper, + &app.Keepers.BankKeeper, + &app.Keepers.StakingKeeper, + &app.Keepers.SlashingKeeper, + &app.Keepers.DistrKeeper, + &app.Keepers.ConsensusParamsKeeper, + &app.Keepers.GovKeeper, + &app.Keepers.UpgradeKeeper, + &app.Keepers.EvmStakingKeeper, + &app.Keepers.EVMEngKeeper, + ); err != nil { + return nil, errors.Wrap(err, "dep inject") + } + + baseAppOpts = append(baseAppOpts, func(bapp *baseapp.BaseApp) { + // Use evm engine to create block proposals. + // Note that we do not check MaxTxBytes since all EngineEVM transaction MUST be included since we cannot + // postpone them to the next block. Nit: we could drop some vote extensions though...? + bapp.SetPrepareProposal(app.Keepers.EVMEngKeeper.PrepareProposal) + + // Route proposed messages to keepers for verification and external state updates. + bapp.SetProcessProposal(makeProcessProposalHandler(app)) + }) + + app.App = appBuilder.Build(db, nil, baseAppOpts...) + + // Override the preblockers with custom PreBlocker function, which handles forks. + { + app.ModuleManager.SetOrderPreBlockers(preBlockers...) + app.SetPreBlocker(app.PreBlocker) + } + + // Set "OrderEndBlockers" directly instead of using "SetOrderEndBlockers," which will panic since the staking module + // is missing in the "endBlockers", which is an intended behavior in Iliad. The panic message is: + // `panic: all modules must be defined when setting SetOrderEndBlockers, missing: [staking]` + { + app.ModuleManager.OrderEndBlockers = endBlockers + app.SetEndBlocker(app.EndBlocker) + } + + // Need to manually set the module version map, otherwise dep inject will NOT call `SetModuleVersionMap` for + // whatever reason that needs to be investigated. Since `SetModuleVersionMap` is not called, `fromVM` will have + // no entries (i.e. does not know about each module's consensus version) and will try to "add" modules during an + // upgrade. Specifically, the upgrade module will try to add all modules as new from version 0 to the latest version + // of each module since `fromVM` is empty on the very first upgrade. + app.SetInitChainer(func(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { + err := app.Keepers.UpgradeKeeper.SetModuleVersionMap(ctx, app.ModuleManager.GetVersionMap()) + if err != nil { + return nil, errors.Wrap(err, "set module version map") + } + + return app.App.InitChainer(ctx, req) + }) + + app.setupUpgradeHandlers() + app.setupUpgradeStoreLoaders() + + if err := app.Load(true); err != nil { + return nil, errors.Wrap(err, "load app") + } + + return app, nil +} + +// PreBlocker application updates every pre block. +func (a *App) PreBlocker(ctx sdk.Context, _ *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) { + // All forks should be executed at their planned upgrade heights before any modules. + a.scheduleForkUpgrade(ctx) + + res, err := a.ModuleManager.PreBlock(ctx) + if err != nil { + return nil, errors.Wrap(err, "module manager preblocker") + } + + return res, nil +} + +func (App) LegacyAmino() *codec.LegacyAmino { + return nil +} + +func (App) ExportAppStateAndValidators(_ bool, _, _ []string) (servertypes.ExportedApp, error) { + return servertypes.ExportedApp{}, errors.New("not implemented") +} + +func (App) SimulationManager() *module.SimulationManager { + return nil +} + +// SetCometAPI sets the comet API client. +// TODO: Figure out how to use depinject to set this. +func (a App) SetCometAPI(api comet.API) { + a.Keepers.EVMEngKeeper.SetCometAPI(api) +} + +func (a App) GetStakingKeeper() *stakingkeeper.Keeper { + return a.Keepers.StakingKeeper +} + +func (a App) GetAccountKeeper() authkeeper.AccountKeeper { + return a.Keepers.AccountKeeper +} + +func (a App) GetBankKeeper() bankkeeper.Keeper { + return a.Keepers.BankKeeper +} + +func (a App) GetDistrKeeper() distrkeeper.Keeper { + return a.Keepers.DistrKeeper +} + +func (a App) GetUpgradeKeeper() *upgradekeeper.Keeper { + return a.Keepers.UpgradeKeeper +} diff --git a/client/app/app_config.go b/client/app/app_config.go new file mode 100644 index 00000000..a6f7d711 --- /dev/null +++ b/client/app/app_config.go @@ -0,0 +1,213 @@ +package app + +import ( + runtimev1alpha1 "cosmossdk.io/api/cosmos/app/runtime/v1alpha1" + appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" + authmodulev1 "cosmossdk.io/api/cosmos/auth/module/v1" + bankmodulev1 "cosmossdk.io/api/cosmos/bank/module/v1" + consensusmodulev1 "cosmossdk.io/api/cosmos/consensus/module/v1" + distrmodulev1 "cosmossdk.io/api/cosmos/distribution/module/v1" + genutilmodulev1 "cosmossdk.io/api/cosmos/genutil/module/v1" + govmodulev1 "cosmossdk.io/api/cosmos/gov/module/v1" + mintmodulev1 "cosmossdk.io/api/cosmos/mint/module/v1" + slashingmodulev1 "cosmossdk.io/api/cosmos/slashing/module/v1" + stakingmodulev1 "cosmossdk.io/api/cosmos/staking/module/v1" + txconfigv1 "cosmossdk.io/api/cosmos/tx/config/v1" + upgrademodulev1 "cosmossdk.io/api/cosmos/upgrade/module/v1" + "cosmossdk.io/core/appconfig" + "cosmossdk.io/depinject" + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + evmenginemodule "github.com/piplabs/story/client/x/evmengine/module" + evmenginetypes "github.com/piplabs/story/client/x/evmengine/types" + evmstakingmodule "github.com/piplabs/story/client/x/evmstaking/module" + evmstakingtypes "github.com/piplabs/story/client/x/evmstaking/types" +) + +// Bech32HRP is the human-readable-part of the Bech32 address format. +const ( + Bech32HRP = "story" + + defaultPruningKeep = 72_000 // Keep 1 day's of application state by default (given period of 1.2s). + defaultPruningInterval = 300 // Prune every 5 minutes or so. +) + +// init initializes the Cosmos SDK configuration. +// +//nolint:gochecknoinits // Cosmos-style +func init() { + // Set prefixes + accountPubKeyPrefix := Bech32HRP + "pub" + validatorAddressPrefix := Bech32HRP + "valoper" + validatorPubKeyPrefix := Bech32HRP + "valoperpub" + consNodeAddressPrefix := Bech32HRP + "valcons" + consNodePubKeyPrefix := Bech32HRP + "valconspub" + + // Set and seal config + cfg := sdk.GetConfig() + cfg.SetBech32PrefixForAccount(Bech32HRP, accountPubKeyPrefix) + cfg.SetBech32PrefixForValidator(validatorAddressPrefix, validatorPubKeyPrefix) + cfg.SetBech32PrefixForConsensusNode(consNodeAddressPrefix, consNodePubKeyPrefix) + cfg.Seal() +} + +// DepConfig returns the default app depinject config. +func DepConfig() depinject.Config { + return depinject.Configs( + appConfig, + depinject.Supply(), + ) +} + +//nolint:gochecknoglobals // Cosmos-style +var ( + genesisModuleOrder = []string{ + authtypes.ModuleName, + banktypes.ModuleName, + distrtypes.ModuleName, + stakingtypes.ModuleName, + slashingtypes.ModuleName, + govtypes.ModuleName, + minttypes.ModuleName, + genutiltypes.ModuleName, + upgradetypes.ModuleName, + // Iliad modules + evmenginetypes.ModuleName, + evmstakingtypes.ModuleName, + } + + // NOTE: upgrade module must come first, as upgrades might break state schema. + preBlockers = []string{ + upgradetypes.ModuleName, + } + + // During begin block slashing happens after distr.BeginBlocker so that + // there is nothing left over in the validator fee pool, so as to keep the + // CanWithdrawInvariant invariant. + // NOTE: staking module is required if HistoricalEntries param > 0. + beginBlockers = []string{ + minttypes.ModuleName, + distrtypes.ModuleName, // Note: slashing happens after distr.BeginBlocker + slashingtypes.ModuleName, + stakingtypes.ModuleName, + } + + endBlockers = []string{ + govtypes.ModuleName, + evmstakingtypes.ModuleName, // Must be before staking module removes mature unbonding delegations & validators. + } + + // blocked account addresses. + blockAccAddrs = []string{ + authtypes.FeeCollectorName, + minttypes.ModuleName, + distrtypes.ModuleName, + stakingtypes.BondedPoolName, + stakingtypes.NotBondedPoolName, + evmstakingtypes.ModuleName, + } + + moduleAccPerms = []*authmodulev1.ModuleAccountPermission{ + {Account: authtypes.FeeCollectorName}, + {Account: minttypes.ModuleName, Permissions: []string{authtypes.Minter}}, + {Account: distrtypes.ModuleName}, + {Account: stakingtypes.BondedPoolName, Permissions: []string{authtypes.Burner, authtypes.Staking}}, + {Account: stakingtypes.NotBondedPoolName, Permissions: []string{authtypes.Burner, authtypes.Staking}}, + {Account: evmstakingtypes.ModuleName, Permissions: []string{authtypes.Burner, authtypes.Minter}}, + {Account: govtypes.ModuleName, Permissions: []string{authtypes.Burner}}, + } + + // appConfig application configuration (used by depinject). + appConfig = appconfig.Compose(&appv1alpha1.Config{ + Modules: []*appv1alpha1.ModuleConfig{ + { + Name: runtime.ModuleName, + Config: appconfig.WrapAny(&runtimev1alpha1.Module{ + AppName: Name, + // NOTE: "PreBlockers" is set in app.go to override the ABCI++ PreBlocker. + BeginBlockers: beginBlockers, + // NOTE: "EndBlockers" is set in app.go since evmstaking endblocker replaces the staking endblocker. + InitGenesis: genesisModuleOrder, + OverrideStoreKeys: []*runtimev1alpha1.StoreKeyConfig{ + { + ModuleName: authtypes.ModuleName, + KvStoreKey: "acc", + }, + }, + }), + }, + { + Name: authtypes.ModuleName, + Config: appconfig.WrapAny(&authmodulev1.Module{ + ModuleAccountPermissions: moduleAccPerms, + Bech32Prefix: Bech32HRP, + }), + }, + { + Name: "tx", + Config: appconfig.WrapAny(&txconfigv1.Config{ + SkipAnteHandler: true, // Disable ante handler (since we don't have proper txs). + SkipPostHandler: true, + }), + }, + { + Name: banktypes.ModuleName, + Config: appconfig.WrapAny(&bankmodulev1.Module{ + BlockedModuleAccountsOverride: blockAccAddrs, + }), + }, + { + Name: consensustypes.ModuleName, + Config: appconfig.WrapAny(&consensusmodulev1.Module{}), + }, + { + Name: distrtypes.ModuleName, + Config: appconfig.WrapAny(&distrmodulev1.Module{}), + }, + { + Name: slashingtypes.ModuleName, + Config: appconfig.WrapAny(&slashingmodulev1.Module{}), + }, + { + Name: genutiltypes.ModuleName, + Config: appconfig.WrapAny(&genutilmodulev1.Module{}), + }, + { + Name: govtypes.ModuleName, + Config: appconfig.WrapAny(&govmodulev1.Module{}), + }, + { + Name: stakingtypes.ModuleName, + Config: appconfig.WrapAny(&stakingmodulev1.Module{}), + }, + { + Name: upgradetypes.ModuleName, + Config: appconfig.WrapAny(&upgrademodulev1.Module{}), + }, + { + Name: evmstakingtypes.ModuleName, + Config: appconfig.WrapAny(&evmstakingmodule.Module{}), + }, + { + Name: evmenginetypes.ModuleName, + Config: appconfig.WrapAny(&evmenginemodule.Module{}), + }, + { + Name: minttypes.ModuleName, + Config: appconfig.WrapAny(&mintmodulev1.Module{}), + }, + }, + }) +) diff --git a/client/app/cmtlog.go b/client/app/cmtlog.go new file mode 100644 index 00000000..0027aed4 --- /dev/null +++ b/client/app/cmtlog.go @@ -0,0 +1,91 @@ +package app + +import ( + "context" + "strings" + + cmtlog "github.com/cometbft/cometbft/libs/log" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +var _ cmtlog.Logger = (*cmtLogger)(nil) + +const ( + levelError = iota + 1 + levelInfo + levelDebug +) + +// levels maps strings to numbers for easy comparison. +// +//nolint:gochecknoglobals // Global is ok here. +var levels = map[string]int{ + "error": levelError, + "info": levelInfo, + "debug": levelDebug, +} + +// dropCometDebugs is a map of cometBFT debug messages that should be dropped. +// These are super noisy and not useful. +// +//nolint:gochecknoglobals // Static mapping +var dropCometDebugs = map[string]bool{ + "Read PacketMsg": true, + "Received bytes": true, + "Send": true, +} + +// cmtLogger implements cmtlog.Logger by using the iliad logging pattern. +// Comet log level is controlled separately in config.toml, since comet logs are very noisy. +type cmtLogger struct { + ctx context.Context //nolint:containedctx // This is a wrapper around the iliad logger which is context based. + level int +} + +func newCmtLogger(ctx context.Context, levelStr string) (cmtlog.Logger, error) { + level, ok := levels[strings.ToLower(levelStr)] + if !ok { + return cmtLogger{}, errors.New("invalid comet log level", "level", levelStr) + } + + return cmtLogger{ + ctx: log.WithSkip(ctx, 4), //nolint:mnd // Skip this logger. + level: level, + }, nil +} + +func (c cmtLogger) Debug(msg string, keyvals ...any) { + if c.level < levelDebug { + return + } else if dropCometDebugs[msg] { + return + } + + log.Debug(c.ctx, msg, keyvals...) +} + +func (c cmtLogger) Info(msg string, keyvals ...any) { + if c.level < levelInfo { + return + } + log.Info(c.ctx, msg, keyvals...) +} + +func (c cmtLogger) Error(msg string, keyvals ...any) { + if c.level < levelError { + return + } + + keyvals, err := splitOutError(keyvals) + + log.Error(c.ctx, msg, err, keyvals...) +} + +func (c cmtLogger) With(keyvals ...any) cmtlog.Logger { //nolint:ireturn // This signature is required by interface. + return cmtLogger{ + ctx: log.WithCtx(c.ctx, keyvals...), + level: c.level, + } +} diff --git a/client/app/keepers/types.go b/client/app/keepers/types.go new file mode 100644 index 00000000..31aeda4f --- /dev/null +++ b/client/app/keepers/types.go @@ -0,0 +1,37 @@ +package keepers + +// Keepers have been moved to a separate package to ensure all keepers are accessible in `upgrades.Upgrade.CreateUpgradeHandler`. +// This allows for passing all keepers into the upgrade handler and accessing/changing blockchain state across all modules. +// When performing `ignite scaffold` the keeper will be added to `app.go`. Please move them here. + +import ( + upgradekeeper "cosmossdk.io/x/upgrade/keeper" + + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + consensuskeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + evmengkeeper "github.com/piplabs/story/client/x/evmengine/keeper" + evmstakingkeeper "github.com/piplabs/story/client/x/evmstaking/keeper" +) + +// Keepers includes all possible keepers. We separated it into a separate struct to make it easier to scaffold upgrades. +type Keepers struct { + // keepers + AccountKeeper authkeeper.AccountKeeper + BankKeeper bankkeeper.Keeper + SlashingKeeper slashingkeeper.Keeper + StakingKeeper *stakingkeeper.Keeper + DistrKeeper distrkeeper.Keeper + ConsensusParamsKeeper consensuskeeper.Keeper + GovKeeper *govkeeper.Keeper + UpgradeKeeper *upgradekeeper.Keeper + + // Iliad + EvmStakingKeeper *evmstakingkeeper.Keeper + EVMEngKeeper *evmengkeeper.Keeper +} diff --git a/client/app/privkey.go b/client/app/privkey.go new file mode 100644 index 00000000..d83310c0 --- /dev/null +++ b/client/app/privkey.go @@ -0,0 +1,100 @@ +package app + +import ( + "os" + + "github.com/cometbft/cometbft/crypto" + cmtjson "github.com/cometbft/cometbft/libs/json" + "github.com/cometbft/cometbft/privval" + "github.com/ethereum/go-ethereum/accounts/keystore" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" +) + +// loadPrivVal returns a privval.FilePV by loading either a CometBFT priv validator key or an Ethereum keystore file. +func loadPrivVal(cfg Config) (*privval.FilePV, error) { + cmtFile := cfg.Comet.PrivValidatorKeyFile() + cmtExists := exists(cmtFile) + + if !cmtExists { + return nil, errors.New("no cometBFT priv validator key file exists", "comet_file", cmtFile) + } + + var key crypto.PrivKey + key, err := loadCometFilePV(cmtFile) + if err != nil { + return nil, err + } + + state, err := loadCometPVState(cfg.Comet.PrivValidatorStateFile()) + if err != nil { + return nil, err + } + + // Create a new privval.FilePV with the loaded key and state. + // This is a workaround for the fact that there is no other way + // to set FilePVLastSignState filePath field. + resp := privval.NewFilePV(key, "", cfg.Comet.PrivValidatorStateFile()) + resp.LastSignState.Step = state.Step + resp.LastSignState.Round = state.Round + resp.LastSignState.Height = state.Height + resp.LastSignState.Signature = state.Signature + resp.LastSignState.SignBytes = state.SignBytes + + return resp, nil +} + +// loadEthKeystore loads an Ethereum keystore file and returns the private key. +// +//nolint:unused //Ignore unused function temporarily +func loadEthKeystore(keystoreFile string, password string) (crypto.PrivKey, error) { + bz, err := os.ReadFile(keystoreFile) + if err != nil { + return nil, errors.Wrap(err, "read keystore file", "path", keystoreFile) + } + + key, err := keystore.DecryptKey(bz, password) + if err != nil { + return nil, errors.Wrap(err, "decrypt keystore file", "path", keystoreFile) + } + + return k1util.StdPrivKeyToComet(key.PrivateKey) +} + +// loadCometFilePV loads a CometBFT privval file and returns the private key. +func loadCometFilePV(file string) (crypto.PrivKey, error) { + bz, err := os.ReadFile(file) + if err != nil { + return nil, errors.Wrap(err, "read comet privval", "path", file) + } + + var pvKey privval.FilePVKey + err = cmtjson.Unmarshal(bz, &pvKey) + if err != nil { + return nil, errors.Wrap(err, "unmarshal comet privval", "path", file) + } + + return pvKey.PrivKey, nil +} + +// loadCometPVState loads a CometBFT privval state file. +func loadCometPVState(file string) (privval.FilePVLastSignState, error) { + bz, err := os.ReadFile(file) + if err != nil { + return privval.FilePVLastSignState{}, errors.Wrap(err, "read comet privval state", "path", file) + } + + var state privval.FilePVLastSignState + err = cmtjson.Unmarshal(bz, &state) + if err != nil { + return privval.FilePVLastSignState{}, errors.Wrap(err, "unmarshal comet privval state", "path", file) + } + + return state, nil +} + +func exists(file string) bool { + _, err := os.Stat(file) + return !os.IsNotExist(err) +} diff --git a/client/app/prouter.go b/client/app/prouter.go new file mode 100644 index 00000000..c5be482f --- /dev/null +++ b/client/app/prouter.go @@ -0,0 +1,68 @@ +package app + +import ( + "context" + "fmt" + + abci "github.com/cometbft/cometbft/abci/types" + cmttypes "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// makeProcessProposalHandler creates a new process proposal handler. +// It ensures all messages included in a cpayload proposal are valid. +// It also updates some external state. +func makeProcessProposalHandler(app *App) sdk.ProcessProposalHandler { + router := baseapp.NewMsgServiceRouter() + router.SetInterfaceRegistry(app.interfaceRegistry) + app.Keepers.EVMEngKeeper.RegisterProposalService(router) // EVMEngine calls NewPayload on proposals to verify it. + + return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + // Ensure the proposal includes quorum vote extensions (unless first block). + if req.Height > 1 { + var totalPower, votedPower int64 + for _, vote := range req.ProposedLastCommit.Votes { + totalPower += vote.Validator.Power + if vote.BlockIdFlag != cmttypes.BlockIDFlagCommit { + continue + } + votedPower += vote.Validator.Power + } + if totalPower*2/3 >= votedPower { + return handleErr(ctx, errors.New("proposed doesn't include quorum votes exttensions")) + } + } + + for _, rawTX := range req.Txs { + tx, err := app.txConfig.TxDecoder()(rawTX) + if err != nil { + return handleErr(ctx, errors.Wrap(err, "decode transaction")) + } + + for _, msg := range tx.GetMsgs() { + handler := router.Handler(msg) + if handler == nil { + return handleErr(ctx, errors.New("msg handler not found", + "msg_type", fmt.Sprintf("%T", msg), + )) + } + + _, err := handler(ctx, msg) + if err != nil { + return handleErr(ctx, errors.Wrap(err, "execute message")) + } + } + } + + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil + } +} + +func handleErr(ctx context.Context, err error) (*abci.ResponseProcessProposal, error) { + log.Error(ctx, "Rejecting failed process proposal", err) + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil +} diff --git a/client/app/rollback.go b/client/app/rollback.go new file mode 100644 index 00000000..60fc1961 --- /dev/null +++ b/client/app/rollback.go @@ -0,0 +1,76 @@ +package app + +import ( + "context" + "fmt" + "math/big" + + "cosmossdk.io/store" + storemetrics "cosmossdk.io/store/metrics" + + cmtcmd "github.com/cometbft/cometbft/cmd/cometbft/commands" + dbm "github.com/cosmos/cosmos-db" + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +func Rollback(ctx context.Context, cfg Config, removeBlock bool) error { + engineCl, err := newEngineClient(ctx, cfg, cfg.Network, nil) + if err != nil { + return err + } + + latestHeigth, err := engineCl.BlockNumber(ctx) + if err != nil { + return err + } + + latestBlock, err := engineCl.BlockByNumber(ctx, big.NewInt(int64(latestHeigth))) + if err != nil { + return err + } else if latestBlock.BeaconRoot() == nil { + return errors.New("cannot rollback EVM with nil beacon root", "height", latestHeigth) + } + + db, err := dbm.NewDB("application", cfg.BackendType(), cfg.DataDir()) + if err != nil { + return errors.Wrap(err, "create db") + } + + // Rollback CometBFT state + height, hash, err := cmtcmd.RollbackState(&cfg.Comet, removeBlock) + if err != nil { + return errors.Wrap(err, "rollback comet state") + } + + // Rollback the multistore + cms := store.NewCommitMultiStore(db, newSDKLogger(ctx), storemetrics.NewNoOpMetrics()) + if err := cms.RollbackToVersion(height); err != nil { + return errors.Wrap(err, "rollback to height") + } + + log.Info(ctx, "Rolled back consensus state", "height", height, "hash", fmt.Sprintf("%X", hash)) + + // Rollback EVM if latest EVM block built on-top of new rolled-back consensus head. + if *latestBlock.BeaconRoot() != common.BytesToHash(hash) { + return errors.New("cannot rollback EVM, latest EVM block not built on new rolled-back state", + "evm_height", latestHeigth, + "evm_beacon_root", *latestBlock.BeaconRoot(), + ) + } + + if err := engineCl.SetHead(ctx, latestHeigth-1); err != nil { + return errors.Wrap(err, "set head") + } + + rolledBackBlock, err := engineCl.BlockByNumber(ctx, big.NewInt(int64(latestHeigth-1))) + if err != nil { + return err + } + + log.Info(ctx, "Rolled back execution state", "height", rolledBackBlock.Number(), "hash", rolledBackBlock.Hash()) + + return nil +} diff --git a/client/app/sdklog.go b/client/app/sdklog.go new file mode 100644 index 00000000..687ce342 --- /dev/null +++ b/client/app/sdklog.go @@ -0,0 +1,81 @@ +package app + +import ( + "context" + + sdklog "cosmossdk.io/log" + + "github.com/piplabs/story/lib/log" +) + +var _ sdklog.Logger = sdkLogger{} + +// dropCosmosDebugs is a map of cosmosSDK debug messages that should be dropped. +// These are super noisy and not useful. +// +//nolint:gochecknoglobals // Static mapping +var dropCosmosDebugs = map[string]bool{ + "recursiveRemove": true, + "BATCH SAVE": true, + "SAVE TREE": true, +} + +// sdkLogger implements sdklog.Logger by using the iliad logging pattern. +// Comet log level is controlled separately in config.toml, since comet logs are very noisy. +type sdkLogger struct { + ctx context.Context //nolint:containedctx // This is a wrapper around the iliad logger which is context based. +} + +func newSDKLogger(ctx context.Context) sdkLogger { + return sdkLogger{ + ctx: log.WithSkip(ctx, 4), //nolint:mnd // Skip this logger. + } +} + +func (c sdkLogger) Debug(msg string, keyvals ...any) { + if dropCosmosDebugs[msg] { + return + } + log.Debug(c.ctx, msg, keyvals...) +} + +func (c sdkLogger) Info(msg string, keyvals ...any) { + log.Info(c.ctx, msg, keyvals...) +} + +func (c sdkLogger) Warn(msg string, keyvals ...any) { + keyvals, err := splitOutError(keyvals) + + log.Warn(c.ctx, msg, err, keyvals...) +} + +func (c sdkLogger) Error(msg string, keyvals ...any) { + keyvals, err := splitOutError(keyvals) + + log.Error(c.ctx, msg, err, keyvals...) +} + +func (c sdkLogger) With(keyVals ...any) sdklog.Logger { + return sdkLogger{ + ctx: log.WithCtx(c.ctx, keyVals...), + } +} + +func (c sdkLogger) Impl() any { + return c +} + +// splitOutError splits the keyvals into a slice of keyvals without the error and the error. +func splitOutError(keyvals []any) ([]any, error) { + var remaining []any + var err error + for i := 0; i < len(keyvals); i += 2 { + if keyErr, ok := keyvals[i+1].(error); ok { + err = keyErr + } else { + remaining = append(remaining, keyvals[i], keyvals[i+1]) + } + } + + return remaining, err +} diff --git a/client/app/start.go b/client/app/start.go new file mode 100644 index 00000000..095dfa8f --- /dev/null +++ b/client/app/start.go @@ -0,0 +1,312 @@ +package app + +import ( + "context" + "time" + + "cosmossdk.io/store" + pruningtypes "cosmossdk.io/store/pruning/types" + "cosmossdk.io/store/snapshots" + snapshottypes "cosmossdk.io/store/snapshots/types" + storetypes "cosmossdk.io/store/types" + + cmtcfg "github.com/cometbft/cometbft/config" + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/node" + "github.com/cometbft/cometbft/p2p" + "github.com/cometbft/cometbft/proxy" + rpclocal "github.com/cometbft/cometbft/rpc/client/local" + cmttypes "github.com/cometbft/cometbft/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/server" + sdktelemetry "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/cosmos/cosmos-sdk/types/mempool" + + "github.com/piplabs/story/client/comet" + iliadcfg "github.com/piplabs/story/client/config" + apisvr "github.com/piplabs/story/client/server" + "github.com/piplabs/story/lib/buildinfo" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tracer" +) + +// Config wraps the iliad (app) and comet (client) configurations. +type Config struct { + iliadcfg.Config + Comet cmtcfg.Config +} + +// BackendType returns the iliad config backend type +// or the comet backend type otherwise. +func (c Config) BackendType() dbm.BackendType { + if c.Config.BackendType == "" { + return dbm.BackendType(c.Comet.DBBackend) + } + + return dbm.BackendType(c.Config.BackendType) +} + +// Run runs the iliad client until the context is canceled. +// +//nolint:contextcheck // Explicit new stop context. +func Run(ctx context.Context, cfg Config) error { + stopFunc, err := Start(ctx, cfg) + if err != nil { + return err + } + + <-ctx.Done() + log.Info(ctx, "Shutdown detected, stopping...") + + //nolint:mnd // Use a fresh context for stopping (only allow 5 seconds). + stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return stopFunc(stopCtx) +} + +// Start starts the iliad client returning a stop function or an error. +// +// Note that the original context used to start the app must be canceled first +// before calling the stop function and a fresh context should be passed into the stop function. +func Start(ctx context.Context, cfg Config) (func(context.Context) error, error) { + log.Info(ctx, "Starting iliad consensus client") + + if err := cfg.Verify(); err != nil { + return nil, errors.Wrap(err, "verify iliad config") + } + + buildinfo.Instrument(ctx) + + tracerIDs := tracer.Identifiers{Network: cfg.Network, Service: "iliad", Instance: cfg.Comet.Moniker} + stopTracer, err := tracer.Init(ctx, tracerIDs, cfg.Tracer) + if err != nil { + return nil, err + } + + if err := enableSDKTelemetry(); err != nil { + return nil, errors.Wrap(err, "enable cosmos-sdk telemetry") + } + + privVal, err := loadPrivVal(cfg) + if err != nil { + return nil, errors.Wrap(err, "load validator key") + } + + db, err := dbm.NewDB("application", cfg.BackendType(), cfg.DataDir()) + if err != nil { + return nil, errors.Wrap(err, "create db") + } + + baseAppOpts, err := makeBaseAppOpts(cfg) + if err != nil { + return nil, errors.Wrap(err, "make base app opts") + } + + engineCl, err := newEngineClient(ctx, cfg, cfg.Network, privVal.Key.PubKey) + if err != nil { + return nil, err + } + + //nolint:contextcheck // False positive + app, err := newApp( + newSDKLogger(ctx), + db, + engineCl, + baseAppOpts..., + ) + if err != nil { + return nil, errors.Wrap(err, "create app") + } + app.Keepers.EVMEngKeeper.SetBuildDelay(cfg.EVMBuildDelay) + app.Keepers.EVMEngKeeper.SetBuildOptimistic(cfg.EVMBuildOptimistic) + + addr, err := k1util.PubKeyToAddress(privVal.Key.PrivKey.PubKey()) + if err != nil { + return nil, errors.Wrap(err, "convert validator pubkey to address") + } + app.Keepers.EVMEngKeeper.SetValidatorAddress(addr) + + cmtNode, err := newCometNode(ctx, &cfg.Comet, app, privVal) + if err != nil { + return nil, errors.Wrap(err, "create comet node") + } + + rpcClient := rpclocal.New(cmtNode) + cmtAPI := comet.NewAPI(rpcClient) + app.SetCometAPI(cmtAPI) + + log.Info(ctx, "Starting CometBFT", "listeners", cmtNode.Listeners()) + + if err := cmtNode.Start(); err != nil { + return nil, errors.Wrap(err, "start comet node") + } + + var apiSvr *apisvr.Server + if cfg.APIEnable { + log.Info(ctx, "Starting API server", + "api_address", cfg.APIAddress, + "enable_unsafe_cors", cfg.EnableUnsafeCORS) + apiSvr, err = apisvr.NewServer(app, rpcClient, cfg.APIAddress, cfg.EnableUnsafeCORS) + if err != nil { + return nil, errors.Wrap(err, "create API server") + } + if err := apiSvr.Start(); err != nil { + return nil, errors.Wrap(err, "start API server") + } + } + + // Return the stop function. + // Note that the original context used to start the app must be canceled first. + // And a fresh context should be passed into the stop function. + return func(ctx context.Context) error { + if err := cmtNode.Stop(); err != nil { + return errors.Wrap(err, "stop comet node") + } + cmtNode.Wait() + + // Note that cometBFT doesn't shut down cleanly. It leaves a bunch of goroutines running... + + if cfg.APIEnable { + if err := apiSvr.Stop(ctx); err != nil { + return errors.Wrap(err, "stop API server") + } + } + + if err := stopTracer(ctx); err != nil { + return errors.Wrap(err, "stop tracer") + } + + return nil + }, nil +} + +func newCometNode(ctx context.Context, cfg *cmtcfg.Config, app *App, privVal cmttypes.PrivValidator, +) (*node.Node, error) { + nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) + if err != nil { + return nil, errors.Wrap(err, "load or gen node key", "key_file", cfg.NodeKeyFile()) + } + + cmtLog, err := newCmtLogger(ctx, cfg.LogLevel) + if err != nil { + return nil, err + } + + wrapper := newABCIWrapper( + server.NewCometABCIWrapper(app), + app.Keepers.EVMEngKeeper.PostFinalize, + func() storetypes.CacheMultiStore { + return app.CommitMultiStore().CacheMultiStore() + }, + ) + + cmtNode, err := node.NewNode(cfg, + privVal, + nodeKey, + proxy.NewLocalClientCreator(wrapper), + node.DefaultGenesisDocProviderFunc(cfg), + cmtcfg.DefaultDBProvider, + node.DefaultMetricsProvider(cfg.Instrumentation), + cmtLog, + ) + if err != nil { + return nil, errors.Wrap(err, "create node") + } + + return cmtNode, nil +} + +func makeBaseAppOpts(cfg Config) ([]func(*baseapp.BaseApp), error) { + chainID, err := chainIDFromGenesis(cfg) + if err != nil { + return nil, err + } + + snapshotStore, err := newSnapshotStore(cfg) + if err != nil { + return nil, err + } + + snapshotOptions := snapshottypes.NewSnapshotOptions(cfg.SnapshotInterval, uint32(cfg.SnapshotKeepRecent)) + + pruneOpts := pruningtypes.NewPruningOptionsFromString(cfg.PruningOption) + if cfg.PruningOption == pruningtypes.PruningOptionDefault { + // Override the default cosmosSDK pruning values with much more aggressive defaults + // since historical state isn't very important for most use-cases. + pruneOpts = pruningtypes.NewCustomPruningOptions(defaultPruningKeep, defaultPruningInterval) + } + + return []func(*baseapp.BaseApp){ + // baseapp.SetOptimisticExecution(), // TODO: Enable this. + baseapp.SetChainID(chainID), + baseapp.SetMinRetainBlocks(cfg.MinRetainBlocks), + baseapp.SetPruning(pruneOpts), + baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()), + baseapp.SetSnapshot(snapshotStore, snapshotOptions), + baseapp.SetMempool(mempool.NoOpMempool{}), + }, nil +} + +func newSnapshotStore(cfg Config) (*snapshots.Store, error) { + db, err := dbm.NewDB("metadata", cfg.BackendType(), cfg.SnapshotDir()) + if err != nil { + return nil, errors.Wrap(err, "create snapshot db") + } + + ss, err := snapshots.NewStore(db, cfg.SnapshotDir()) + if err != nil { + return nil, errors.Wrap(err, "create snapshot store") + } + + return ss, nil +} + +func chainIDFromGenesis(cfg Config) (string, error) { + genDoc, err := node.DefaultGenesisDocProviderFunc(&cfg.Comet)() + if err != nil { + return "", errors.Wrap(err, "load genesis doc") + } + + return genDoc.ChainID, nil +} + +// newEngineClient returns a new engine API client. +func newEngineClient(ctx context.Context, cfg Config, network netconf.ID, pubkey crypto.PubKey) (ethclient.EngineClient, error) { + if network == netconf.Simnet { + return ethclient.NewEngineMock(ethclient.WithMockSelfDelegation(pubkey, 1)) + } + + jwtBytes, err := ethclient.LoadJWTHexFile(cfg.EngineJWTFile) + if err != nil { + return nil, errors.Wrap(err, "load engine JWT file") + } + + engineCl, err := ethclient.NewAuthClient(ctx, cfg.EngineEndpoint, jwtBytes) + if err != nil { + return nil, errors.Wrap(err, "create engine client") + } + + return engineCl, nil +} + +// enableSDKTelemetry enables prometheus based cosmos-sdk telemetry. +func enableSDKTelemetry() error { + const farFuture = time.Hour * 24 * 365 * 10 // 10 years ~= infinity. + + _, err := sdktelemetry.New(sdktelemetry.Config{ + ServiceName: "cosmos", + Enabled: true, + PrometheusRetentionTime: int64(farFuture.Seconds()), // Prometheus metrics never expire once created in-app. + }) + if err != nil { + return errors.Wrap(err, "enable cosmos-sdk telemetry") + } + + return nil +} diff --git a/client/app/upgrades.go b/client/app/upgrades.go new file mode 100644 index 00000000..e1bb1088 --- /dev/null +++ b/client/app/upgrades.go @@ -0,0 +1,79 @@ +package app + +import ( + "fmt" + + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/client/app/upgrades" +) + +var ( + // `Upgrades` defines the upgrade handlers and store loaders for the application. + // New upgrades should be added to this slice after they are implemented. + Upgrades = []upgrades.Upgrade{} + // Forks are for hard forks that breaks backward compatibility. + Forks = []upgrades.Fork{} +) + +func (a *App) setupUpgradeHandlers() { + for _, upgrade := range Upgrades { + a.Keepers.UpgradeKeeper.SetUpgradeHandler( + upgrade.UpgradeName, + upgrade.CreateUpgradeHandler(a.ModuleManager, a.Configurator(), &a.Keepers), + ) + } +} + +// setUpgradeStoreLoaders sets custom store loaders to customize the rootMultiStore initialization for software upgrades. +func (a *App) setupUpgradeStoreLoaders() { + upgradeInfo, err := a.Keepers.UpgradeKeeper.ReadUpgradeInfoFromDisk() + if err != nil { + panic(fmt.Sprintf("failed to read upgrade info from disk %s", err)) + } + + if a.Keepers.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + return + } + + for _, upgrade := range Upgrades { + if upgradeInfo.Name == upgrade.UpgradeName { + a.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &upgrade.StoreUpgrades)) + } + } +} + +// ScheduleForkUpgrade executes any necessary fork logic for based upon the current block height. It sets an upgrade +// plan once the chain reaches the pre-defined upgrade height. +// +// CONTRACT: for this logic to work properly it is required to: +// 1. Release a non-breaking patch version so that the chain can set the scheduled upgrade plan at upgrade-height. +// 2. Release the software defined in the upgrade-info. +func (a *App) scheduleForkUpgrade(ctx sdk.Context) { + currentBlockHeight := ctx.BlockHeight() + for _, fork := range Forks { + if currentBlockHeight == fork.UpgradeHeight { + upgradePlan := upgradetypes.Plan{ + Height: currentBlockHeight, + Name: fork.UpgradeName, + Info: fork.UpgradeInfo, + } + + // schedule the upgrade plan to the current block height, effectively performing + // a hard fork that uses the upgrade handler to manage the migration. + if err := a.Keepers.UpgradeKeeper.ScheduleUpgrade(ctx, upgradePlan); err != nil { + panic( + //nolint:errorlint // use "%v" to obfuscate the underlying error + fmt.Errorf( + "hard fork: failed to schedule upgrade %s during BeginBlock at height %d: %v", + upgradePlan.Name, + ctx.BlockHeight(), + err, + ), + ) + } + } + } +} diff --git a/client/app/upgrades/historical.go b/client/app/upgrades/historical.go new file mode 100644 index 00000000..923692d9 --- /dev/null +++ b/client/app/upgrades/historical.go @@ -0,0 +1,92 @@ +package upgrades + +// This file is intended to keep old, historical upgrades in one place. It is advised to keep the future upgrades in the +// separate file, and then move them to `historical.go` after a successful upgrade so the new nodes can still sync from +// the genesis. + +// TODO_CONSIDERATION: after we verify `State Sync` is fully functional, we can hypothetically remove old upgrades from +// the codebase, as the nodes won't have to execute upgrades and will download the "snapshot" instead. Some other +// blockchain networks do that (such as `evmos`: https://github.com/evmos/evmos/tree/main/app/upgrades). +// Note that this may inhibit a full state sync from genesis. + +import ( + "context" + + storetypes "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/cosmos/cosmos-sdk/types/module" + consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + + "github.com/piplabs/story/client/app/keepers" + "github.com/piplabs/story/lib/errors" +) + +// defaultUpgradeHandler should be used for upgrades that only update the `ConsensusVersion`. +// If an upgrade involves state changes, parameter updates, data migrations, authz authorisation, etc, +// a new version-specific upgrade handler must be created. +func defaultUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + _ *keepers.Keepers, +) upgradetypes.UpgradeHandler { + return func(ctx context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + return mm.RunMigrations(ctx, configurator, vm) + } +} + +// An example of an upgrade that uses the default upgrade handler and also performs additional state changes. +// For example, even if `ConsensusVersion` is not modified for any modules, it still might be beneficial to create +// an upgrade so node runners are signaled to start utilizing `Cosmovisor` for new binaries. +var UpgradeExample = Upgrade{ + UpgradeName: "v0.0.0-Example", + CreateUpgradeHandler: defaultUpgradeHandler, + + // We can also add, rename and delete KVStores. + // More info in cosmos-sdk docs: https://docs.cosmos.network/v0.50/learn/advanced/upgrade#add-storeupgrades-for-new-modules + StoreUpgrades: storetypes.StoreUpgrades{ + // Added: []string{"newmodule"}, + }, +} + +// Upgrade0_0_4 is an example of an upgrade that increases the block size. +// This example demonstrates how to change the block size using an upgrade. +var Upgrade0_0_4 = Upgrade{ + UpgradeName: "v0.0.4", + CreateUpgradeHandler: func( + mm *module.Manager, + configurator module.Configurator, + keepers *keepers.Keepers, + ) upgradetypes.UpgradeHandler { + return func(ctx context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + // Get current consensus module parameters + currentParams, err := keepers.ConsensusParamsKeeper.ParamsStore.Get(ctx) + if err != nil { + return vm, errors.Wrap(err, "failed to get consensus params") + } + + // Supply all params even when changing just one, as `ToProtoConsensusParams` requires them to be present. + newParams := consensusparamtypes.MsgUpdateParams{ + Authority: keepers.ConsensusParamsKeeper.GetAuthority(), + Block: currentParams.Block, + Evidence: currentParams.Evidence, + Validator: currentParams.Validator, + + // This seems to be deprecated/not needed, but it's fine as we're copying the existing data. + Abci: currentParams.Abci, + } + + // Increase block size two-fold, 22020096 is the default value. + newParams.Block.MaxBytes = 22020096 * 2 + + // Update the chain state + if _, err = keepers.ConsensusParamsKeeper.UpdateParams(ctx, &newParams); err != nil { + return vm, errors.Wrap(err, "failed to update consensus params") + } + + return mm.RunMigrations(ctx, configurator, vm) + } + }, + // No changes to the KVStore in this upgrade. + StoreUpgrades: storetypes.StoreUpgrades{}, +} diff --git a/client/app/upgrades/types.go b/client/app/upgrades/types.go new file mode 100644 index 00000000..93684a1d --- /dev/null +++ b/client/app/upgrades/types.go @@ -0,0 +1,39 @@ +package upgrades + +import ( + store "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/piplabs/story/client/app/keepers" +) + +// Upgrade defines a struct containing necessary fields that a MsgSoftwareUpgrade +// must have written, in order for the state migration to go smoothly. +// An upgrade must implement this struct, and then set it in the app.go. +// The app.go will then define the handler. +type Upgrade struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + + // CreateUpgradeHandler defines the function that creates an upgrade handler + CreateUpgradeHandler func(*module.Manager, module.Configurator, *keepers.Keepers) upgradetypes.UpgradeHandler + + // Store upgrades, should be used for any new modules introduced, new modules deleted, or store names renamed. + StoreUpgrades store.StoreUpgrades +} + +// Fork defines a struct containing the requisite fields for a non-software upgrade proposal +// Hard Fork at a given height to implement. +type Fork struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + // Height the upgrade occurs at. + UpgradeHeight int64 + // Upgrade info for this fork. + UpgradeInfo string + // Function that runs some custom state transition code at the beginning of a fork. + BeginForkLogic func(ctx sdk.Context, keepers *keepers.Keepers) +} diff --git a/client/cmd/abi/IPTokenStaking.abi.json b/client/cmd/abi/IPTokenStaking.abi.json new file mode 100644 index 00000000..ce489f2d --- /dev/null +++ b/client/cmd/abi/IPTokenStaking.abi.json @@ -0,0 +1,440 @@ +[ + { + "type": "function", + "name": "addOperator", + "inputs": [ + { + "name": "uncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "createValidator", + "inputs": [ + { + "name": "validatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "moniker", + "type": "string", + "internalType": "string" + }, + { + "name": "commissionRate", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "maxCommissionRate", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "maxCommissionChangeRate", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "createValidatorOnBehalf", + "inputs": [ + { + "name": "validatorPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "getOperators", + "inputs": [ + { + "name": "pubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "redelegate", + "inputs": [ + { + "name": "p", + "type": "tuple", + "internalType": "struct IIPTokenStaking.RedelegateParams", + "components": [ + { + "name": "delegatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorSrcPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorDstPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "redelegateOnBehalf", + "inputs": [ + { + "name": "p", + "type": "tuple", + "internalType": "struct IIPTokenStaking.RedelegateParams", + "components": [ + { + "name": "delegatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorSrcPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorDstPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeOperator", + "inputs": [ + { + "name": "uncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "roundedStakeAmount", + "inputs": [ + { + "name": "rawAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "remainder", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setWithdrawalAddress", + "inputs": [ + { + "name": "delegatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "newWithdrawalAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "stake", + "inputs": [ + { + "name": "delegatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "stakeOnBehalf", + "inputs": [ + { + "name": "delegatorPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "unstake", + "inputs": [ + { + "name": "delegatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unstakeOnBehalf", + "inputs": [ + { + "name": "delegatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "CreateValidator", + "inputs": [ + { + "name": "validatorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "moniker", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "stakeAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "commissionRate", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + }, + { + "name": "maxCommissionRate", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + }, + { + "name": "maxCommissionChangeRate", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Deposit", + "inputs": [ + { + "name": "depositorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "validatorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Redelegate", + "inputs": [ + { + "name": "depositorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "validatorSrcPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "validatorDstPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetWithdrawalAddress", + "inputs": [ + { + "name": "depositorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "executionAddress", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Withdraw", + "inputs": [ + { + "name": "depositorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "validatorPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/client/cmd/cmd.go b/client/cmd/cmd.go new file mode 100644 index 00000000..6208103e --- /dev/null +++ b/client/cmd/cmd.go @@ -0,0 +1,61 @@ +// Package cmd provides the cli for running the iliad consensus client. +package cmd + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/piplabs/story/client/app" + iliadcfg "github.com/piplabs/story/client/config" + "github.com/piplabs/story/lib/buildinfo" + libcmd "github.com/piplabs/story/lib/cmd" + "github.com/piplabs/story/lib/log" +) + +// New returns a new root cobra command that handles our command line tool. +func New() *cobra.Command { + return libcmd.NewRootCmd( + "iliad", + "Iliad is a consensus client implementation for the Story L1 blockchain", + newRunCmd("run", app.Run), + newInitCmd(), + buildinfo.NewVersionCmd(), + newValidatorCmds(), + ) +} + +// newRunCmd returns a new cobra command that runs the iliad consensus client. +func newRunCmd(name string, runFunc func(context.Context, app.Config) error) *cobra.Command { + iliadCfg := iliadcfg.DefaultConfig() + logCfg := log.DefaultConfig() + + cmd := &cobra.Command{ + Use: name, + Short: "Runs the iliad consensus client", + RunE: func(cmd *cobra.Command, _ []string) error { + ctx, err := log.Init(cmd.Context(), logCfg) + if err != nil { + return err + } + if err := libcmd.LogFlags(ctx, cmd.Flags()); err != nil { + return err + } + + cometCfg, err := parseCometConfig(ctx, iliadCfg.HomeDir) + if err != nil { + return err + } + + return runFunc(ctx, app.Config{ + Config: iliadCfg, + Comet: cometCfg, + }) + }, + } + + bindRunFlags(cmd, &iliadCfg) + log.BindFlags(cmd.Flags(), &logCfg) + + return cmd +} diff --git a/client/cmd/cometconfig.go b/client/cmd/cometconfig.go new file mode 100644 index 00000000..5ce3a271 --- /dev/null +++ b/client/cmd/cometconfig.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "context" + "path/filepath" + "time" + + cfg "github.com/cometbft/cometbft/config" + "github.com/spf13/viper" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +//nolint:gochecknoglobals // Overrides cometbft default moniker for testing. +var testMoniker string + +// DefaultCometConfig returns the default cometBFT config. +func DefaultCometConfig(homeDir string) cfg.Config { + conf := cfg.DefaultConfig() + + if testMoniker != "" { + conf.Moniker = testMoniker + } + + conf.RootDir = homeDir + conf.SetRoot(conf.RootDir) + conf.LogLevel = "error" // Decrease default comet log level, it is super noisy. + conf.TxIndex = &cfg.TxIndexConfig{Indexer: "null"} // Disable tx indexing. + conf.StateSync.DiscoveryTime = time.Second * 10 // Increase discovery time + conf.StateSync.ChunkRequestTimeout = time.Minute // Increase timeout + conf.Mempool.Type = cfg.MempoolTypeNop // Disable cometBFT mempool + + return *conf +} + +// parseCometConfig parses the cometBFT config from disk and verifies it. +func parseCometConfig(ctx context.Context, homeDir string) (cfg.Config, error) { + const ( + file = "config" // CometBFT config files are named config.toml + dir = "config" // CometBFT config files are stored in the config directory + ) + + v := viper.New() + v.SetConfigName(file) + v.AddConfigPath(filepath.Join(homeDir, dir)) + + // Attempt to read the cometBFT config file, gracefully ignoring errors + // caused by a config file not being found. Return an error + // if we cannot parse the config file. + if err := v.ReadInConfig(); err != nil { + // It's okay if there isn't a config file + var cfgError viper.ConfigFileNotFoundError + if ok := errors.As(err, &cfgError); !ok { + return cfg.Config{}, errors.Wrap(err, "read comet config") + } + + log.Warn(ctx, "No comet config.toml file found, using default config", nil) + } + + conf := DefaultCometConfig(homeDir) + + if err := v.Unmarshal(&conf); err != nil { + return cfg.Config{}, errors.Wrap(err, "unmarshal comet config") + } + + if err := conf.ValidateBasic(); err != nil { + return cfg.Config{}, errors.Wrap(err, "validate comet config") + } + + if warnings := conf.CheckDeprecated(); len(warnings) > 0 { + for _, warning := range warnings { + log.Info(ctx, "Deprecated CometBFT config", "usage", warning) + } + } + + return conf, nil +} diff --git a/client/cmd/flags.go b/client/cmd/flags.go new file mode 100644 index 00000000..886356fb --- /dev/null +++ b/client/cmd/flags.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + iliadcfg "github.com/piplabs/story/client/config" + libcmd "github.com/piplabs/story/lib/cmd" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tracer" +) + +func bindRunFlags(cmd *cobra.Command, cfg *iliadcfg.Config) { + flags := cmd.Flags() + + libcmd.BindHomeFlag(flags, &cfg.HomeDir) + tracer.BindFlags(flags, &cfg.Tracer) + netconf.BindFlag(flags, &cfg.Network) + flags.StringVar(&cfg.EngineEndpoint, "engine-endpoint", cfg.EngineEndpoint, "An EVM execution client Engine API http endpoint") + flags.StringVar(&cfg.EngineJWTFile, "engine-jwt-file", cfg.EngineJWTFile, "The path to the Engine API JWT file") + flags.Uint64Var(&cfg.SnapshotInterval, "snapshot-interval", cfg.SnapshotInterval, "State sync snapshot interval") + flags.Uint64Var(&cfg.SnapshotKeepRecent, "snapshot-keep-recent", cfg.SnapshotKeepRecent, "State sync snapshot to keep") + flags.Uint64Var(&cfg.MinRetainBlocks, "min-retain-blocks", cfg.MinRetainBlocks, "Minimum block height offset during ABCI commit to prune CometBFT blocks") + flags.StringVar(&cfg.BackendType, "app-db-backend", cfg.BackendType, "The type of database for application and snapshots databases") + flags.StringVar(&cfg.PruningOption, "pruning", cfg.PruningOption, "Pruning strategy (default|nothing|everything)") + flags.DurationVar(&cfg.EVMBuildDelay, "evm-build-delay", cfg.EVMBuildDelay, "Minimum delay between triggering and fetching a EVM payload build") + flags.BoolVar(&cfg.EVMBuildOptimistic, "evm-build-optimistic", cfg.EVMBuildOptimistic, "Enables optimistic building of EVM payloads on previous block finalize") + flags.BoolVar(&cfg.APIEnable, "api-enable", cfg.APIEnable, "Define if the API server should be enabled") + flags.StringVar(&cfg.APIAddress, "api-address", cfg.APIAddress, "The API server address to listen on") + flags.BoolVar(&cfg.EnableUnsafeCORS, "enabled-unsafe-cors", cfg.EnableUnsafeCORS, "Enable unsafe CORS for API server") +} + +func bindInitFlags(flags *pflag.FlagSet, cfg *InitConfig) { + libcmd.BindHomeFlag(flags, &cfg.HomeDir) + netconf.BindFlag(flags, &cfg.Network) + flags.BoolVar(&cfg.TrustedSync, "trusted-sync", cfg.TrustedSync, "Initialize trusted state-sync height and hash by querying the Iliad RPC") + flags.BoolVar(&cfg.Force, "force", cfg.Force, "Force initialization (overwrite existing files)") + flags.BoolVar(&cfg.Clean, "clean", cfg.Clean, "Delete home directory before initialization") +} diff --git a/client/cmd/genesis.go b/client/cmd/genesis.go new file mode 100644 index 00000000..0eb7ada4 --- /dev/null +++ b/client/cmd/genesis.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/types" + cmttime "github.com/cometbft/cometbft/types/time" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/client/genutil" + "github.com/piplabs/story/lib/netconf" +) + +func MakeGenesis(network netconf.ID, valPubKeys ...crypto.PubKey) (*types.GenesisDoc, error) { + power := cosmostypes.DefaultPowerReduction // Use any non-zero power for this single validator. + + cometVals := make([]types.GenesisValidator, 0, len(valPubKeys)) + for _, pubkey := range valPubKeys { + cometVals = append(cometVals, types.GenesisValidator{ + Address: pubkey.Address(), + PubKey: pubkey, + Power: power.Int64(), + }) + } + + return &types.GenesisDoc{ + ChainID: network.Static().IliadConsensusChainIDStr(), + GenesisTime: cmttime.Now(), + ConsensusParams: genutil.DefaultConsensusParams(), + Validators: cometVals, + AppState: []byte("{}"), + }, nil +} diff --git a/client/cmd/init.go b/client/cmd/init.go new file mode 100644 index 00000000..daa8eaf8 --- /dev/null +++ b/client/cmd/init.go @@ -0,0 +1,329 @@ +package cmd + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + cmtconfig "github.com/cometbft/cometbft/config" + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + cmtos "github.com/cometbft/cometbft/libs/os" + "github.com/cometbft/cometbft/p2p" + "github.com/cometbft/cometbft/privval" + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + + iliadcfg "github.com/piplabs/story/client/config" + "github.com/piplabs/story/client/genutil" + libcmd "github.com/piplabs/story/lib/cmd" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" +) + +// InitConfig is the config for the init command. +type InitConfig struct { + HomeDir string + Network netconf.ID + TrustedSync bool + Force bool + Clean bool + Cosmos bool + ExecutionHash common.Hash +} + +// newInitCmd returns a new cobra command that initializes the files and folders required by iliad. +func newInitCmd() *cobra.Command { + // Default config flags + cfg := InitConfig{ + HomeDir: iliadcfg.DefaultHomeDir(), + Force: false, + } + + cmd := &cobra.Command{ + Use: "init", + Short: "Initializes required iliad files and directories", + Long: `Initializes required iliad files and directories. + +Ensures all the following files and directories exist: + / # Iliad home directory + ā”œā”€ā”€ config # Config directory + ā”‚ ā”œā”€ā”€ config.toml # CometBFT configuration + ā”‚ ā”œā”€ā”€ genesis.json # Iliad chain genesis file + ā”‚ ā”œā”€ā”€ iliad.toml # Iliad configuration + ā”‚ ā”œā”€ā”€ node_key.json # Node P2P identity key + ā”‚ ā””ā”€ā”€ priv_validator_key.json # CometBFT private validator key (back this up and keep it safe) + ā”œā”€ā”€ data # Data directory + ā”‚ ā”œā”€ā”€ snapshots # Snapshot directory + ā”‚ ā”œā”€ā”€ priv_validator_state.json # CometBFT private validator state (slashing protection) + +Existing files are not overwritten, unless --clean is specified. +The home directory should only contain subdirectories, no files, use --force to ignore this check. +`, + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + if err := libcmd.LogFlags(ctx, cmd.Flags()); err != nil { + return err + } + + return InitFiles(cmd.Context(), cfg) + }, + } + + bindInitFlags(cmd.Flags(), &cfg) + + return cmd +} + +// InitFiles initializes the files and folders required by iliad. +// It ensures a network and genesis file is generated/downloaded for the provided network. +// +//nolint:gocognit,nestif // This is just many sequential steps. +func InitFiles(ctx context.Context, initCfg InitConfig) error { + if initCfg.Network == "" { + return errors.New("required flag --network empty") + } + + log.Info(ctx, "Initializing iliad files and directories") + homeDir := initCfg.HomeDir + network := initCfg.Network + + if err := prepareHomeDirectory(ctx, initCfg, homeDir); err != nil { + return err + } + + // Initialize default configs. + comet := DefaultCometConfig(homeDir) + + var cfg iliadcfg.Config + + switch { + case network == netconf.Iliad: + cfg = iliadcfg.IliadConfig + case network == netconf.Local: + cfg = iliadcfg.LocalConfig + default: + cfg = iliadcfg.DefaultConfig() + cfg.HomeDir = homeDir + cfg.Network = network + } + + // Folders + folders := []struct { + Name string + Path string + }{ + {"home", homeDir}, + {"data", filepath.Join(homeDir, cmtconfig.DefaultDataDir)}, + {"config", filepath.Join(homeDir, cmtconfig.DefaultConfigDir)}, + {"comet db", comet.DBDir()}, + {"snapshot", cfg.SnapshotDir()}, + {"app db", cfg.AppStateDir()}, + } + for _, folder := range folders { + if cmtos.FileExists(folder.Path) { + // Dir exists, just skip + continue + } else if err := cmtos.EnsureDir(folder.Path, 0o755); err != nil { + return errors.Wrap(err, "create folder") + } + log.Info(ctx, "Generated folder", "reason", folder.Name, "path", folder.Path) + } + + // Add P2P seeds to comet config + if seeds := network.Static().ConsensusSeeds(); len(seeds) > 0 { + comet.P2P.Seeds = strings.Join(seeds, ",") + } + + if initCfg.TrustedSync && network.IsProtected() { + rpcServer := fmt.Sprintf("https://rpc.consensus.%s.storyprotocol", network) + + // Trusted state sync only supported for protected networks. + height, hash, err := getTrustHeightAndHash(ctx, rpcServer) + if err != nil { + return errors.Wrap(err, "get trusted height") + } + + comet.StateSync.Enable = true + comet.StateSync.RPCServers = []string{rpcServer, rpcServer} // CometBFT requires two RPC servers. Duplicate our RPC for now. + comet.StateSync.TrustHeight = height + comet.StateSync.TrustHash = hash + + log.Info(ctx, "Trusted state-sync enabled", "height", height, "hash", hash, "rpc_endpoint", rpcServer) + } else { + log.Info(ctx, "Not initializing trusted state sync") + } + + // Setup comet config + cmtConfigFile := filepath.Join(homeDir, cmtconfig.DefaultConfigDir, cmtconfig.DefaultConfigFileName) + if cmtos.FileExists(cmtConfigFile) { + log.Info(ctx, "Found comet config file", "path", cmtConfigFile) + } else { + cmtconfig.WriteConfigFile(cmtConfigFile, &comet) // This panics on any error :( + log.Info(ctx, "Generated default comet config file", "path", cmtConfigFile) + } + + // Setup iliad config + iliadConfigFile := cfg.ConfigFile() + if cmtos.FileExists(iliadConfigFile) { + log.Info(ctx, "Found iliad config file", "path", iliadConfigFile) + } else if err := iliadcfg.WriteConfigTOML(cfg, log.DefaultConfig()); err != nil { + return err + } else { + log.Info(ctx, "Generated default iliad config file", "path", iliadConfigFile) + } + + // Setup comet private validator + var pv *privval.FilePV + privValKeyFile := comet.PrivValidatorKeyFile() + privValStateFile := comet.PrivValidatorStateFile() + if cmtos.FileExists(privValKeyFile) { + pv = privval.LoadFilePV(privValKeyFile, privValStateFile) // This hard exits on any error. + log.Info(ctx, "Found cometBFT private validator", + "key_file", privValKeyFile, + "state_file", privValStateFile, + ) + } else { + pv = privval.NewFilePV(k1.GenPrivKey(), privValKeyFile, privValStateFile) + pv.Save() + log.Info(ctx, "Generated private validator", + "key_file", privValKeyFile, + "state_file", privValStateFile) + } + + // Setup node key + nodeKeyFile := comet.NodeKeyFile() + if cmtos.FileExists(nodeKeyFile) { + log.Info(ctx, "Found node key", "path", nodeKeyFile) + } else if _, err := p2p.LoadOrGenNodeKey(nodeKeyFile); err != nil { + return errors.Wrap(err, "load or generate node key") + } else { + log.Info(ctx, "Generated node key", "path", nodeKeyFile) + } + + // Setup genesis file + genFile := comet.GenesisFile() + if cmtos.FileExists(genFile) { + log.Info(ctx, "Found genesis file", "path", genFile) + } else if network == netconf.Simnet { + pubKey, err := pv.GetPubKey() + if err != nil { + return errors.Wrap(err, "get public key") + } + + var genDoc *types.GenesisDoc + if initCfg.Cosmos { + cosmosGen, err := genutil.MakeGenesis(network, time.Now(), initCfg.ExecutionHash, pubKey) + if err != nil { + return err + } + + genDoc, err = cosmosGen.ToGenesisDoc() + if err != nil { + return errors.Wrap(err, "convert to genesis doc") + } + } else { + genDoc, err = MakeGenesis(network, pubKey) + if err != nil { + return err + } + } + + if err := genDoc.SaveAs(genFile); err != nil { + return errors.Wrap(err, "save genesis file") + } + log.Info(ctx, "Generated simnet genesis file", "path", genFile) + } else if len(network.Static().ConsensusGenesisJSON) > 0 { + if err := os.WriteFile(genFile, network.Static().ConsensusGenesisJSON, 0o644); err != nil { + return errors.Wrap(err, "failed to write genesis file") + } + pubKey, err := pv.GetPubKey() + if err != nil { + return errors.Wrap(err, "failed to get public key") + } + + // Derive the various addresses from the public key + accAddr := sdk.AccAddress(pubKey.Address().Bytes()).String() + valAddr := sdk.ValAddress(pubKey.Address().Bytes()).String() + pubKeyBase64 := base64.StdEncoding.EncodeToString(pubKey.Bytes()) + fmt.Println("Base64 Encoded Public Key:", pubKeyBase64) + + genesisJSON := string(network.Static().ConsensusGenesisJSON) + genesisJSON = strings.ReplaceAll(genesisJSON, "{{LOCAL_ACCOUNT_ADDRESS}}", accAddr) + genesisJSON = strings.ReplaceAll(genesisJSON, "{{LOCAL_VALIDATOR_ADDRESS}}", valAddr) + genesisJSON = strings.ReplaceAll(genesisJSON, "{{LOCAL_VALIDATOR_KEY}}", pubKeyBase64) + + err = os.WriteFile(genFile, []byte(genesisJSON), 0o644) + + if err != nil { + return errors.Wrap(err, "save genesis file") + } + log.Info(ctx, "Generated well-known network genesis file", "path", genFile) + } else { + return errors.New("network genesis file not supported yet", "network", network) + } + + return nil +} + +func getTrustHeightAndHash(ctx context.Context, baseURL string) (int64, string, error) { + cl, err := rpchttp.New(baseURL, "/websocket") + if err != nil { + return 0, "", errors.Wrap(err, "create rpc client") + } + + latest, err := cl.Block(ctx, nil) + if err != nil { + return 0, "", errors.Wrap(err, "get latest block") + } + + // Truncate height to last defaultSnapshotPeriod + const defaultSnapshotPeriod int64 = 1000 + snapshotHeight := defaultSnapshotPeriod * (latest.Block.Height / defaultSnapshotPeriod) + + b, err := cl.Block(ctx, &snapshotHeight) + if err != nil { + return 0, "", errors.Wrap(err, "get snapshot block") + } + + return b.Block.Height, b.BlockID.Hash.String(), nil +} + +func checkHomeDir(homeDir string) error { + files, _ := os.ReadDir(homeDir) // Ignore error, we'll just assume it's empty. + for _, file := range files { + if file.IsDir() { + continue + } + + return errors.New("home directory contains unexpected file(s), use --force to initialize anyway", + "home", homeDir, "example_file", file.Name()) + } + + return nil +} + +func prepareHomeDirectory(ctx context.Context, initCfg InitConfig, homeDir string) error { + if !initCfg.Force { + log.Info(ctx, "Ensuring provided home folder does not contain files, since --force=true") + if err := checkHomeDir(homeDir); err != nil { + return err + } + } + + if initCfg.Clean { + log.Info(ctx, "Deleting home directory, since --clean=true") + if err := os.RemoveAll(homeDir); err != nil { + return errors.Wrap(err, "remove home dir") + } + } + + return nil +} diff --git a/client/cmd/init_internal_test.go b/client/cmd/init_internal_test.go new file mode 100644 index 00000000..a9bb1f41 --- /dev/null +++ b/client/cmd/init_internal_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test . -run TestInit -golden + +func TestInitFiles(t *testing.T) { + t.Parallel() + dir := t.TempDir() + cfg := InitConfig{ + HomeDir: dir, + Network: netconf.Simnet, + } + err := InitFiles(context.Background(), cfg) + require.NoError(t, err) + + files, err := filepath.Glob(dir + "/**/*") + require.NoError(t, err) + + var resp string + for _, file := range files { + resp += strings.TrimPrefix(file, dir) + "\n" + } + + tutil.RequireGoldenBytes(t, []byte(resp)) +} + +func TestInitForce(t *testing.T) { + t.Parallel() + dir := t.TempDir() + + // Create a dummy file + err := os.WriteFile(filepath.Join(dir, "dummy"), nil, 0o644) + require.NoError(t, err) + + cfg := InitConfig{ + HomeDir: dir, + Network: netconf.Simnet, + } + + err = InitFiles(context.Background(), cfg) + require.ErrorContains(t, err, "unexpected file") + + cfg.Force = true + err = InitFiles(context.Background(), cfg) + require.NoError(t, err) +} diff --git a/client/cmd/testdata/TestCLIReference_iliad.golden b/client/cmd/testdata/TestCLIReference_iliad.golden new file mode 100644 index 00000000..a5fb8556 --- /dev/null +++ b/client/cmd/testdata/TestCLIReference_iliad.golden @@ -0,0 +1,17 @@ +Iliad is a consensus client implementation for the Story L1 blockchain + +Usage: + iliad [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + init Initializes required iliad files and directories + run Runs the iliad consensus client + validator Commands for validator operations + version Print the version information of this binary + +Flags: + -h, --help help for iliad + +Use "iliad [command] --help" for more information about a command. diff --git a/client/cmd/testdata/TestCLIReference_init.golden b/client/cmd/testdata/TestCLIReference_init.golden new file mode 100644 index 00000000..065b1268 --- /dev/null +++ b/client/cmd/testdata/TestCLIReference_init.golden @@ -0,0 +1,27 @@ +Initializes required iliad files and directories. + +Ensures all the following files and directories exist: + / # Iliad home directory + ā”œā”€ā”€ config # Config directory + ā”‚ ā”œā”€ā”€ config.toml # CometBFT configuration + ā”‚ ā”œā”€ā”€ genesis.json # Iliad chain genesis file + ā”‚ ā”œā”€ā”€ iliad.toml # Iliad configuration + ā”‚ ā”œā”€ā”€ node_key.json # Node P2P identity key + ā”‚ ā””ā”€ā”€ priv_validator_key.json # CometBFT private validator key (back this up and keep it safe) + ā”œā”€ā”€ data # Data directory + ā”‚ ā”œā”€ā”€ snapshots # Snapshot directory + ā”‚ ā”œā”€ā”€ priv_validator_state.json # CometBFT private validator state (slashing protection) + +Existing files are not overwritten, unless --clean is specified. +The home directory should only contain subdirectories, no files, use --force to ignore this check. + +Usage: + iliad init [flags] + +Flags: + --clean Delete home directory before initialization + --force Force initialization (overwrite existing files) + -h, --help help for init + --home string The application home directory containing config and data (default "./iliad") + --network string Iliad network to participate in: mainnet, testnet, devnet + --trusted-sync Initialize trusted state-sync height and hash by querying the Iliad RPC diff --git a/client/cmd/testdata/TestCLIReference_run.golden b/client/cmd/testdata/TestCLIReference_run.golden new file mode 100644 index 00000000..fb764cb6 --- /dev/null +++ b/client/cmd/testdata/TestCLIReference_run.golden @@ -0,0 +1,26 @@ +Runs the iliad consensus client + +Usage: + iliad run [flags] + +Flags: + --api-address string The API server address to listen on (default "127.0.0.1:1317") + --api-enable Define if the API server should be enabled + --app-db-backend string The type of database for application and snapshots databases (default "goleveldb") + --enabled-unsafe-cors Enable unsafe CORS for API server + --engine-endpoint string An EVM execution client Engine API http endpoint + --engine-jwt-file string The path to the Engine API JWT file + --evm-build-delay duration Minimum delay between triggering and fetching a EVM payload build (default 600ms) + --evm-build-optimistic Enables optimistic building of EVM payloads on previous block finalize (default true) + -h, --help help for run + --home string The application home directory containing config and data (default "./iliad") + --log-color string Log color (only applicable to console format); auto, force, disable (default "auto") + --log-format string Log format; console, json (default "console") + --log-level string Log level; debug, info, warn, error (default "info") + --min-retain-blocks uint Minimum block height offset during ABCI commit to prune CometBFT blocks + --network string Iliad network to participate in: mainnet, testnet, devnet + --pruning string Pruning strategy (default|nothing|everything) (default "nothing") + --snapshot-interval uint State sync snapshot interval (default 1000) + --snapshot-keep-recent uint State sync snapshot to keep (default 2) + --tracing-endpoint string Tracing OTLP endpoint + --tracing-headers string Tracing OTLP headers diff --git a/client/cmd/testdata/TestInitFiles.golden b/client/cmd/testdata/TestInitFiles.golden new file mode 100644 index 00000000..ac4ebce9 --- /dev/null +++ b/client/cmd/testdata/TestInitFiles.golden @@ -0,0 +1,7 @@ +/config/config.toml +/config/genesis.json +/config/iliad.toml +/config/node_key.json +/config/priv_validator_key.json +/data/priv_validator_state.json +/data/snapshots diff --git a/client/cmd/testdata/TestRunCmd_defaults.golden b/client/cmd/testdata/TestRunCmd_defaults.golden new file mode 100644 index 00000000..56188c3c --- /dev/null +++ b/client/cmd/testdata/TestRunCmd_defaults.golden @@ -0,0 +1,163 @@ +{ + "HomeDir": "./iliad", + "Network": "", + "EthKeyPassword": "", + "EngineJWTFile": "", + "EngineEndpoint": "", + "SnapshotInterval": 1000, + "SnapshotKeepRecent": 2, + "BackendType": "goleveldb", + "MinRetainBlocks": 0, + "PruningOption": "nothing", + "EVMBuildDelay": 600000000, + "EVMBuildOptimistic": true, + "APIEnable": false, + "APIAddress": "127.0.0.1:1317", + "EnableUnsafeCORS": false, + "Tracer": { + "Endpoint": "", + "Headers": "" + }, + "Comet": { + "Version": "0.38.9", + "RootDir": "./iliad", + "ProxyApp": "tcp://127.0.0.1:26658", + "Moniker": "testmoniker", + "DBBackend": "goleveldb", + "DBPath": "data", + "LogLevel": "error", + "LogFormat": "plain", + "Genesis": "config/genesis.json", + "PrivValidatorKey": "config/priv_validator_key.json", + "PrivValidatorState": "data/priv_validator_state.json", + "PrivValidatorListenAddr": "", + "NodeKey": "config/node_key.json", + "ABCI": "socket", + "FilterPeers": false, + "RPC": { + "RootDir": "./iliad", + "ListenAddress": "tcp://127.0.0.1:26657", + "CORSAllowedOrigins": [], + "CORSAllowedMethods": [ + "HEAD", + "GET", + "POST" + ], + "CORSAllowedHeaders": [ + "Origin", + "Accept", + "Content-Type", + "X-Requested-With", + "X-Server-Time" + ], + "GRPCListenAddress": "", + "GRPCMaxOpenConnections": 900, + "Unsafe": false, + "MaxOpenConnections": 900, + "MaxSubscriptionClients": 100, + "MaxSubscriptionsPerClient": 5, + "SubscriptionBufferSize": 200, + "WebSocketWriteBufferSize": 200, + "CloseOnSlowClient": false, + "TimeoutBroadcastTxCommit": 10000000000, + "MaxRequestBatchSize": 10, + "MaxBodyBytes": 1000000, + "MaxHeaderBytes": 1048576, + "TLSCertFile": "", + "TLSKeyFile": "", + "PprofListenAddress": "" + }, + "P2P": { + "RootDir": "./iliad", + "ListenAddress": "tcp://0.0.0.0:26656", + "ExternalAddress": "", + "Seeds": "", + "PersistentPeers": "", + "AddrBook": "config/addrbook.json", + "AddrBookStrict": true, + "MaxNumInboundPeers": 40, + "MaxNumOutboundPeers": 10, + "UnconditionalPeerIDs": "", + "PersistentPeersMaxDialPeriod": 0, + "FlushThrottleTimeout": 100000000, + "MaxPacketMsgPayloadSize": 1024, + "SendRate": 5120000, + "RecvRate": 5120000, + "PexReactor": true, + "SeedMode": false, + "PrivatePeerIDs": "", + "AllowDuplicateIP": false, + "HandshakeTimeout": 20000000000, + "DialTimeout": 3000000000, + "TestDialFail": false, + "TestFuzz": false, + "TestFuzzConfig": { + "Mode": 0, + "MaxDelay": 3000000000, + "ProbDropRW": 0.2, + "ProbDropConn": 0, + "ProbSleep": 0 + } + }, + "Mempool": { + "Type": "nop", + "RootDir": "./iliad", + "Recheck": true, + "RecheckTimeout": 1000000000, + "Broadcast": true, + "WalPath": "", + "Size": 5000, + "MaxTxsBytes": 1073741824, + "CacheSize": 10000, + "KeepInvalidTxsInCache": false, + "MaxTxBytes": 1048576, + "MaxBatchBytes": 0, + "ExperimentalMaxGossipConnectionsToPersistentPeers": 0, + "ExperimentalMaxGossipConnectionsToNonPersistentPeers": 0 + }, + "StateSync": { + "Enable": false, + "TempDir": "", + "RPCServers": null, + "TrustPeriod": 604800000000000, + "TrustHeight": 0, + "TrustHash": "", + "DiscoveryTime": 10000000000, + "ChunkRequestTimeout": 60000000000, + "ChunkFetchers": 4 + }, + "BlockSync": { + "Version": "v0" + }, + "Consensus": { + "RootDir": "./iliad", + "WalPath": "data/cs.wal/wal", + "TimeoutPropose": 3000000000, + "TimeoutProposeDelta": 500000000, + "TimeoutPrevote": 1000000000, + "TimeoutPrevoteDelta": 500000000, + "TimeoutPrecommit": 1000000000, + "TimeoutPrecommitDelta": 500000000, + "TimeoutCommit": 1000000000, + "SkipTimeoutCommit": false, + "CreateEmptyBlocks": true, + "CreateEmptyBlocksInterval": 0, + "PeerGossipSleepDuration": 100000000, + "PeerQueryMaj23SleepDuration": 2000000000, + "DoubleSignCheckHeight": 0 + }, + "Storage": { + "DiscardABCIResponses": false + }, + "TxIndex": { + "Indexer": "null", + "PsqlConn": "" + }, + "Instrumentation": { + "Prometheus": false, + "PrometheusListenAddr": ":26660", + "MaxOpenConnections": 3, + "Namespace": "cometbft" + } + } +} \ No newline at end of file diff --git a/client/cmd/testdata/TestRunCmd_flags.golden b/client/cmd/testdata/TestRunCmd_flags.golden new file mode 100644 index 00000000..f0e5bbcc --- /dev/null +++ b/client/cmd/testdata/TestRunCmd_flags.golden @@ -0,0 +1,163 @@ +{ + "HomeDir": "foo", + "Network": "", + "EthKeyPassword": "", + "EngineJWTFile": "bar", + "EngineEndpoint": "", + "SnapshotInterval": 1000, + "SnapshotKeepRecent": 2, + "BackendType": "goleveldb", + "MinRetainBlocks": 0, + "PruningOption": "nothing", + "EVMBuildDelay": 600000000, + "EVMBuildOptimistic": true, + "APIEnable": false, + "APIAddress": "127.0.0.1:1317", + "EnableUnsafeCORS": false, + "Tracer": { + "Endpoint": "", + "Headers": "" + }, + "Comet": { + "Version": "0.38.9", + "RootDir": "foo", + "ProxyApp": "tcp://127.0.0.1:26658", + "Moniker": "testmoniker", + "DBBackend": "goleveldb", + "DBPath": "data", + "LogLevel": "error", + "LogFormat": "plain", + "Genesis": "config/genesis.json", + "PrivValidatorKey": "config/priv_validator_key.json", + "PrivValidatorState": "data/priv_validator_state.json", + "PrivValidatorListenAddr": "", + "NodeKey": "config/node_key.json", + "ABCI": "socket", + "FilterPeers": false, + "RPC": { + "RootDir": "foo", + "ListenAddress": "tcp://127.0.0.1:26657", + "CORSAllowedOrigins": [], + "CORSAllowedMethods": [ + "HEAD", + "GET", + "POST" + ], + "CORSAllowedHeaders": [ + "Origin", + "Accept", + "Content-Type", + "X-Requested-With", + "X-Server-Time" + ], + "GRPCListenAddress": "", + "GRPCMaxOpenConnections": 900, + "Unsafe": false, + "MaxOpenConnections": 900, + "MaxSubscriptionClients": 100, + "MaxSubscriptionsPerClient": 5, + "SubscriptionBufferSize": 200, + "WebSocketWriteBufferSize": 200, + "CloseOnSlowClient": false, + "TimeoutBroadcastTxCommit": 10000000000, + "MaxRequestBatchSize": 10, + "MaxBodyBytes": 1000000, + "MaxHeaderBytes": 1048576, + "TLSCertFile": "", + "TLSKeyFile": "", + "PprofListenAddress": "" + }, + "P2P": { + "RootDir": "foo", + "ListenAddress": "tcp://0.0.0.0:26656", + "ExternalAddress": "", + "Seeds": "", + "PersistentPeers": "", + "AddrBook": "config/addrbook.json", + "AddrBookStrict": true, + "MaxNumInboundPeers": 40, + "MaxNumOutboundPeers": 10, + "UnconditionalPeerIDs": "", + "PersistentPeersMaxDialPeriod": 0, + "FlushThrottleTimeout": 100000000, + "MaxPacketMsgPayloadSize": 1024, + "SendRate": 5120000, + "RecvRate": 5120000, + "PexReactor": true, + "SeedMode": false, + "PrivatePeerIDs": "", + "AllowDuplicateIP": false, + "HandshakeTimeout": 20000000000, + "DialTimeout": 3000000000, + "TestDialFail": false, + "TestFuzz": false, + "TestFuzzConfig": { + "Mode": 0, + "MaxDelay": 3000000000, + "ProbDropRW": 0.2, + "ProbDropConn": 0, + "ProbSleep": 0 + } + }, + "Mempool": { + "Type": "nop", + "RootDir": "foo", + "Recheck": true, + "RecheckTimeout": 1000000000, + "Broadcast": true, + "WalPath": "", + "Size": 5000, + "MaxTxsBytes": 1073741824, + "CacheSize": 10000, + "KeepInvalidTxsInCache": false, + "MaxTxBytes": 1048576, + "MaxBatchBytes": 0, + "ExperimentalMaxGossipConnectionsToPersistentPeers": 0, + "ExperimentalMaxGossipConnectionsToNonPersistentPeers": 0 + }, + "StateSync": { + "Enable": false, + "TempDir": "", + "RPCServers": null, + "TrustPeriod": 604800000000000, + "TrustHeight": 0, + "TrustHash": "", + "DiscoveryTime": 10000000000, + "ChunkRequestTimeout": 60000000000, + "ChunkFetchers": 4 + }, + "BlockSync": { + "Version": "v0" + }, + "Consensus": { + "RootDir": "foo", + "WalPath": "data/cs.wal/wal", + "TimeoutPropose": 3000000000, + "TimeoutProposeDelta": 500000000, + "TimeoutPrevote": 1000000000, + "TimeoutPrevoteDelta": 500000000, + "TimeoutPrecommit": 1000000000, + "TimeoutPrecommitDelta": 500000000, + "TimeoutCommit": 1000000000, + "SkipTimeoutCommit": false, + "CreateEmptyBlocks": true, + "CreateEmptyBlocksInterval": 0, + "PeerGossipSleepDuration": 100000000, + "PeerQueryMaj23SleepDuration": 2000000000, + "DoubleSignCheckHeight": 0 + }, + "Storage": { + "DiscardABCIResponses": false + }, + "TxIndex": { + "Indexer": "null", + "PsqlConn": "" + }, + "Instrumentation": { + "Prometheus": false, + "PrometheusListenAddr": ":26660", + "MaxOpenConnections": 3, + "Namespace": "cometbft" + } + } +} \ No newline at end of file diff --git a/client/cmd/testdata/TestRunCmd_json_files.golden b/client/cmd/testdata/TestRunCmd_json_files.golden new file mode 100644 index 00000000..275ed5d9 --- /dev/null +++ b/client/cmd/testdata/TestRunCmd_json_files.golden @@ -0,0 +1,163 @@ +{ + "HomeDir": "testinput/input2", + "Network": "", + "EthKeyPassword": "", + "EngineJWTFile": "", + "EngineEndpoint": "", + "SnapshotInterval": 1000, + "SnapshotKeepRecent": 2, + "BackendType": "goleveldb", + "MinRetainBlocks": 0, + "PruningOption": "nothing", + "EVMBuildDelay": 600000000, + "EVMBuildOptimistic": true, + "APIEnable": false, + "APIAddress": "127.0.0.1:1317", + "EnableUnsafeCORS": false, + "Tracer": { + "Endpoint": "", + "Headers": "" + }, + "Comet": { + "Version": "0.38.9", + "RootDir": "testinput/input2", + "ProxyApp": "tcp://127.0.0.1:26658", + "Moniker": "testmoniker", + "DBBackend": "goleveldb", + "DBPath": "data", + "LogLevel": "error", + "LogFormat": "plain", + "Genesis": "config/genesis.json", + "PrivValidatorKey": "config/priv_validator_key.json", + "PrivValidatorState": "data/priv_validator_state.json", + "PrivValidatorListenAddr": "", + "NodeKey": "config/node_key.json", + "ABCI": "socket", + "FilterPeers": false, + "RPC": { + "RootDir": "testinput/input2", + "ListenAddress": "tcp://127.0.0.1:26657", + "CORSAllowedOrigins": [], + "CORSAllowedMethods": [ + "HEAD", + "GET", + "POST" + ], + "CORSAllowedHeaders": [ + "Origin", + "Accept", + "Content-Type", + "X-Requested-With", + "X-Server-Time" + ], + "GRPCListenAddress": "", + "GRPCMaxOpenConnections": 900, + "Unsafe": false, + "MaxOpenConnections": 900, + "MaxSubscriptionClients": 100, + "MaxSubscriptionsPerClient": 5, + "SubscriptionBufferSize": 200, + "WebSocketWriteBufferSize": 200, + "CloseOnSlowClient": false, + "TimeoutBroadcastTxCommit": 10000000000, + "MaxRequestBatchSize": 10, + "MaxBodyBytes": 1000000, + "MaxHeaderBytes": 1048576, + "TLSCertFile": "", + "TLSKeyFile": "", + "PprofListenAddress": "" + }, + "P2P": { + "RootDir": "testinput/input2", + "ListenAddress": "tcp://0.0.0.0:26656", + "ExternalAddress": "", + "Seeds": "", + "PersistentPeers": "", + "AddrBook": "config/addrbook.json", + "AddrBookStrict": true, + "MaxNumInboundPeers": 40, + "MaxNumOutboundPeers": 10, + "UnconditionalPeerIDs": "", + "PersistentPeersMaxDialPeriod": 0, + "FlushThrottleTimeout": 100000000, + "MaxPacketMsgPayloadSize": 1024, + "SendRate": 5120000, + "RecvRate": 5120000, + "PexReactor": true, + "SeedMode": false, + "PrivatePeerIDs": "", + "AllowDuplicateIP": false, + "HandshakeTimeout": 20000000000, + "DialTimeout": 3000000000, + "TestDialFail": false, + "TestFuzz": false, + "TestFuzzConfig": { + "Mode": 0, + "MaxDelay": 3000000000, + "ProbDropRW": 0.2, + "ProbDropConn": 0, + "ProbSleep": 0 + } + }, + "Mempool": { + "Type": "nop", + "RootDir": "testinput/input2", + "Recheck": true, + "RecheckTimeout": 1000000000, + "Broadcast": true, + "WalPath": "", + "Size": 5000, + "MaxTxsBytes": 1073741824, + "CacheSize": 10000, + "KeepInvalidTxsInCache": false, + "MaxTxBytes": 1048576, + "MaxBatchBytes": 0, + "ExperimentalMaxGossipConnectionsToPersistentPeers": 0, + "ExperimentalMaxGossipConnectionsToNonPersistentPeers": 0 + }, + "StateSync": { + "Enable": false, + "TempDir": "", + "RPCServers": null, + "TrustPeriod": 604800000000000, + "TrustHeight": 0, + "TrustHash": "", + "DiscoveryTime": 10000000000, + "ChunkRequestTimeout": 60000000000, + "ChunkFetchers": 4 + }, + "BlockSync": { + "Version": "v0" + }, + "Consensus": { + "RootDir": "testinput/input2", + "WalPath": "data/cs.wal/wal", + "TimeoutPropose": 3000000000, + "TimeoutProposeDelta": 500000000, + "TimeoutPrevote": 1000000000, + "TimeoutPrevoteDelta": 500000000, + "TimeoutPrecommit": 1000000000, + "TimeoutPrecommitDelta": 500000000, + "TimeoutCommit": 1000000000, + "SkipTimeoutCommit": false, + "CreateEmptyBlocks": true, + "CreateEmptyBlocksInterval": 0, + "PeerGossipSleepDuration": 100000000, + "PeerQueryMaj23SleepDuration": 2000000000, + "DoubleSignCheckHeight": 0 + }, + "Storage": { + "DiscardABCIResponses": false + }, + "TxIndex": { + "Indexer": "null", + "PsqlConn": "" + }, + "Instrumentation": { + "Prometheus": false, + "PrometheusListenAddr": ":26660", + "MaxOpenConnections": 3, + "Namespace": "cometbft" + } + } +} \ No newline at end of file diff --git a/client/cmd/testdata/TestRunCmd_toml_files.golden b/client/cmd/testdata/TestRunCmd_toml_files.golden new file mode 100644 index 00000000..a93b1f2e --- /dev/null +++ b/client/cmd/testdata/TestRunCmd_toml_files.golden @@ -0,0 +1,163 @@ +{ + "HomeDir": "testinput/input1", + "Network": "", + "EthKeyPassword": "", + "EngineJWTFile": "", + "EngineEndpoint": "", + "SnapshotInterval": 1000, + "SnapshotKeepRecent": 2, + "BackendType": "goleveldb", + "MinRetainBlocks": 0, + "PruningOption": "nothing", + "EVMBuildDelay": 600000000, + "EVMBuildOptimistic": true, + "APIEnable": false, + "APIAddress": "127.0.0.1:1317", + "EnableUnsafeCORS": false, + "Tracer": { + "Endpoint": "", + "Headers": "" + }, + "Comet": { + "Version": "0.38.9", + "RootDir": "testinput/input1", + "ProxyApp": "tcp://127.0.0.1:26658", + "Moniker": "config.toml", + "DBBackend": "goleveldb", + "DBPath": "data", + "LogLevel": "error", + "LogFormat": "plain", + "Genesis": "config/genesis.json", + "PrivValidatorKey": "config/priv_validator_key.json", + "PrivValidatorState": "data/priv_validator_state.json", + "PrivValidatorListenAddr": "", + "NodeKey": "config/node_key.json", + "ABCI": "socket", + "FilterPeers": false, + "RPC": { + "RootDir": "testinput/input1", + "ListenAddress": ":1234", + "CORSAllowedOrigins": [], + "CORSAllowedMethods": [ + "HEAD", + "GET", + "POST" + ], + "CORSAllowedHeaders": [ + "Origin", + "Accept", + "Content-Type", + "X-Requested-With", + "X-Server-Time" + ], + "GRPCListenAddress": "", + "GRPCMaxOpenConnections": 900, + "Unsafe": false, + "MaxOpenConnections": 900, + "MaxSubscriptionClients": 100, + "MaxSubscriptionsPerClient": 5, + "SubscriptionBufferSize": 200, + "WebSocketWriteBufferSize": 200, + "CloseOnSlowClient": false, + "TimeoutBroadcastTxCommit": 10000000000, + "MaxRequestBatchSize": 10, + "MaxBodyBytes": 1000000, + "MaxHeaderBytes": 1048576, + "TLSCertFile": "", + "TLSKeyFile": "", + "PprofListenAddress": "" + }, + "P2P": { + "RootDir": "testinput/input1", + "ListenAddress": "tcp://0.0.0.0:26656", + "ExternalAddress": "", + "Seeds": "", + "PersistentPeers": "", + "AddrBook": "config/addrbook.json", + "AddrBookStrict": true, + "MaxNumInboundPeers": 40, + "MaxNumOutboundPeers": 10, + "UnconditionalPeerIDs": "", + "PersistentPeersMaxDialPeriod": 0, + "FlushThrottleTimeout": 100000000, + "MaxPacketMsgPayloadSize": 1024, + "SendRate": 5120000, + "RecvRate": 5120000, + "PexReactor": true, + "SeedMode": false, + "PrivatePeerIDs": "", + "AllowDuplicateIP": false, + "HandshakeTimeout": 20000000000, + "DialTimeout": 3000000000, + "TestDialFail": false, + "TestFuzz": false, + "TestFuzzConfig": { + "Mode": 0, + "MaxDelay": 3000000000, + "ProbDropRW": 0.2, + "ProbDropConn": 0, + "ProbSleep": 0 + } + }, + "Mempool": { + "Type": "nop", + "RootDir": "testinput/input1", + "Recheck": true, + "RecheckTimeout": 1000000000, + "Broadcast": true, + "WalPath": "", + "Size": 5000, + "MaxTxsBytes": 1073741824, + "CacheSize": 10000, + "KeepInvalidTxsInCache": false, + "MaxTxBytes": 1048576, + "MaxBatchBytes": 0, + "ExperimentalMaxGossipConnectionsToPersistentPeers": 0, + "ExperimentalMaxGossipConnectionsToNonPersistentPeers": 0 + }, + "StateSync": { + "Enable": false, + "TempDir": "", + "RPCServers": null, + "TrustPeriod": 604800000000000, + "TrustHeight": 0, + "TrustHash": "", + "DiscoveryTime": 10000000000, + "ChunkRequestTimeout": 60000000000, + "ChunkFetchers": 4 + }, + "BlockSync": { + "Version": "v0" + }, + "Consensus": { + "RootDir": "testinput/input1", + "WalPath": "data/cs.wal/wal", + "TimeoutPropose": 3000000000, + "TimeoutProposeDelta": 500000000, + "TimeoutPrevote": 1000000000, + "TimeoutPrevoteDelta": 500000000, + "TimeoutPrecommit": 1000000000, + "TimeoutPrecommitDelta": 500000000, + "TimeoutCommit": 99000000000, + "SkipTimeoutCommit": false, + "CreateEmptyBlocks": true, + "CreateEmptyBlocksInterval": 0, + "PeerGossipSleepDuration": 100000000, + "PeerQueryMaj23SleepDuration": 2000000000, + "DoubleSignCheckHeight": 0 + }, + "Storage": { + "DiscardABCIResponses": false + }, + "TxIndex": { + "Indexer": "null", + "PsqlConn": "" + }, + "Instrumentation": { + "Prometheus": false, + "PrometheusListenAddr": ":26660", + "MaxOpenConnections": 3, + "Namespace": "cometbft" + } + } +} \ No newline at end of file diff --git a/client/cmd/testinput/input1/config/config.toml b/client/cmd/testinput/input1/config/config.toml new file mode 100644 index 00000000..c751509d --- /dev/null +++ b/client/cmd/testinput/input1/config/config.toml @@ -0,0 +1,7 @@ +moniker = "config.toml" + +[rpc] +laddr = ":1234" + +[consensus] +timeout_commit = "99s" diff --git a/client/cmd/testinput/input1/config/halo.toml b/client/cmd/testinput/input1/config/halo.toml new file mode 100644 index 00000000..e45ce30a --- /dev/null +++ b/client/cmd/testinput/input1/config/halo.toml @@ -0,0 +1,12 @@ +engine-jwt-file="jwt.toml" + + + +snapshot-interval=999 + +[state] +persist-interval=99 + +[tracing] +endpoint="http://tracing.com" +headers="Authorization=Basic 123456" diff --git a/client/cmd/testinput/input2/config/halo.json b/client/cmd/testinput/input2/config/halo.json new file mode 100644 index 00000000..e156a5f8 --- /dev/null +++ b/client/cmd/testinput/input2/config/halo.json @@ -0,0 +1,5 @@ +{ +"engine-jwt-file": "jwt.json", +"snapshot-interval": 123, +"state-persist-interval": 12 +} diff --git a/client/cmd/validator.go b/client/cmd/validator.go new file mode 100644 index 00000000..6184e36a --- /dev/null +++ b/client/cmd/validator.go @@ -0,0 +1,881 @@ +package cmd + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/joho/godotenv" + "github.com/spf13/cobra" + + "github.com/piplabs/story/client/config" + "github.com/piplabs/story/lib/errors" + + _ "embed" +) + +//go:embed abi/IPTokenStaking.abi.json +var ipTokenStakingABI []byte + +type ValidatorKey struct { + Address string `json:"address"` + PubKey KeyInfo `json:"pub_key"` + PrivKey KeyInfo `json:"priv_key"` +} + +type KeyInfo struct { + Type string `json:"type"` + Value string `json:"value"` +} +type stakeConfig struct { + RPC string + ValidatorPubKey string + StakeAmount string + PrivateKey string + Explorer string + ChainID int64 +} + +type unstakeConfig struct { + RPC string + ValidatorPubKey string + UnstakeAmount string + ExecutionAddress string + PrivateKey string + Explorer string + ChainID int64 +} + +type createValidatorConfig struct { + RPC string + ValidatorKeyFile string + StakeAmount string + PrivateKey string + Explorer string + ChainID int64 +} + +func addKeyFileFlag(cmd *cobra.Command, keyFilePath *string) { + defaultKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "priv_validator_key.json") + cmd.Flags().StringVar(keyFilePath, "keyfile", defaultKeyFilePath, "Path to the Tendermint key file") +} + +func loadEnv() { + err := godotenv.Load() + if err != nil { + fmt.Println("Warning: No .env file found") + } +} + +func newValidatorCmds() *cobra.Command { + cmd := &cobra.Command{ + Use: "validator", + Short: "Commands for validator operations", + Args: cobra.NoArgs, + } + + cmd.AddCommand( + newValidatorCreateCmd(), + newValidatorKeyExportCmd(), + newValidatorStakeCmd(), + newValidatorUnstakeCmd(), + ) + + return cmd +} + +func newValidatorKeyExportCmd() *cobra.Command { + var keyFilePath string + + cmd := &cobra.Command{ + Use: "export", + Short: "Export the EVM private key from the Tendermint key file", + RunE: func(_ *cobra.Command, _ []string) error { + loadEnv() + return validatorKeyExport(keyFilePath) + }, + } + + addKeyFileFlag(cmd, &keyFilePath) + + return cmd +} + +func createValidator(ctx context.Context, cfg createValidatorConfig) error { + // Read the priv_validator_key.json file + keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) + if err != nil { + return errors.Wrap(err, "invalid key file") + } + + var keyFileData ValidatorKey + if err := json.Unmarshal(keyFileBytes, &keyFileData); err != nil { + return errors.Wrap(err, "failed to unmarshal priv_validator_key.json") + } + + // Decode and uncompress the public key + compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyFileData.PubKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode base64 key") + } + if len(compressedPubKeyBytes) != 33 { + return errors.New("invalid compressed public key length", "length", len(compressedPubKeyBytes)) + } + + // Load the private key for funding the EVM transaction from the .env file if not provided as a flag + if cfg.PrivateKey == "" { + cfg.PrivateKey = os.Getenv("PRIVATE_KEY") + if cfg.PrivateKey == "" { + return errors.New("missing required flag", "private-key", "EVM private key") + } + } + + evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + if err != nil { + return errors.Wrap(err, "invalid EVM private key") + } + + // Connect to the Ethereum client + client, err := ethclient.Dial(cfg.RPC) + if err != nil { + return errors.Wrap(err, "failed to connect to Ethereum client") + } + + // Get the balance of the account + publicKey := evmPrivKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return errors.New("error casting public key to ECDSA") + } + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + // Fetch balance + balance, err := client.BalanceAt(ctx, fromAddress, nil) + if err != nil { + return errors.Wrap(err, "failed to fetch balance") + } + + fmt.Printf("Balance: %s wei\n", balance.String()) + + // Convert the stake amount to a big.Int + stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) + if !ok { + return errors.New("invalid stake amount", "amount", cfg.StakeAmount) + } + + // Suggest gas price + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + return errors.Wrap(err, "failed to suggest gas price") + } + + // Increase gas price by 20% to ensure faster confirmation + gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) + gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) + + // Read the ABI file from embedded content + contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) + if err != nil { + return errors.Wrap(err, "failed to parse ABI") + } + + contractAddress := common.HexToAddress("0xCCcCcC0000000000000000000000000000000001") + + // Get the nonce for the transaction + nonce, err := client.PendingNonceAt(ctx, fromAddress) + if err != nil { + return errors.Wrap(err, "failed to get nonce") + } + + fmt.Printf("Using Nonce: %d\n", nonce) + + // Prepare transaction data + data, err := contractABI.Pack( + "createValidatorOnBehalf", + compressedPubKeyBytes, + ) + if err != nil { + return errors.Wrap(err, "failed to pack data") + } + + fmt.Printf("Packed Data: %x\n", data) + + chainID := big.NewInt(cfg.ChainID) + + // Estimate gas limit + msg := ethereum.CallMsg{ + From: fromAddress, + To: &contractAddress, + GasPrice: gasPrice, + Value: stakeAmount, + Data: data, + } + gasLimit, err := client.EstimateGas(ctx, msg) + if err != nil { + return errors.Wrap(err, "failed to estimate gas") + } + + // Define gas fee cap and gas tip cap dynamically + gasTipCap := gasPrice + gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) + + gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) + totalTxCost := new(big.Int).Add(gasCost, stakeAmount) + + fmt.Printf("Stake Amount: %s wei\n", stakeAmount.String()) + fmt.Printf("Gas Limit: %d\n", gasLimit) + fmt.Printf("Gas Price: %s wei\n", gasPrice.String()) + fmt.Printf("Gas Tip Cap: %s wei\n", gasTipCap.String()) + fmt.Printf("Gas Fee Cap: %s wei\n", gasFeeCap.String()) + fmt.Printf("Gas Cost: %s wei\n", gasCost.String()) + fmt.Printf("Total Transaction Cost: %s wei\n", totalTxCost.String()) + + if balance.Cmp(totalTxCost) < 0 { + return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) + } + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + To: &contractAddress, + Value: stakeAmount, + Data: data, + }) + + // Sign the transaction + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), evmPrivKey) + if err != nil { + return errors.Wrap(err, "failed to sign transaction") + } + + txHash := signedTx.Hash().Hex() + fmt.Printf("Transaction hash: %s\n", txHash) + fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) + + // Send the transaction + err = client.SendTransaction(ctx, signedTx) + if err != nil { + return errors.Wrap(err, "failed to send transaction") + } + + fmt.Println("Transaction sent, waiting for confirmation...") + + // Use bind.WaitMined to wait for the transaction receipt + receipt, err := bind.WaitMined(ctx, client, signedTx) + if err != nil { + return errors.Wrap(err, "transaction failed") + } + + if receipt.Status == types.ReceiptStatusFailed { + return errors.New("transaction failed", "status", receipt.Status) + } + + fmt.Println("Transaction confirmed successfully!") + fmt.Println("Validator created successfully!") + + return nil +} + +func validatorKeyExport(keyFilePath string) error { + // Read the key file + keyFileBytes, err := os.ReadFile(keyFilePath) + if err != nil { + return errors.Wrap(err, "failed to read key file") + } + + // Unmarshal the key file + var keyData ValidatorKey + if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { + return errors.Wrap(err, "failed to unmarshal key file") + } + + // Decode the base64 encoded private key + privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode private key") + } + + // Convert to EVM private key + privateKey, err := crypto.ToECDSA(privKeyBytes) + if err != nil { + return errors.Wrap(err, "invalid private key") + } + + // Derive EVM public key + publicKeyInterface := privateKey.Public() + publicKey, ok := publicKeyInterface.(*ecdsa.PublicKey) + if !ok { + return errors.New("failed to cast public key to ecdsa.PublicKey") + } + evmPublicKey := crypto.PubkeyToAddress(*publicKey).Hex() + + // Print the EVM private key and the uncompressed public key + evmPrivateKey := hex.EncodeToString(crypto.FromECDSA(privateKey)) + + // Decode the base64 encoded compressed public key + compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PubKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode base64 public key") + } + if len(compressedPubKeyBytes) != 33 { + return fmt.Errorf("invalid compressed public key length: %d", len(compressedPubKeyBytes)) + } + + curve := elliptic.P256() + x, y := elliptic.UnmarshalCompressed(curve, compressedPubKeyBytes) + if x == nil || y == nil { + return errors.New("failed to unmarshal compressed public key") + } + + // lint:ignore SA1019 ignoring deprecation warning for now + uncompressedPubKeyBytes := elliptic.Marshal(curve, x, y) + uncompressedPubKeyHex := hex.EncodeToString(uncompressedPubKeyBytes) + compressedPubKeyHex := hex.EncodeToString(compressedPubKeyBytes) + + fmt.Println("------------------------------------------------------") + fmt.Println("EVM Public Key") + fmt.Println("------------------------------------------------------") + fmt.Println(evmPublicKey) + fmt.Println("------------------------------------------------------") + fmt.Println("EVM Private Key:") + fmt.Println("------------------------------------------------------") + fmt.Println(evmPrivateKey) + fmt.Println("------------------------------------------------------") + fmt.Println("Compressed Public Key:") + fmt.Println("------------------------------------------------------") + fmt.Println(compressedPubKeyHex) + fmt.Println("------------------------------------------------------") + fmt.Println("Uncompressed Public Key:") + fmt.Println("------------------------------------------------------") + fmt.Println(uncompressedPubKeyHex) + fmt.Println("------------------------------------------------------") + + return nil +} + +func newValidatorCreateCmd() *cobra.Command { + var cfg createValidatorConfig + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new validator", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + loadEnv() + if err := validateFlags(cfg); err != nil { + fmt.Println("Debug: Entering cmd.Help()") + _ = cmd.Help() // Print the help message + + return err + } + + return createValidator(cmd.Context(), cfg) + }, + } + + bindCreateValidatorConfig(cmd, &cfg) + + return cmd +} + +func validateFlags(cfg createValidatorConfig) error { + var missingFlags []string + + if cfg.RPC == "" { + missingFlags = append(missingFlags, "rpc") + } + if cfg.ValidatorKeyFile == "" { + missingFlags = append(missingFlags, "keyfile") + } + if cfg.StakeAmount == "" { + missingFlags = append(missingFlags, "stake") + } + + if len(missingFlags) > 0 { + return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) + } + + return nil +} + +func bindCreateValidatorConfig(cmd *cobra.Command, cfg *createValidatorConfig) { + cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://rpc.partner.testnet.storyprotocol.net", "RPC URL to connect to the testnet") + addKeyFileFlag(cmd, &cfg.ValidatorKeyFile) + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount for the validator to self-delegate in wei") + cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used to issue the validator creation transaction") + cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://explorer.testnet.storyprotocol.net", "URL of the blockchain explorer") + cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") +} + +func newValidatorStakeCmd() *cobra.Command { + var cfg stakeConfig + + cmd := &cobra.Command{ + Use: "stake", + Short: "Stake tokens on behalf of a delegator", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + loadEnv() + if err := validateStakeFlags(cfg); err != nil { + fmt.Println("Debug: Entering cmd.Help()") + _ = cmd.Help() // Print the help message + + return err + } + + return stakeTokens(cmd.Context(), cfg) + }, + } + + bindStakeConfig(cmd, &cfg) + + return cmd +} + +func validateStakeFlags(cfg stakeConfig) error { + var missingFlags []string + + if cfg.RPC == "" { + missingFlags = append(missingFlags, "rpc") + } + if cfg.ValidatorPubKey == "" { + missingFlags = append(missingFlags, "validator-pubkey") + } + if cfg.StakeAmount == "" { + missingFlags = append(missingFlags, "stake") + } + + if len(missingFlags) > 0 { + return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) + } + + return nil +} + +func bindStakeConfig(cmd *cobra.Command, cfg *stakeConfig) { + cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://rpc.partner.testnet.storyprotocol.net", "RPC URL to connect to the testnet") + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount to stake on behalf of the delegator in wei") + cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used to derive the delegator's public key") + cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://explorer.testnet.storyprotocol.net", "URL of the blockchain explorer") + cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") +} + +func newValidatorUnstakeCmd() *cobra.Command { + var cfg unstakeConfig + + cmd := &cobra.Command{ + Use: "unstake", + Short: "Unstake tokens on behalf of a delegator", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + loadEnv() + if err := validateUnstakeFlags(cfg); err != nil { + fmt.Println("Debug: Entering cmd.Help()") + _ = cmd.Help() // Print the help message + + return err + } + + return unstakeTokens(cmd.Context(), cfg) + }, + } + + bindUnstakeConfig(cmd, &cfg) + + return cmd +} + +func validateUnstakeFlags(cfg unstakeConfig) error { + var missingFlags []string + + if cfg.RPC == "" { + missingFlags = append(missingFlags, "rpc") + } + if cfg.ValidatorPubKey == "" { + missingFlags = append(missingFlags, "validator-pubkey") + } + if cfg.UnstakeAmount == "" { + missingFlags = append(missingFlags, "unstake") + } + + if len(missingFlags) > 0 { + return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) + } + + return nil +} + +func bindUnstakeConfig(cmd *cobra.Command, cfg *unstakeConfig) { + cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://rpc.partner.testnet.storyprotocol.net", "RPC URL to connect to the testnet") + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") + cmd.Flags().StringVar(&cfg.UnstakeAmount, "unstake", "", "Amount to unstake on behalf of the delegator in wei") + cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used to derive the delegator's public key") + cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://explorer.testnet.storyprotocol.net", "URL of the blockchain explorer") + cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") +} + +func stakeTokens(ctx context.Context, cfg stakeConfig) error { + // Decode the validator public key + validatorPubKeyBytes := common.Hex2Bytes(cfg.ValidatorPubKey) + if len(validatorPubKeyBytes) != 33 { + return fmt.Errorf("invalid validator public key length: %d", len(validatorPubKeyBytes)) + } + + // Load the private key from the .env file if not provided as a flag + if cfg.PrivateKey == "" { + cfg.PrivateKey = os.Getenv("PRIVATE_KEY") + if cfg.PrivateKey == "" { + return errors.New("missing required flag", "private-key", "EVM private key") + } + } + + evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + if err != nil { + return errors.New("invalid EVM private key", err) + } + + // Derive the delegator's uncompressed public key + pubKey := evmPrivKey.PublicKey + // lint:ignore SA1019 ignoring deprecation warning for now + uncompressedPubKey := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) + if len(uncompressedPubKey) != 65 { + return fmt.Errorf("invalid uncompressed public key length: %d", len(uncompressedPubKey)) + } + fmt.Printf("Uncompressed Delegator PubKey: %x\n", uncompressedPubKey) + + // Connect to the Ethereum client + client, err := ethclient.Dial(cfg.RPC) + if err != nil { + return errors.Wrap(err, "failed to connect to Ethereum client") + } + + // Get the balance of the account + publicKey := evmPrivKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return errors.New("error casting public key to ECDSA") + } + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + // Fetch balance + balance, err := client.BalanceAt(ctx, fromAddress, nil) + if err != nil { + return errors.Wrap(err, "failed to fetch balance") + } + + fmt.Printf("Balance: %s wei\n", balance.String()) + + // Convert the stake amount to a big.Int + stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) + if !ok { + return errors.New("invalid stake amount", "amount", cfg.StakeAmount) + } + + // Suggest gas price + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + return errors.Wrap(err, "failed to suggest gas price") + } + + // Increase gas price by 20% to ensure faster confirmation + gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) + gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) + + // Read the ABI file from embedded content + contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) + if err != nil { + return errors.Wrap(err, "failed to parse ABI") + } + + contractAddress := common.HexToAddress("0xCCcCcC0000000000000000000000000000000001") + + // Get the nonce for the transaction + nonce, err := client.PendingNonceAt(ctx, fromAddress) + if err != nil { + return errors.Wrap(err, "failed to get nonce") + } + + fmt.Printf("Using Nonce: %d\n", nonce) + + // Prepare transaction data + data, err := contractABI.Pack( + "stake", + uncompressedPubKey, + validatorPubKeyBytes, + ) + if err != nil { + return errors.Wrap(err, "failed to pack data") + } + + fmt.Printf("Packed Data: %x\n", data) + + chainID := big.NewInt(cfg.ChainID) + + // Estimate gas limit + msg := ethereum.CallMsg{ + From: fromAddress, + To: &contractAddress, + GasPrice: gasPrice, + Value: stakeAmount, + Data: data, + } + gasLimit, err := client.EstimateGas(ctx, msg) + if err != nil { + return errors.Wrap(err, "failed to estimate gas") + } + + // Define gas fee cap and gas tip cap dynamically + gasTipCap := gasPrice + gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) + + gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) + totalTxCost := new(big.Int).Add(gasCost, stakeAmount) + + fmt.Printf("Stake Amount: %s wei\n", stakeAmount.String()) + fmt.Printf("Gas Limit: %d\n", gasLimit) + fmt.Printf("Gas Price: %s wei\n", gasPrice.String()) + fmt.Printf("Gas Tip Cap: %s wei\n", gasTipCap.String()) + fmt.Printf("Gas Fee Cap: %s wei\n", gasFeeCap.String()) + fmt.Printf("Gas Cost: %s wei\n", gasCost.String()) + fmt.Printf("Total Transaction Cost: %s wei\n", totalTxCost.String()) + + if balance.Cmp(totalTxCost) < 0 { + return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) + } + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + To: &contractAddress, + Value: stakeAmount, + Data: data, + }) + + // Sign the transaction + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), evmPrivKey) + if err != nil { + return errors.Wrap(err, "failed to sign transaction") + } + + txHash := signedTx.Hash().Hex() + fmt.Printf("Transaction hash: %s\n", txHash) + fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) + + // Send the transaction + err = client.SendTransaction(ctx, signedTx) + if err != nil { + return errors.Wrap(err, "failed to send transaction") + } + + fmt.Println("Transaction sent, waiting for confirmation...") + + // Use bind.WaitMined to wait for the transaction receipt + receipt, err := bind.WaitMined(ctx, client, signedTx) + if err != nil { + return errors.Wrap(err, "transaction failed") + } + + if receipt.Status == types.ReceiptStatusFailed { + return errors.New("transaction failed", "status", receipt.Status) + } + + fmt.Println("Transaction confirmed successfully!") + fmt.Println("Tokens staked successfully!") + + return nil +} + +func unstakeTokens(ctx context.Context, cfg unstakeConfig) error { + // Decode the validator public key + validatorPubKeyBytes := common.Hex2Bytes(cfg.ValidatorPubKey) + if len(validatorPubKeyBytes) != 33 { + return fmt.Errorf("invalid validator public key length: %d", len(validatorPubKeyBytes)) + } + + // Load the private key from the .env file if not provided as a flag + if cfg.PrivateKey == "" { + cfg.PrivateKey = os.Getenv("PRIVATE_KEY") + if cfg.PrivateKey == "" { + return errors.New("missing required flag", "private-key", "EVM private key") + } + } + + evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + if err != nil { + return errors.Wrap(err, "invalid EVM private key") + } + + // Derive the delegator's uncompressed public key + pubKey := evmPrivKey.PublicKey + // lint:ignore SA1019 ignoring deprecation warning for now + uncompressedPubKey := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) + if len(uncompressedPubKey) != 65 { + return fmt.Errorf("invalid uncompressed public key length: %d", len(uncompressedPubKey)) + } + fmt.Printf("Uncompressed Delegator PubKey: %x\n", uncompressedPubKey) + + // Convert the unstake amount to a big.Int + unstakeAmount, ok := new(big.Int).SetString(cfg.UnstakeAmount, 10) + if !ok { + return errors.New("invalid unstake amount", "amount", cfg.UnstakeAmount) + } + + // Connect to the Ethereum client + client, err := ethclient.Dial(cfg.RPC) + if err != nil { + return errors.Wrap(err, "failed to connect to Ethereum client") + } + + // Get the balance of the account + publicKey := evmPrivKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return errors.New("error casting public key to ECDSA") + } + + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + balance, err := client.BalanceAt(ctx, fromAddress, nil) + if err != nil { + return errors.Wrap(err, "failed to fetch balance") + } + + fmt.Printf("Balance: %s wei\n", balance.String()) + + // Suggest gas price + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + return errors.Wrap(err, "failed to suggest gas price") + } + + // Increase gas price by 20% to ensure faster confirmation + gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) + gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) + + // Read the ABI file from embedded content + contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) + if err != nil { + return errors.Wrap(err, "failed to parse ABI") + } + + contractAddress := common.HexToAddress("0xCCcCcC0000000000000000000000000000000001") + + // Get the nonce for the transaction + nonce, err := client.PendingNonceAt(ctx, fromAddress) + if err != nil { + return errors.Wrap(err, "failed to get nonce") + } + + fmt.Printf("Using Nonce: %d\n", nonce) + + // Prepare transaction data + data, err := contractABI.Pack( + "unstake", + uncompressedPubKey, + validatorPubKeyBytes, + unstakeAmount, + ) + if err != nil { + return errors.Wrap(err, "failed to pack data") + } + + fmt.Printf("Packed Data: %x\n", data) + + chainID := big.NewInt(cfg.ChainID) + + // Estimate gas limit + msg := ethereum.CallMsg{ + From: fromAddress, + To: &contractAddress, + GasPrice: gasPrice, + Data: data, + } + gasLimit, err := client.EstimateGas(ctx, msg) + if err != nil { + return errors.Wrap(err, "failed to estimate gas") + } + + // Define gas fee cap and gas tip cap dynamically + gasTipCap := gasPrice + gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) + + gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) + totalTxCost := gasCost // Only gas cost is considered + + fmt.Printf("Unstake Amount: %s wei\n", unstakeAmount.String()) + fmt.Printf("Gas Limit: %d\n", gasLimit) + fmt.Printf("Gas Price: %s wei\n", gasPrice.String()) + fmt.Printf("Gas Tip Cap: %s wei\n", gasTipCap.String()) + fmt.Printf("Gas Fee Cap: %s wei\n", gasFeeCap.String()) + fmt.Printf("Gas Cost: %s wei\n", gasCost.String()) + fmt.Printf("Total Transaction Cost: %s wei\n", totalTxCost.String()) + + if balance.Cmp(totalTxCost) < 0 { + return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) + } + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + To: &contractAddress, + Value: big.NewInt(0), // No value for unstake + Data: data, + }) + + // Sign the transaction + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), evmPrivKey) + if err != nil { + return errors.Wrap(err, "failed to sign transaction") + } + + txHash := signedTx.Hash().Hex() + fmt.Printf("Transaction hash: %s\n", txHash) + fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) + + // Send the transaction + err = client.SendTransaction(ctx, signedTx) + if err != nil { + return errors.Wrap(err, "failed to send transaction") + } + + fmt.Println("Transaction sent, waiting for confirmation...") + + // Use bind.WaitMined to wait for the transaction receipt + receipt, err := bind.WaitMined(ctx, client, signedTx) + if err != nil { + return errors.Wrap(err, "transaction failed") + } + + if receipt.Status == types.ReceiptStatusFailed { + return errors.New("transaction failed", "status", receipt.Status) + } + + fmt.Println("Transaction confirmed successfully!") + fmt.Println("Tokens unstaked successfully!") + + return nil +} diff --git a/client/collections/queue.go b/client/collections/queue.go new file mode 100644 index 00000000..7a2272cf --- /dev/null +++ b/client/collections/queue.go @@ -0,0 +1,173 @@ +package collections + +import ( + "context" + "errors" + + "cosmossdk.io/collections" + "cosmossdk.io/collections/codec" + + ierrors "github.com/piplabs/story/lib/errors" +) + +var ( + ErrPeek = errors.New("queue peek failed") + ErrEmptyQueue = errors.New("queue is empty") + ErrOutOfBoundsQueue = errors.New("queue index is out of bounds") +) + +const ( + QueueElementsNameSuffix = "_elements" + QueueFrontNameSuffix = "_front" + QueueRearNameSuffix = "_rear" + QueueElementsPrefixSuffix = 0x0 + QueueFrontPrefixSuffix = 0x1 + QueueRearPrefixSuffix = 0x2 +) + +func NewQueue[T any](sb *collections.SchemaBuilder, prefix collections.Prefix, name string, vc codec.ValueCodec[T]) Queue[T] { + return Queue[T]{ + front: collections.NewItem(sb, append(prefix, QueueFrontPrefixSuffix), name+QueueFrontNameSuffix, collections.Uint64Value), + rear: collections.NewItem(sb, append(prefix, QueueRearPrefixSuffix), name+QueueRearNameSuffix, collections.Uint64Value), + elements: collections.NewMap(sb, append(prefix, QueueElementsPrefixSuffix), name+QueueElementsNameSuffix, collections.Uint64Key, vc), + } +} + +type Queue[T any] struct { + front collections.Item[uint64] + rear collections.Item[uint64] + elements collections.Map[uint64, T] +} + +func (q Queue[T]) Initialize(ctx context.Context) error { + // initialize the front and rear + err := q.front.Set(ctx, 0) + if err != nil { + return ierrors.Wrap(err, "initialize set front") + } + err = q.rear.Set(ctx, 0) + if err != nil { + return ierrors.Wrap(err, "initialize set rear") + } + + return nil +} + +func (q Queue[T]) Enqueue(ctx context.Context, elem T) error { + rear, err := q.rear.Get(ctx) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return ierrors.Wrap(err, "enqueue rear get") + } + + err = q.elements.Set(ctx, rear, elem) + if err != nil { + return ierrors.Wrap(err, "enqueue elements set") + } + + err = q.rear.Set(ctx, rear+1) + if err != nil { + return ierrors.Wrap(err, "enqueue rear set") + } + + return nil +} + +func (q Queue[T]) Dequeue(ctx context.Context) (elem T, err error) { + if q.IsEmpty(ctx) { + return elem, ierrors.Wrap(ErrEmptyQueue, "dequeue") + } + + front, _ := q.front.Get(ctx) + elem, err = q.elements.Get(ctx, front) + if err != nil { + return elem, ierrors.Wrap(err, "dequeue elements get") + } + + err = q.elements.Remove(ctx, front) + if err != nil { + return elem, ierrors.Wrap(err, "dequeue elements remove") + } + + err = q.front.Set(ctx, front+1) + if err != nil { + return elem, ierrors.Wrap(err, "dequeue front set") + } + + return elem, nil +} + +func (q Queue[T]) Get(ctx context.Context, index uint64) (elem T, err error) { + // adjust the index to start at the front of the elements + front, _ := q.front.Get(ctx) + rear, _ := q.rear.Get(ctx) + if front+index >= rear { + return elem, ierrors.Wrap(ErrOutOfBoundsQueue, "get") + } + + elem, err = q.elements.Get(ctx, front+index) + if err != nil { + return elem, ierrors.Wrap(err, "get elements") + } + + return elem, nil +} + +func (q Queue[T]) Len(ctx context.Context) uint64 { + front, _ := q.front.Get(ctx) + rear, _ := q.rear.Get(ctx) + + return rear - front +} + +func (q Queue[T]) IsEmpty(ctx context.Context) bool { + return q.Len(ctx) == 0 +} + +func (q Queue[T]) Peek(ctx context.Context) (elem T, err error) { + if q.IsEmpty(ctx) { + return elem, ierrors.Wrap(ErrEmptyQueue, "peek") + } + + front, err := q.front.Get(ctx) + if err != nil { + return elem, ierrors.Wrap(err, "peek front get") + } + + elem, err = q.elements.Get(ctx, front) + if err != nil { + return elem, ierrors.Wrap(err, "peek elements get") + } + + return elem, nil +} + +func (q Queue[T]) Front(ctx context.Context) (uint64, error) { + item, err := q.front.Get(ctx) + if err != nil { + return 0, ierrors.Wrap(err, "front get") + } + + return item, nil +} + +func (q Queue[T]) Rear(ctx context.Context) (uint64, error) { + item, err := q.rear.Get(ctx) + if err != nil { + return 0, ierrors.Wrap(err, "rear get") + } + + return item, nil +} + +// TODO: Iterate with a custom range, clamp the range to the front and rear. +func (q Queue[T]) Iterate(ctx context.Context) (collections.Iterator[uint64, T], error) { + front, _ := q.front.Get(ctx) + rear, _ := q.rear.Get(ctx) + + iter, err := q.elements.Iterate(ctx, new(collections.Range[uint64]).StartInclusive(front).EndExclusive(rear)) + if err != nil { + return collections.Iterator[uint64, T]{}, ierrors.Wrap(err, "iterate") + } + + return iter, nil +} diff --git a/client/comet/comet.go b/client/comet/comet.go new file mode 100644 index 00000000..c6573af0 --- /dev/null +++ b/client/comet/comet.go @@ -0,0 +1,120 @@ +package comet + +import ( + "context" + + rpcclient "github.com/cometbft/cometbft/rpc/client" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/tracer" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const perPageConst = 100 + +var _ API = adapter{} + +type API interface { + // Validators returns the cometBFT validators at the given height or false if not + // available (probably due to snapshot sync after height). + Validators(ctx context.Context, height int64) (*cmttypes.ValidatorSet, bool, error) + + // IsValidator returns true if the given address is a validator at the latest height. + // It is best-effort, so returns false on any error. + IsValidator(ctx context.Context, valAddress common.Address) bool +} + +func NewAPI(cl rpcclient.Client) API { + return adapter{cl: cl} +} + +type adapter struct { + cl rpcclient.Client +} + +// IsValidator returns true if the given address is a validator at the latest height. +// It is best-effort, so returns false on any error. +func (a adapter) IsValidator(ctx context.Context, valAddress common.Address) bool { + ctx, span := tracer.Start(ctx, "comet/is_validator") + defer span.End() + + status, err := a.cl.Status(ctx) + if err != nil || status.SyncInfo.CatchingUp { + return false // Best effort + } + + valset, ok, err := a.Validators(ctx, status.SyncInfo.LatestBlockHeight) + if !ok || err != nil { + return false // Best effort + } + + for _, val := range valset.Validators { + addr, err := k1util.PubKeyToAddress(val.PubKey) + if err != nil { + continue // Best effort + } + + if addr == valAddress { + return true + } + } + + return false +} + +// Validators returns the cometBFT validators at the given height or false if not +// available (probably due to snapshot sync after height). +func (a adapter) Validators(ctx context.Context, height int64) (*cmttypes.ValidatorSet, bool, error) { + ctx, span := tracer.Start(ctx, "comet/validators", trace.WithAttributes(attribute.Int64("height", height))) + defer span.End() + + perPage := perPageConst // Can't take a pointer to a const directly. + + var vals []*cmttypes.Validator + for page := 1; ; page++ { // Pages are 1-indexed. + if page > 10 { // Sanity check. + return nil, false, errors.New("too many validators [BUG]") + } + + status, err := a.cl.Status(ctx) + if err != nil { + return nil, false, errors.Wrap(err, "fetch status") + } else if height < status.SyncInfo.EarliestBlockHeight { + // This can happen if height is before snapshot restore. + return nil, false, nil + } + + valResp, err := a.cl.Validators(ctx, &height, &page, &perPage) + if err != nil { + return nil, false, errors.Wrap(err, "fetch validators") + } + + for _, v := range valResp.Validators { + vals = append(vals, cmttypes.NewValidator(v.PubKey, v.VotingPower)) + } + + if len(vals) == valResp.Total { + break + } + } + + // cmttypes.NewValidatorSet() panics on error, so manually construct it for proper error handling. + valset := new(cmttypes.ValidatorSet) + if err := valset.UpdateWithChangeSet(vals); err != nil { + return nil, false, errors.Wrap(err, "update with change set") + } + if len(vals) > 0 { + valset.IncrementProposerPriority(1) // See cmttypes.NewValidatorSet + } + + if err := valset.ValidateBasic(); err != nil { + return nil, false, errors.Wrap(err, "validate basic") + } + + return valset, true, nil +} diff --git a/client/config/config.go b/client/config/config.go new file mode 100644 index 00000000..f80a381e --- /dev/null +++ b/client/config/config.go @@ -0,0 +1,222 @@ +package config + +import ( + "bytes" + "os" + "os/user" + "path/filepath" + "runtime" + "text/template" + "time" + + pruningtypes "cosmossdk.io/store/pruning/types" + + cmtos "github.com/cometbft/cometbft/libs/os" + db "github.com/cosmos/cosmos-db" + + "github.com/piplabs/story/lib/buildinfo" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tracer" + + _ "embed" +) + +const ( + configFile = "iliad.toml" + dataDir = "data" + configDir = "config" + snapshotDataDir = "snapshots" + networkFile = "network.json" + + DefaultEngineEndpoint = "http://localhost:8551" // Default host endpoint for the Engine API + defaultSnapshotInterval = 1000 // Roughly once an hour (given 3s blocks) + defaultSnapshotKeepRecent = 2 + defaultMinRetainBlocks = 0 // Retain all blocks + + defaultPruningOption = pruningtypes.PruningOptionNothing // Prune nothing + defaultDBBackend = db.GoLevelDBBackend + defaultEVMBuildDelay = time.Millisecond * 600 // 100ms longer than geth's --miner.recommit=500ms. + defaultEVMBuildOptimistic = true +) + +var ( + IliadConfig = Config{ + HomeDir: DefaultHomeDir(), + Network: "iliad", + EngineEndpoint: DefaultEngineEndpoint, + EngineJWTFile: DefaultJWTFile("iliad"), + SnapshotInterval: defaultSnapshotInterval, + SnapshotKeepRecent: defaultSnapshotKeepRecent, + BackendType: string(defaultDBBackend), + MinRetainBlocks: defaultMinRetainBlocks, + PruningOption: pruningtypes.PruningOptionDefault, + EVMBuildDelay: defaultEVMBuildDelay, + EVMBuildOptimistic: false, + APIEnable: false, + APIAddress: "127.0.0.1:1317", + EnableUnsafeCORS: false, + Tracer: tracer.DefaultConfig(), + } + LocalConfig = Config{ + HomeDir: DefaultHomeDir(), + Network: "local", + EngineEndpoint: DefaultEngineEndpoint, + EngineJWTFile: DefaultJWTFile("local"), + SnapshotInterval: defaultSnapshotInterval, + SnapshotKeepRecent: defaultSnapshotKeepRecent, + BackendType: string(defaultDBBackend), + MinRetainBlocks: defaultMinRetainBlocks, + PruningOption: pruningtypes.PruningOptionDefault, + EVMBuildDelay: defaultEVMBuildDelay, + EVMBuildOptimistic: false, + APIEnable: false, + APIAddress: "127.0.0.1:1317", + EnableUnsafeCORS: false, + Tracer: tracer.DefaultConfig(), + } +) + +// DefaultConfig returns the default iliad config. +func DefaultConfig() Config { + return Config{ + HomeDir: DefaultHomeDir(), + Network: "", // No default + EngineEndpoint: "http://localhost:8551", // No default + EngineJWTFile: "", // No default + SnapshotInterval: defaultSnapshotInterval, + SnapshotKeepRecent: defaultSnapshotKeepRecent, + BackendType: string(defaultDBBackend), + MinRetainBlocks: defaultMinRetainBlocks, + PruningOption: defaultPruningOption, + EVMBuildDelay: defaultEVMBuildDelay, + EVMBuildOptimistic: defaultEVMBuildOptimistic, + APIEnable: false, + APIAddress: "127.0.0.1:1317", + EnableUnsafeCORS: false, + Tracer: tracer.DefaultConfig(), + } +} + +// DefaultJWTFile returns the default engine-api jwt file assumed to be used by the execution client. +func DefaultJWTFile(network string) string { + return filepath.Join(baseDir(), "geth", network, "geth", "jwtsecret") +} + +// DefaultHomeDir returns the default consensus client home directory. +func DefaultHomeDir() string { + return filepath.Join(baseDir(), "story") +} + +// baseDir generates the base directory path for the "Story" application. +func baseDir() string { + home := homeDir() + if home != "" { + switch runtime.GOOS { + case "darwin": + return filepath.Join(home, "Library", "Story") + case "windows": + return filepath.Join(home, "AppData", "Roaming", "Story") + default: + return filepath.Join(home, ".story") + } + } + + return "" +} + +// Config defines all iliad specific config. +type Config struct { + HomeDir string + Network netconf.ID + EthKeyPassword string + EngineJWTFile string + EngineEndpoint string + SnapshotInterval uint64 // See cosmossdk.io/store/snapshots/types/options.go + SnapshotKeepRecent uint64 // See cosmossdk.io/store/snapshots/types/options.go + BackendType string // See cosmos-db/db.go + MinRetainBlocks uint64 + PruningOption string // See cosmossdk.io/store/pruning/types/options.go + EVMBuildDelay time.Duration + EVMBuildOptimistic bool + APIEnable bool + APIAddress string + EnableUnsafeCORS bool + Tracer tracer.Config +} + +// ConfigFile returns the default path to the toml iliad config file. +func (c Config) ConfigFile() string { + return filepath.Join(c.HomeDir, configDir, configFile) +} + +func (c Config) DataDir() string { + return filepath.Join(c.HomeDir, dataDir) +} + +func (c Config) AppStateDir() string { + return c.DataDir() // Maybe add a subdirectory for app state? +} + +func (c Config) SnapshotDir() string { + return filepath.Join(c.DataDir(), snapshotDataDir) +} + +func (c Config) Verify() error { + if c.EngineEndpoint == "" { + return errors.New("flag --engine-endpoint is empty") + } else if c.EngineJWTFile == "" { + return errors.New("flag --engine-jwt-file is empty") + } else if c.Network == "" { + return errors.New("flag --network is empty") + } else if err := c.Network.Verify(); err != nil { + return err + } + + return nil +} + +//go:embed config.toml.tmpl +var tomlTemplate []byte + +// WriteConfigTOML writes the toml iliad config to disk. +func WriteConfigTOML(cfg Config, logCfg log.Config) error { + var buffer bytes.Buffer + + t, err := template.New("").Parse(string(tomlTemplate)) + if err != nil { + return errors.Wrap(err, "parse template") + } + + s := struct { + Config + Log log.Config + Version string + }{ + Config: cfg, + Log: logCfg, + Version: buildinfo.Version(), + } + + if err := t.Execute(&buffer, s); err != nil { + return errors.Wrap(err, "execute template") + } + + if err := cmtos.WriteFile(cfg.ConfigFile(), buffer.Bytes(), 0o644); err != nil { + return errors.Wrap(err, "write config") + } + + return nil +} + +func homeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + if usr, err := user.Current(); err == nil { + return usr.HomeDir + } + + return "" +} diff --git a/client/config/config.toml.tmpl b/client/config/config.toml.tmpl new file mode 100644 index 00000000..c974491d --- /dev/null +++ b/client/config/config.toml.tmpl @@ -0,0 +1,94 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# The version of the Iliad binary that created or +# last modified the config file. Do not modify this. +version = "{{ .Version }}" + +# Iliad network to participate in: mainnet, testnet, or devnet. +network = "{{ .Network }}" + +####################################################################### +### Iliad Options ### +####################################################################### + +# Iliad execution client Engine API http endpoint. +engine-endpoint = "{{ .EngineEndpoint }}" + +# Iliad execution client JWT file used for authentication. +engine-jwt-file = "{{ .EngineJWTFile }}" + +# SnapshotInterval specifies the height interval at which iliad +# will take state sync snapshots. Defaults to 1000 (roughly once an hour), setting this to +# 0 disables state snapshots. +snapshot-interval = {{ .SnapshotInterval }} + +# snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). +snapshot-keep-recent = {{ .SnapshotKeepRecent }} + +# MinRetainBlocks defines the minimum block height offset from the current +# block being committed, such that all blocks past this offset are pruned +# from CometBFT. It is used as part of the process of determining the +# ResponseCommit.RetainHeight value during ABCI Commit. A value of 0 indicates +# that no blocks should be pruned. +# +# This configuration value is only responsible for pruning CometBFT blocks. +# It has no bearing on application state pruning which is determined by the +# "pruning-*" configurations. +# +# Note: CometBFT block pruning is dependent on this parameter in conjunction +# with the unbonding (safety threshold) period, state pruning and state sync +# snapshot parameters to determine the correct minimum value of +# ResponseCommit.RetainHeight. +min-retain-blocks = {{ .MinRetainBlocks }} + +# default: the last 72000 states are kept, pruning at 10 block intervals +# nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +# everything: 2 latest states will be kept; pruning at 10 block intervals. +pruning = "{{ .PruningOption }}" + +# AppDBBackend defines the database backend type to use for the application and snapshots DBs. +# An empty string indicates that a fallback will be used. +# The fallback is the db_backend value set in CometBFT's config.toml. +app-db-backend = "{{ .BackendType }}" + +# EVMBuildDelay defines the minimum delay between triggering a EVM payload build and fetching the result. +# This is a tradeoff between "high value blocks" and "fast consensus". +# It should be slightly higher than geth's --miner.recommit value. +evm-build-delay = "{{ .EVMBuildDelay }}" + +# EVMBuildOptimistic defines whether to trigger optimistic EVM payload building. +# If true, the EVM payload will be triggered on previous finalisation. This allows +# more time for block building while ensuring faster consensus blocks. +evm-build-optimistic = {{ .EVMBuildOptimistic }} + +# APIEnable defines if the API server should be enabled. +api-enable = {{ .APIEnable }} + +# APIAddress defines the API server address to listen on. +api-address = "{{ .APIAddress }}" + +# EnableUnsafeCORS defines whether to enable CORS for API server. +enabled-unsafe-cors = {{ .EnableUnsafeCORS }} + +####################################################################### +### Logging Options ### +####################################################################### + +[log] +# Logging level. Note cometBFT internal logs are configured in config.yaml. +# Options are: debug, info, warn, error. +level = "{{ .Log.Level }}" + +# Logging format. Options are: console, json. +format = "{{ .Log.Format }}" + +# Logging color if console format is chosen. Options are: auto, force, disable. +color = "{{ .Log.Color }}" + +[tracing] +# Open Telemetry OTLP endpoint URL. See https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp. +endpoint = "{{ .Tracer.Endpoint }}" + +# Open Telemetry OTLP headers. See https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/setup/quickstart/go/. +headers = "{{ .Tracer.Headers }}" diff --git a/client/config/testdata/default.toml b/client/config/testdata/default.toml new file mode 100644 index 00000000..996c7fc1 --- /dev/null +++ b/client/config/testdata/default.toml @@ -0,0 +1,94 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# The version of the Iliad binary that created or +# last modified the config file. Do not modify this. +version = "v0.1.5" + +# Iliad network to participate in: mainnet, testnet, or devnet. +network = "" + +####################################################################### +### Iliad Options ### +####################################################################### + +# Iliad execution client Engine API http endpoint. +engine-endpoint = "" + +# Iliad execution client JWT file used for authentication. +engine-jwt-file = "" + +# SnapshotInterval specifies the height interval at which iliad +# will take state sync snapshots. Defaults to 1000 (roughly once an hour), setting this to +# 0 disables state snapshots. +snapshot-interval = 1000 + +# snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). +snapshot-keep-recent = 2 + +# MinRetainBlocks defines the minimum block height offset from the current +# block being committed, such that all blocks past this offset are pruned +# from CometBFT. It is used as part of the process of determining the +# ResponseCommit.RetainHeight value during ABCI Commit. A value of 0 indicates +# that no blocks should be pruned. +# +# This configuration value is only responsible for pruning CometBFT blocks. +# It has no bearing on application state pruning which is determined by the +# "pruning-*" configurations. +# +# Note: CometBFT block pruning is dependent on this parameter in conjunction +# with the unbonding (safety threshold) period, state pruning and state sync +# snapshot parameters to determine the correct minimum value of +# ResponseCommit.RetainHeight. +min-retain-blocks = 0 + +# default: the last 72000 states are kept, pruning at 10 block intervals +# nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +# everything: 2 latest states will be kept; pruning at 10 block intervals. +pruning = "nothing" + +# AppDBBackend defines the database backend type to use for the application and snapshots DBs. +# An empty string indicates that a fallback will be used. +# The fallback is the db_backend value set in CometBFT's config.toml. +app-db-backend = "goleveldb" + +# EVMBuildDelay defines the minimum delay between triggering a EVM payload build and fetching the result. +# This is a tradeoff between "high value blocks" and "fast consensus". +# It should be slightly higher than geth's --miner.recommit value. +evm-build-delay = "600ms" + +# EVMBuildOptimistic defines whether to trigger optimistic EVM payload building. +# If true, the EVM payload will be triggered on previous finalisation. This allows +# more time for block building while ensuring faster consensus blocks. +evm-build-optimistic = true + +# APIEnable defines if the API server should be enabled. +api-enable = false + +# APIAddress defines the API server address to listen on. +api-address = "127.0.0.1:1317" + +# EnableUnsafeCORS defines whether to enable CORS for API server. +enabled-unsafe-cors = false + +####################################################################### +### Logging Options ### +####################################################################### + +[log] +# Logging level. Note cometBFT internal logs are configured in config.yaml. +# Options are: debug, info, warn, error. +level = "info" + +# Logging format. Options are: console, json. +format = "console" + +# Logging color if console format is chosen. Options are: auto, force, disable. +color = "auto" + +[tracing] +# Open Telemetry OTLP endpoint URL. See https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp. +endpoint = "" + +# Open Telemetry OTLP headers. See https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/setup/quickstart/go/. +headers = "" diff --git a/client/config/testdata/iliad.toml b/client/config/testdata/iliad.toml new file mode 100644 index 00000000..0d23ee65 --- /dev/null +++ b/client/config/testdata/iliad.toml @@ -0,0 +1,94 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# The version of the Iliad binary that created or +# last modified the config file. Do not modify this. +version = "v0.1.5" + +# Iliad network to participate in: mainnet, testnet, or devnet. +network = "iliad" + +####################################################################### +### Iliad Options ### +####################################################################### + +# Iliad execution client Engine API http endpoint. +engine-endpoint = "http://localhost:8551" + +# Iliad execution client JWT file used for authentication. +engine-jwt-file = "" + +# SnapshotInterval specifies the height interval at which iliad +# will take state sync snapshots. Defaults to 1000 (roughly once an hour), setting this to +# 0 disables state snapshots. +snapshot-interval = 1000 + +# snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). +snapshot-keep-recent = 2 + +# MinRetainBlocks defines the minimum block height offset from the current +# block being committed, such that all blocks past this offset are pruned +# from CometBFT. It is used as part of the process of determining the +# ResponseCommit.RetainHeight value during ABCI Commit. A value of 0 indicates +# that no blocks should be pruned. +# +# This configuration value is only responsible for pruning CometBFT blocks. +# It has no bearing on application state pruning which is determined by the +# "pruning-*" configurations. +# +# Note: CometBFT block pruning is dependent on this parameter in conjunction +# with the unbonding (safety threshold) period, state pruning and state sync +# snapshot parameters to determine the correct minimum value of +# ResponseCommit.RetainHeight. +min-retain-blocks = 0 + +# default: the last 72000 states are kept, pruning at 10 block intervals +# nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +# everything: 2 latest states will be kept; pruning at 10 block intervals. +pruning = "default" + +# AppDBBackend defines the database backend type to use for the application and snapshots DBs. +# An empty string indicates that a fallback will be used. +# The fallback is the db_backend value set in CometBFT's config.toml. +app-db-backend = "goleveldb" + +# EVMBuildDelay defines the minimum delay between triggering a EVM payload build and fetching the result. +# This is a tradeoff between "high value blocks" and "fast consensus". +# It should be slightly higher than geth's --miner.recommit value. +evm-build-delay = "600ms" + +# EVMBuildOptimistic defines whether to trigger optimistic EVM payload building. +# If true, the EVM payload will be triggered on previous finalisation. This allows +# more time for block building while ensuring faster consensus blocks. +evm-build-optimistic = false + +# APIEnable defines if the API server should be enabled. +api-enable = false + +# APIAddress defines the API server address to listen on. +api-address = "127.0.0.1:1317" + +# EnableUnsafeCORS defines whether to enable CORS for API server. +enabled-unsafe-cors = false + +####################################################################### +### Logging Options ### +####################################################################### + +[log] +# Logging level. Note cometBFT internal logs are configured in config.yaml. +# Options are: debug, info, warn, error. +level = "info" + +# Logging format. Options are: console, json. +format = "console" + +# Logging color if console format is chosen. Options are: auto, force, disable. +color = "auto" + +[tracing] +# Open Telemetry OTLP endpoint URL. See https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp. +endpoint = "" + +# Open Telemetry OTLP headers. See https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/setup/quickstart/go/. +headers = "" diff --git a/client/genutil/defaults.go b/client/genutil/defaults.go new file mode 100644 index 00000000..782865b0 --- /dev/null +++ b/client/genutil/defaults.go @@ -0,0 +1,12 @@ +package genutil + +import "github.com/cometbft/cometbft/types" + +// DefaultConsensusParams returns the default cometBFT consensus params for iliad protocol. +func DefaultConsensusParams() *types.ConsensusParams { + resp := types.DefaultConsensusParams() + resp.ABCI.VoteExtensionsEnableHeight = 1 // Enable vote extensions from the start. + resp.Validator.PubKeyTypes = []string{types.ABCIPubKeyTypeSecp256k1} // Only k1 keys. + + return resp +} diff --git a/client/genutil/evm/evm.go b/client/genutil/evm/evm.go new file mode 100644 index 00000000..db610fd3 --- /dev/null +++ b/client/genutil/evm/evm.go @@ -0,0 +1,133 @@ +package evm + +import ( + "math/big" + + "cosmossdk.io/math" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/miner" + "github.com/ethereum/go-ethereum/params" + + "github.com/piplabs/story/client/genutil/evm/predeploys" + "github.com/piplabs/story/lib/anvil" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/netconf" +) + +//nolint:unused // added for potential future usage +var eth1k = math.NewInt(1000).MulRaw(params.Ether).BigInt() +var eth1m = math.NewInt(1000000).MulRaw(params.Ether).BigInt() + +func newUint64(val uint64) *uint64 { return &val } + +// MakeGenesis returns a genesis block for a development chain. +// See geth reference: https://github.com/ethereum/go-ethereum/blob/master/core/genesis.go#L564 +func MakeGenesis(network netconf.ID) (core.Genesis, error) { + predeps, err := predeploys.Alloc(network) + if err != nil { + return core.Genesis{}, errors.Wrap(err, "predeploys") + } + + allocs := mergeAllocs(precompilesAlloc(), predeps) + + if network.IsEphemeral() { + allocs = mergeAllocs(allocs, stagingPrefundAlloc()) + } else if network == netconf.Testnet { + allocs = mergeAllocs(allocs, testnetPrefundAlloc()) + } else { + return core.Genesis{}, errors.New("unsupported network", "network", network.String()) + } + + return core.Genesis{ + Config: defaultChainConfig(network), + GasLimit: miner.DefaultConfig.GasCeil, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(0), + Alloc: allocs, + }, nil +} + +// defaultChainConfig returns the default chain config for a network. +// See geth reference: https://github.com/ethereum/go-ethereum/blob/master/params/config.go#L65 +func defaultChainConfig(network netconf.ID) *params.ChainConfig { + return ¶ms.ChainConfig{ + ChainID: big.NewInt(int64(network.Static().IliadExecutionChainID)), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + GrayGlacierBlock: big.NewInt(0), + ShanghaiTime: newUint64(0), + CancunTime: newUint64(0), + TerminalTotalDifficulty: big.NewInt(0), + TerminalTotalDifficultyPassed: true, + } +} + +// precompilesAlloc returns allocs for precompiled contracts +// TODO: this matches go-ethereum's precompiles, but we should understand why balances are set to 1. +func precompilesAlloc() types.GenesisAlloc { + return types.GenesisAlloc{ + common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover + common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256 + common.BytesToAddress([]byte{3}): {Balance: big.NewInt(1)}, // RIPEMD + common.BytesToAddress([]byte{4}): {Balance: big.NewInt(1)}, // Identity + common.BytesToAddress([]byte{5}): {Balance: big.NewInt(1)}, // ModExp + common.BytesToAddress([]byte{6}): {Balance: big.NewInt(1)}, // ECAdd + common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul + common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing + common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b + } +} + +// devPrefundAlloc returns allocs for pre-funded geth dev accounts. +func stagingPrefundAlloc() types.GenesisAlloc { + return types.GenesisAlloc{ + // anvil pre-funded accounts + anvil.DevAccount0(): {Balance: eth1m}, + anvil.DevAccount1(): {Balance: eth1m}, + anvil.DevAccount2(): {Balance: eth1m}, + anvil.DevAccount3(): {Balance: eth1m}, + anvil.DevAccount4(): {Balance: eth1m}, + anvil.DevAccount5(): {Balance: eth1m}, + anvil.DevAccount6(): {Balance: eth1m}, + anvil.DevAccount7(): {Balance: eth1m}, + anvil.DevAccount8(): {Balance: eth1m}, + anvil.DevAccount9(): {Balance: eth1m}, + + // TODO: team accounts + common.HexToAddress("0x0000000000000000000000000000000000000000"): {Balance: eth1m}, + } +} + +func testnetPrefundAlloc() types.GenesisAlloc { + return types.GenesisAlloc{ + // TODO: team accounts + common.HexToAddress("0x0000000000000000000000000000000000000000"): {Balance: eth1m}, + + // TODO: add validators + } +} + +// mergeAllocs merges multiple allocs into one. +func mergeAllocs(allocs ...types.GenesisAlloc) types.GenesisAlloc { + merged := make(types.GenesisAlloc) + for _, alloc := range allocs { + for addr, account := range alloc { + merged[addr] = account + } + } + + return merged +} diff --git a/client/genutil/evm/evm_test.go b/client/genutil/evm/evm_test.go new file mode 100644 index 00000000..fa77552f --- /dev/null +++ b/client/genutil/evm/evm_test.go @@ -0,0 +1,23 @@ +package evm_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/client/genutil/evm" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" + + _ "github.com/piplabs/story/client/app" // To init SDK config. +) + +//go:generate go test . -golden -clean + +func TestMakeGenesis(t *testing.T) { + t.Parallel() + + genesis, err := evm.MakeGenesis(netconf.Staging) + require.NoError(t, err) + tutil.RequireGoldenJSON(t, genesis) +} diff --git a/client/genutil/evm/predeploys/predeploys.go b/client/genutil/evm/predeploys/predeploys.go new file mode 100644 index 00000000..b9c7e4e4 --- /dev/null +++ b/client/genutil/evm/predeploys/predeploys.go @@ -0,0 +1,133 @@ +//nolint:unused // fix with proper predeploy script +package predeploys + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/genutil/evm/state" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/solc" +) + +const ( + // IliadNamespace is namespace of for iliad specific predeploys. + IliadNamespace = "0x121E240000000000000000000000000000000000" + + // IPTokenNamespace is namespace of for IP Token specific predeploys. + IPTokenNamespace = "0xcccccc0000000000000000000000000000000000" + + // NamespaceSize is the number of proxies to deploy per namespace. + NamespaceSize = 2048 + + // IP Token Predeploys. + IPTokenStaking = "0xcccccc0000000000000000000000000000000001" + IPTokenSlashing = "0xcccccc0000000000000000000000000000000002" + UpgradeEntrypoint = "0xcccccc0000000000000000000000000000000003" + + Secp256k1 = "0x00000000000000000000000000000000000256f1" + + // TransparentUpgradeableProxy storage slots. + // ProxyImplementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + // ProxyAdminSlot = "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103". +) + +var ( + // Namespace big.Ints. + iliadNamespace = common.HexToAddress(IliadNamespace).Big() + ipTokenNamespace = common.HexToAddress(IPTokenNamespace).Big() + + // Predeploy addresses. + ipTokenStaking = common.HexToAddress(IPTokenStaking) + ipTokenSlashing = common.HexToAddress(IPTokenSlashing) + upgradeEntrypoint = common.HexToAddress(UpgradeEntrypoint) + + // Predeploy bytecodes. + ipTokenStakingCode = hexutil.MustDecode(bindings.IPTokenStakingDeployedBytecode) +) + +// Alloc returns the genesis allocs for the predeployed contracts, initializing code and storage. +func Alloc(_ netconf.ID) (types.GenesisAlloc, error) { + emptyGenesis := &core.Genesis{Alloc: types.GenesisAlloc{}} + + db := state.NewMemDB(emptyGenesis) + + // setProxies(db) + + // admin, err := eoa.Admin(network) + // if err != nil { + // return nil, errors.Wrap(err, "network admin") + //} + + if err := setStaking(db); err != nil { + return nil, errors.Wrap(err, "set staking") + } + + if err := setSlashing(db); err != nil { + return nil, errors.Wrap(err, "set slashing") + } + + return db.Genesis().Alloc, nil +} + +// setStaking sets the Staking predeploy. +func setStaking(db *state.MemDB) error { + storage := state.StorageValues{} + + return setPredeploy(db, ipTokenStaking, ipTokenStakingCode, bindings.IPTokenStakingStorageLayout, storage) +} + +// setSlashing sets the Slashing predeploy. +// TODO: Slashing design. +func setSlashing(db *state.MemDB) error { + storage := state.StorageValues{} + + return setPredeploy(db, ipTokenSlashing, ipTokenStakingCode, bindings.IPTokenStakingStorageLayout, storage) +} + +// setPredeploy sets the implementation code and proxy storage for the given predeploy. +func setPredeploy(db *state.MemDB, proxy common.Address, code []byte, layout solc.StorageLayout, storage state.StorageValues) error { + impl := impl(proxy) + // setProxyImplementation(db, proxy, impl) + db.SetCode(impl, code) + + return setStorage(db, proxy, layout, storage) +} + +// setStorage sets the code and storage for the given predeploy. +func setStorage(db *state.MemDB, addr common.Address, layout solc.StorageLayout, storage state.StorageValues) error { + slots, err := state.EncodeStorage(layout, storage) + if err != nil { + return errors.Wrap(err, "encode storage", "addr", addr) + } + + for _, slot := range slots { + db.SetState(addr, slot.Key, slot.Value) + } + + return nil +} + +// setProxyImplementation sets the implementation address for the given proxy address. +// func setProxyImplementation(db *state.MemDB, proxy, impl common.Address) { +// db.SetState(proxy, common.HexToHash(ProxyImplementationSlot), common.HexToHash(impl.Hex())) +//} + +// impl returns the implementation address for the given proxy address. +func impl(addr common.Address) common.Address { + // To get a unique implementation per each proxy address, we subtract the address from the max address. + // Max address is odd, so the result will be unique. + maxAddr := common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff").Big() + return common.BigToAddress(new(big.Int).Sub(maxAddr, addr.Big())) +} + +//nolint:unused // address returns the address at the given index in the namespace. +func address(namespace *big.Int, i int) common.Address { + return common.BigToAddress(new(big.Int).Add(namespace, big.NewInt(int64(i)))) +} diff --git a/client/genutil/evm/predeploys/predeploys_test.go b/client/genutil/evm/predeploys/predeploys_test.go new file mode 100644 index 00000000..55d9cee1 --- /dev/null +++ b/client/genutil/evm/predeploys/predeploys_test.go @@ -0,0 +1,102 @@ +//nolint:unused // fix with proper predeploy test +package predeploys_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/piplabs/story/client/genutil/evm/predeploys" + "github.com/piplabs/story/contracts/bindings" +) + +var ( + // Namespace big.Ints. + mainNamespace = addrToBig(common.HexToAddress(predeploys.IliadNamespace)) + + // Predeploy addresses. + ipTokenStaking = common.HexToAddress(predeploys.IPTokenStaking) + + // Predeploy bytecodes. + ipTokenStakingCode = mustDecodeHex(bindings.IPTokenStakingDeployedBytecode) + ipTokenSlashingCode = mustDecodeHex(bindings.IPTokenSlashingDeployedBytecode) +) + +func TestAlloc(t *testing.T) { + t.Parallel() + + // predeps, err := predeploys.Alloc(netconf.Staging) + // require.NoError(t, err) + + // Check namespace filled with proxies. + // for i := 0; i < predeploys.NamespaceSize; i++ { + // // add one, so that we don't set the namespace zero address + // addr := namespaceAddr(mainNamespace, i+1) + // + // // proxy admin is not a proxy + // if addr == proxyAdmin { + // continue + // } + // + // proxyAloc, ok := predeps[addr] + // require.Truef(t, ok, "proxy not found") + // require.Equalf(t, + // hexutil.Encode(proxyCode), + // hexutil.Encode(proxyAloc.Code), + // "proxy code mismatch") + // require.Equalf(t, + // common.HexToHash(predeploys.ProxyAdmin), + // proxyAloc.Storage[common.HexToHash(predeploys.ProxyAdminSlot)], + // "proxy admin slot empty") + //} + // + //// check ProxyAdmin + // proxyAdminAlloc, ok := predeps[proxyAdmin] + // require.True(t, ok, "proxy admin not found") + // require.Equal(t, + // proxyAdminCode, + // proxyAdminAlloc.Code, + // "proxy admin code mismatch") + // + //// check IPTokenStaking proxy + // ipTokenStakingAlloc, ok := predeps[ipTokenStaking] + // require.True(t, ok, "iliad stake not found") + // require.Equal(t, + // proxyCode, + // ipTokenStakingAlloc.Code, + // "iliad stake code mismatch") + // + //// check IPTokenStaking implementation + // ipTokenStakingImpl, ok := ipTokenStakingAlloc.Storage[common.HexToHash(predeploys.ProxyImplementationSlot)] + // require.True(t, ok, "iliad stake implementation not found") + // + // ipTokenStakingImplAlloc, ok := predeps[common.BytesToAddress(ipTokenStakingImpl.Bytes())] + // require.True(t, ok, "iliad stake implementation not found") + // + // require.Equal(t, + // ipTokenStakingCode, + // ipTokenStakingImplAlloc.Code, + // "iliad stake implementation mismatch") +} + +// namespaceAddr returns the address at the given index in the namespace. +func namespaceAddr(namespace *big.Int, i int) common.Address { + return common.BigToAddress(new(big.Int).Add(namespace, big.NewInt(int64(i)))) +} + +// addrToBig converts an address to a big.Int. +func addrToBig(addr common.Address) *big.Int { + return new(big.Int).SetBytes(addr.Bytes()) +} + +// mustDecodeHex decodes the given hex string. It panics on error. +func mustDecodeHex(hex string) []byte { + b, err := hexutil.Decode(hex) + if err != nil { + panic(err) + } + + return b +} diff --git a/client/genutil/evm/state/memdb.go b/client/genutil/evm/state/memdb.go new file mode 100644 index 00000000..99e418f6 --- /dev/null +++ b/client/genutil/evm/state/memdb.go @@ -0,0 +1,60 @@ +package state + +import ( + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" +) + +// MemDB is an in stateful wrapper around core.Genesis, that makes it easier to build genesis state. +type MemDB struct { + rw sync.RWMutex + genesis *core.Genesis +} + +func NewMemDB(genesis *core.Genesis) *MemDB { + return &MemDB{ + genesis: genesis, + rw: sync.RWMutex{}, + } +} + +func (db *MemDB) Genesis() *core.Genesis { + return db.genesis +} + +func (db *MemDB) SetCode(addr common.Address, code []byte) { + db.rw.Lock() + defer db.rw.Unlock() + + account := db.getOrCreate(addr) + account.Code = code + db.genesis.Alloc[addr] = account +} + +func (db *MemDB) SetState(addr common.Address, key, value common.Hash) { + db.rw.Lock() + defer db.rw.Unlock() + + account := db.getOrCreate(addr) + account.Storage[key] = value + db.genesis.Alloc[addr] = account +} + +func (db *MemDB) getOrCreate(addr common.Address) types.Account { + account, ok := db.genesis.Alloc[addr] + + if !ok { + return types.Account{ + Code: []byte{}, + Storage: make(map[common.Hash]common.Hash), + Balance: big.NewInt(0), + Nonce: 0, + } + } + + return account +} diff --git a/client/genutil/evm/state/state.go b/client/genutil/evm/state/state.go new file mode 100644 index 00000000..fc5ae293 --- /dev/null +++ b/client/genutil/evm/state/state.go @@ -0,0 +1,68 @@ +package state + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/solc" +) + +type StorageValues map[string]any + +type StorageSlot struct { + Key common.Hash + Value common.Hash +} + +// EncodeStorage encodes the given storage values according to the given storage layout. +// If it does not find a slot for a label, it returns an error. +// If it does not support encoding a value, it returns an error. +func EncodeStorage(layout solc.StorageLayout, values StorageValues) ([]StorageSlot, error) { + var slots []StorageSlot + for label, value := range values { + slot, ok := solc.SlotOf(layout, label) + if !ok { + return nil, errors.New("label not found", "label", label) + } + + s, err := encodeStorage(slot, value) + if err != nil { + return nil, err + } + + slots = append(slots, s) + } + + return slots, nil +} + +func encodeStorage(slot uint, value any) (StorageSlot, error) { + key := encodeSlot(slot) + v, err := encodeValue(value) + if err != nil { + return StorageSlot{}, err + } + + return StorageSlot{Key: key, Value: v}, nil +} + +func encodeSlot(slot uint) common.Hash { + s := new(big.Int).SetUint64(uint64(slot)) + + return common.BigToHash(s) +} + +func encodeValue(value any) (common.Hash, error) { + switch v := value.(type) { + case []byte: + return common.BytesToHash(v), nil + case *big.Int: + return common.BigToHash(v), nil + case common.Address: + return common.HexToHash(v.Hex()), nil + default: + return common.Hash{}, errors.New("unsupported type") + } +} diff --git a/client/genutil/evm/testdata/TestMakeGenesis.golden b/client/genutil/evm/testdata/TestMakeGenesis.golden new file mode 100644 index 00000000..ce5ac0cc --- /dev/null +++ b/client/genutil/evm/testdata/TestMakeGenesis.golden @@ -0,0 +1,105 @@ +{ + "config": { + "chainId": 1651, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x", + "gasLimit": "0x1c9c380", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0000000000000000000000000000000000000000": { + "balance": "0xd3c21bcecceda1000000" + }, + "0000000000000000000000000000000000000001": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000002": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000003": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000004": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000005": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000006": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000007": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000008": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000009": { + "balance": "0x1" + }, + "14dc79964da2c08b23698b3d3cc7ca32193d9955": { + "balance": "0xd3c21bcecceda1000000" + }, + "15d34aaf54267db7d7c367839aaf71a00a2c6a65": { + "balance": "0xd3c21bcecceda1000000" + }, + "23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": { + "balance": "0xd3c21bcecceda1000000" + }, + "333333fffffffffffffffffffffffffffffffffd": { + "code": "0x6080604052600436106101f95760003560e01c806386eec4a11161010d578063c24ae586116100a0578063eee5cead1161006f578063eee5cead14610652578063f188768414610672578063f2fde38b14610688578063fc2e5932146106a8578063fc56c2a2146106bb57600080fd5b8063c24ae586146105a7578063d2e1f5b8146105df578063e30c397814610614578063eb4af0451461063257600080fd5b80639855c8b5116100dc5780639855c8b5146104ff578063a1cb18461461051f578063b8db983e1461053f578063bda16b151461057357600080fd5b806386eec4a1146104755780638d3e1e41146104885780638da5cb5b146104ba5780638f37ec19146104ec57600080fd5b80635706750311610190578063715018a61161015f578063715018a6146103de578063787f82c8146103f357806379ba5097146104135780637b6e842c1461042857806383dffd6f1461044857600080fd5b806357067503146103505780635a69825d146103885780635d5ab9681461039e5780636ea3a228146103be57600080fd5b80632ebc6034116101cc5780632ebc6034146102be57806339ec4df91461030757806348903e381461031d57806353972c2a1461033057600080fd5b8063057b9296146101fe578063060ceab01461022057806317e42e12146102495780632d1e973e14610269575b600080fd5b34801561020a57600080fd5b5061021e610219366004612d1f565b6106ef565b005b34801561022c57600080fd5b5061023660055481565b6040519081526020015b60405180910390f35b34801561025557600080fd5b5061021e610264366004612d1f565b610854565b34801561027557600080fd5b50610236610284366004612e35565b8151602081840181018051600882529282019482019490942091909352815180830184018051928152908401929093019190912091525481565b3480156102ca57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b60405163ffffffff9091168152602001610240565b34801561031357600080fd5b5061023660035481565b61021e61032b366004612e98565b6109a6565b34801561033c57600080fd5b5061021e61034b366004612ed9565b610b0e565b34801561035c57600080fd5b5061023661036b366004612f13565b805160208183018101805160078252928201919093012091525481565b34801561039457600080fd5b5061023660045481565b3480156103aa57600080fd5b5061021e6103b9366004612f4f565b610e2c565b3480156103ca57600080fd5b5061021e6103d9366004612fc2565b61104e565b3480156103ea57600080fd5b5061021e6110f5565b3480156103ff57600080fd5b5061021e61040e366004612d1f565b611109565b34801561041f57600080fd5b5061021e6113bf565b34801561043457600080fd5b5061021e610443366004612ed9565b611403565b34801561045457600080fd5b50610468610463366004612e98565b611753565b6040516102409190612fdb565b61021e610483366004613028565b611786565b34801561049457600080fd5b506104a86104a3366004612f13565b61199a565b604051610240969594939291906130e3565b3480156104c657600080fd5b506000546001600160a01b03165b6040516001600160a01b039091168152602001610240565b61021e6104fa366004613028565b611a76565b34801561050b57600080fd5b5061021e61051a366004612fc2565b611c38565b34801561052b57600080fd5b5061021e61053a366004612f4f565b611ce2565b34801561054b57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b34801561057f57600080fd5b506102367f000000000000000000000000000000000000000000000000000000000000000081565b3480156105b357600080fd5b506102366105c2366004612f13565b8051602081830181018051600a8252928201919093012091525481565b3480156105eb57600080fd5b506105ff6105fa366004612fc2565b611ee6565b60408051928352602083019190915201610240565b34801561062057600080fd5b506001546001600160a01b03166104d4565b34801561063e57600080fd5b5061021e61064d366004612fc2565b611f26565b34801561065e57600080fd5b5061021e61066d366004612fc2565b611fcb565b34801561067e57600080fd5b5061023660025481565b34801561069457600080fd5b5061021e6106a336600461312b565b61204e565b61021e6106b636600461315a565b6120bf565b3480156106c757600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b8282336041821461071b5760405162461bcd60e51b8152600401610712906131fb565b60405180910390fd5b8282600081811061072e5761072e613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146107645760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610778848461222d565b6001600160a01b03161461079e5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906107da908a908a90600401613312565b600060405180830381865af41580156107f7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261081f9190810190613326565b905061084a85600983604051610835919061339c565b9081526040519081900360200190209061225a565b5050505050505050565b828233604182146108775760405162461bcd60e51b8152600401610712906131fb565b8282600081811061088a5761088a613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146108c05760405162461bcd60e51b815260040161071290613256565b806001600160a01b03166108d4848461222d565b6001600160a01b0316146108fa5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90610936908a908a90600401613312565b600060405180830381865af4158015610953573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261097b9190810190613326565b905061084a85600983604051610991919061339c565b9081526040519081900360200190209061226f565b8181602181146109c85760405162461bcd60e51b8152600401610712906131fb565b818160008181106109db576109db613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610a29575081816000818110610a0e57610a0e613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610a455760405162461bcd60e51b815260040161071290613256565b610b0884848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250506040805180820190915260098152683b30b634b230ba37b960b91b602082015291507f000000000000000000000000000000000000000000000000000000000000000090507f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000612284565b50505050565b610b1881806133b8565b60418114610b385760405162461bcd60e51b8152600401610712906131fb565b81816000818110610b4b57610b4b613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610b815760405162461bcd60e51b815260040161071290613256565b610b8e60208401846133b8565b60218114610bae5760405162461bcd60e51b8152600401610712906131fb565b81816000818110610bc157610bc1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610c0f575081816000818110610bf457610bf4613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610c2b5760405162461bcd60e51b815260040161071290613256565b60068282604051610c3d9291906133fe565b9081526040519081900360200190205460ff16610c6c5760405162461bcd60e51b81526004016107129061340e565b610c7960408601866133b8565b60218114610c995760405162461bcd60e51b8152600401610712906131fb565b81816000818110610cac57610cac613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610cfa575081816000818110610cdf57610cdf613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610d165760405162461bcd60e51b815260040161071290613256565b60068282604051610d289291906133fe565b9081526040519081900360200190205460ff16610d575760405162461bcd60e51b81526004016107129061340e565b60007300000000000000000000000000000000000256f1636889b16e610d7d8a806133b8565b6040518363ffffffff1660e01b8152600401610d9a929190613312565b600060405180830381865af4158015610db7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610ddf9190810190613326565b90506000610df08960600135611ee6565b509050610dfd82336124e6565b610e2182610e0e60208c018c6133b8565b610e1b60408e018e6133b8565b86612571565b505050505050505050565b84843360418214610e4f5760405162461bcd60e51b8152600401610712906131fb565b82826000818110610e6257610e62613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610e985760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610eac848461222d565b6001600160a01b031614610ed25760405162461bcd60e51b81526004016107129061329b565b858560218114610ef45760405162461bcd60e51b8152600401610712906131fb565b81816000818110610f0757610f07613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610f55575081816000818110610f3a57610f3a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610f715760405162461bcd60e51b815260040161071290613256565b60068282604051610f839291906133fe565b9081526040519081900360200190205460ff16610fb25760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90610fee908e908e90600401613312565b600060405180830381865af415801561100b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110339190810190613326565b9050611041818a8a8a61272f565b5050505050505050505050565b61105661288f565b600081116110bb5760405162461bcd60e51b815260206004820152602c60248201527f4950546f6b656e5374616b696e673a206d696e556e7374616b65416d6f756e7460448201526b02063616e6e6f7420626520360a41b6064820152608401610712565b6110e57f000000000000000000000000000000000000000000000000000000000000000082613456565b6110ef908261348e565b60035550565b6110fd61288f565b61110760006128bc565b565b8282336041821461112c5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061113f5761113f613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146111755760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611189848461222d565b6001600160a01b0316146111af5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906111eb908a908a90600401613312565b600060405180830381865af4158015611208573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526112309190810190613326565b90506000600782604051611244919061339c565b908152602001604051809103902054116112b25760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2044656c656761746f72206d7573742068604482015268617665207374616b6560b81b6064820152608401610712565b42600554600a836040516112c6919061339c565b9081526020016040518091039020546112df91906134a1565b106113485760405162461bcd60e51b815260206004820152603360248201527f4950546f6b656e5374616b696e673a205769746864726177616c20616464726560448201527239b99031b430b733b29031b7b7b616b237bbb760691b6064820152608401610712565b42600a82604051611359919061339c565b9081526020016040518091039020819055507f9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca818660601b6bffffffffffffffffffffffff19166040516113ae9291906134b4565b60405180910390a150505050505050565b60015433906001600160a01b031681146113f75760405163118cdaa760e01b81526001600160a01b0382166004820152602401610712565b611400816128bc565b50565b61140d81806133b8565b336041821461142e5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061144157611441613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146114775760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661148b848461222d565b6001600160a01b0316146114b15760405162461bcd60e51b81526004016107129061329b565b6114be60208501856133b8565b602181146114de5760405162461bcd60e51b8152600401610712906131fb565b818160008181106114f1576114f1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061153f57508181600081811061152457611524613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b61155b5760405162461bcd60e51b815260040161071290613256565b6006828260405161156d9291906133fe565b9081526040519081900360200190205460ff1661159c5760405162461bcd60e51b81526004016107129061340e565b6115a960408701876133b8565b602181146115c95760405162461bcd60e51b8152600401610712906131fb565b818160008181106115dc576115dc613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061162a57508181600081811061160f5761160f613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6116465760405162461bcd60e51b815260040161071290613256565b600682826040516116589291906133fe565b9081526040519081900360200190205460ff166116875760405162461bcd60e51b81526004016107129061340e565b60006116968960600135611ee6565b50905060007300000000000000000000000000000000000256f1636889b16e6116bf8c806133b8565b6040518363ffffffff1660e01b81526004016116dc929190613312565b600060405180830381865af41580156116f9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526117219190810190613326565b90506117478161173460208d018d6133b8565b61174160408f018f6133b8565b87612571565b50505050505050505050565b606061177d6009848460405161176a9291906133fe565b90815260200160405180910390206128d5565b90505b92915050565b838333604182146117a95760405162461bcd60e51b8152600401610712906131fb565b828260008181106117bc576117bc613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146117f25760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611806848461222d565b6001600160a01b03161461182c5760405162461bcd60e51b81526004016107129061329b565b84846021811461184e5760405162461bcd60e51b8152600401610712906131fb565b8181600081811061186157611861613240565b9050013560f81c60f81b6001600160f81b031916600260f81b14806118af57508181600081811061189457611894613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6118cb5760405162461bcd60e51b815260040161071290613256565b600682826040516118dd9291906133fe565b9081526040519081900360200190205460ff1661190c5760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90611948908d908d90600401613312565b600060405180830381865af4158015611965573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261198d9190810190613326565b90506117478189896128e9565b80516020818301810180516006825292820191909301209152805460018201805460ff90921692916119cb906134d6565b80601f01602080910402602001604051908101604052809291908181526020018280546119f7906134d6565b8015611a445780601f10611a1957610100808354040283529160200191611a44565b820191906000526020600020905b815481529060010190602001808311611a2757829003601f168201915b50505050600283015460039093015491929163ffffffff80821692506401000000008204811691600160401b90041686565b838360218114611a985760405162461bcd60e51b8152600401610712906131fb565b81816000818110611aab57611aab613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611af9575081816000818110611ade57611ade613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611b155760405162461bcd60e51b815260040161071290613256565b838360218114611b375760405162461bcd60e51b8152600401610712906131fb565b81816000818110611b4a57611b4a613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611b98575081816000818110611b7d57611b7d613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611bb45760405162461bcd60e51b815260040161071290613256565b60068282604051611bc69291906133fe565b9081526040519081900360200190205460ff16611bf55760405162461bcd60e51b81526004016107129061340e565b61084a88888080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508a92508991506128e99050565b611c4061288f565b60008111611ca85760405162461bcd60e51b815260206004820152602f60248201527f4950546f6b656e5374616b696e673a206d696e526564656c6567617465416d6f60448201526e0756e742063616e6e6f74206265203608c1b6064820152608401610712565b611cd27f000000000000000000000000000000000000000000000000000000000000000082613456565b611cdc908261348e565b60045550565b848460218114611d045760405162461bcd60e51b8152600401610712906131fb565b81816000818110611d1757611d17613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611d65575081816000818110611d4a57611d4a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611d815760405162461bcd60e51b815260040161071290613256565b848460218114611da35760405162461bcd60e51b8152600401610712906131fb565b81816000818110611db657611db6613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611e04575081816000818110611de957611de9613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611e205760405162461bcd60e51b815260040161071290613256565b60068282604051611e329291906133fe565b9081526040519081900360200190205460ff16611e615760405162461bcd60e51b81526004016107129061340e565b611ea289898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152503392506124e6915050565b610e2189898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a915089905061272f565b600080611f137f000000000000000000000000000000000000000000000000000000000000000084613456565b9050611f1f818461348e565b9150915091565b611f2e61288f565b60008111611f915760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a206d696e5374616b65416d6f756e7420636044820152690616e6e6f7420626520360b41b6064820152608401610712565b611fbb7f000000000000000000000000000000000000000000000000000000000000000082613456565b611fc5908261348e565b60025550565b611fd361288f565b600081116120495760405162461bcd60e51b815260206004820152603e60248201527f4950546f6b656e5374616b696e673a206e65775769746864726177616c41646460448201527f726573734368616e6765496e74657276616c2063616e6e6f74206265203000006064820152608401610712565b600555565b61205661288f565b600180546001600160a01b0383166001600160a01b031990911681179091556120876000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b868633604182146120e25760405162461bcd60e51b8152600401610712906131fb565b828260008181106120f5576120f5613240565b9050013560f81c60f81b6001600160f81b031916600460f81b1461212b5760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661213f848461222d565b6001600160a01b0316146121655760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906121a1908e908e90600401613312565b600060405180830381865af41580156121be573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526121e69190810190613326565b9050611041818a8a8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b91508a9050612284565b600061223c8260018186613510565b60405161224a9291906133fe565b6040519081900390209392505050565b600061177d836001600160a01b038416612a00565b600061177d836001600160a01b038416612a4f565b600685604051612294919061339c565b9081526040519081900360200190205460ff16156123055760405162461bcd60e51b815260206004820152602860248201527f4950546f6b656e5374616b696e673a2056616c696461746f7220616c72656164604482015267792065786973747360c01b6064820152608401610712565b60008061231134611ee6565b91509150600082116123355760405162461bcd60e51b81526004016107129061353a565b6040518060c001604052806001151581526020018781526020018381526020018663ffffffff1681526020018563ffffffff1681526020018463ffffffff16815250600688604051612387919061339c565b908152604051602091819003820190208251815460ff19169015151781559082015160018201906123b890826135cf565b506040828101516002830155606083015160039092018054608085015160a09095015163ffffffff908116600160401b026bffffffff0000000000000000199682166401000000000267ffffffffffffffff199093169190951617179390931691909117909155518290600790612430908a9061339c565b9081526020016040518091039020600082825461244d91906134a1565b9250508190555081600888604051612465919061339c565b908152602001604051809103902088604051612481919061339c565b9081526020016040518091039020600082825461249e91906134a1565b909155506124ad905081612b42565b7f5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a188787848888886040516113ae9695949392919061368e565b61250f816009846040516124fa919061339c565b90815260405190819003602001902090612bed565b61256d5760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2043616c6c6572206973206e6f7420616e6044820152681037b832b930ba37b960b91b6064820152608401610712565b5050565b80600887604051612582919061339c565b908152602001604051809103902086866040516125a09291906133fe565b90815260200160405180910390205410156125cd5760405162461bcd60e51b8152600401610712906136e1565b80600686866040516125e09291906133fe565b90815260200160405180910390206002016000828254612600919061348e565b92505081905550806006848460405161261a9291906133fe565b9081526020016040518091039020600201600082825461263a91906134a1565b9250508190555080600887604051612652919061339c565b908152602001604051809103902086866040516126709291906133fe565b9081526020016040518091039020600082825461268d919061348e565b92505081905550806008876040516126a5919061339c565b908152602001604051809103902084846040516126c39291906133fe565b908152602001604051809103902060008282546126e091906134a1565b90915550506040517fb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a9061271f9088908890889088908890889061372b565b60405180910390a1505050505050565b80600885604051612740919061339c565b9081526020016040518091039020848460405161275e9291906133fe565b908152602001604051809103902054101561278b5760405162461bcd60e51b8152600401610712906136e1565b806006848460405161279e9291906133fe565b908152602001604051809103902060020160008282546127be919061348e565b92505081905550806007856040516127d6919061339c565b908152602001604051809103902060008282546127f3919061348e565b925050819055508060088560405161280b919061339c565b908152602001604051809103902084846040516128299291906133fe565b90815260200160405180910390206000828254612846919061348e565b90915550506040517f0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b599061288190869086908690869061377a565b60405180910390a150505050565b6000546001600160a01b031633146111075760405163118cdaa760e01b8152336004820152602401610712565b600180546001600160a01b031916905561140081612c0f565b606060006128e283612c5f565b9392505050565b6000806128f534611ee6565b9150915060025482101561291b5760405162461bcd60e51b81526004016107129061353a565b816006858560405161292e9291906133fe565b9081526040519081900360200181206002018054909201909155829060079061295890889061339c565b9081526040519081900360200181208054909201909155829060089061297f90889061339c565b9081526020016040518091039020858560405161299d9291906133fe565b908152604051908190036020019020805490910190556129bc81612b42565b7fe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0858585856040516129f1949392919061377a565b60405180910390a15050505050565b6000818152600183016020526040812054612a4757508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611780565b506000611780565b60008181526001830160205260408120548015612b38576000612a7360018361348e565b8554909150600090612a879060019061348e565b9050808214612aec576000866000018281548110612aa757612aa7613240565b9060005260206000200154905080876000018481548110612aca57612aca613240565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080612afd57612afd6137b2565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611780565b6000915050611780565b604051600090339083908381818185875af1925050503d8060008114612b84576040519150601f19603f3d011682016040523d82523d6000602084013e612b89565b606091505b505090508061256d5760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a204661696c656420746f20726566756e64604482015269103932b6b0b4b73232b960b11b6064820152608401610712565b6001600160a01b0381166000908152600183016020526040812054151561177d565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b606081600001805480602002602001604051908101604052809291908181526020018280548015612caf57602002820191906000526020600020905b815481526020019060010190808311612c9b575b50505050509050919050565b60008083601f840112612ccd57600080fd5b5081356001600160401b03811115612ce457600080fd5b602083019150836020828501011115612cfc57600080fd5b9250929050565b80356001600160a01b0381168114612d1a57600080fd5b919050565b600080600060408486031215612d3457600080fd5b83356001600160401b03811115612d4a57600080fd5b612d5686828701612cbb565b9094509250612d69905060208501612d03565b90509250925092565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715612db057612db0612d72565b604052919050565b60006001600160401b03821115612dd157612dd1612d72565b50601f01601f191660200190565b600082601f830112612df057600080fd5b8135612e03612dfe82612db8565b612d88565b818152846020838601011115612e1857600080fd5b816020850160208301376000918101602001919091529392505050565b60008060408385031215612e4857600080fd5b82356001600160401b0380821115612e5f57600080fd5b612e6b86838701612ddf565b93506020850135915080821115612e8157600080fd5b50612e8e85828601612ddf565b9150509250929050565b60008060208385031215612eab57600080fd5b82356001600160401b03811115612ec157600080fd5b612ecd85828601612cbb565b90969095509350505050565b600060208284031215612eeb57600080fd5b81356001600160401b03811115612f0157600080fd5b8201608081850312156128e257600080fd5b600060208284031215612f2557600080fd5b81356001600160401b03811115612f3b57600080fd5b612f4784828501612ddf565b949350505050565b600080600080600060608688031215612f6757600080fd5b85356001600160401b0380821115612f7e57600080fd5b612f8a89838a01612cbb565b90975095506020880135915080821115612fa357600080fd5b50612fb088828901612cbb565b96999598509660400135949350505050565b600060208284031215612fd457600080fd5b5035919050565b6020808252825182820181905260009190848201906040850190845b8181101561301c5783516001600160a01b031683529284019291840191600101612ff7565b50909695505050505050565b6000806000806040858703121561303e57600080fd5b84356001600160401b038082111561305557600080fd5b61306188838901612cbb565b9096509450602087013591508082111561307a57600080fd5b5061308787828801612cbb565b95989497509550505050565b60005b838110156130ae578181015183820152602001613096565b50506000910152565b600081518084526130cf816020860160208601613093565b601f01601f19169290920160200192915050565b861515815260c0602082015260006130fe60c08301886130b7565b60408301969096525063ffffffff9384166060820152918316608083015290911660a09091015292915050565b60006020828403121561313d57600080fd5b61177d82612d03565b803563ffffffff81168114612d1a57600080fd5b600080600080600080600060a0888a03121561317557600080fd5b87356001600160401b038082111561318c57600080fd5b6131988b838c01612cbb565b909950975060208a01359150808211156131b157600080fd5b506131be8a828b01612cbb565b90965094506131d1905060408901613146565b92506131df60608901613146565b91506131ed60808901613146565b905092959891949750929550565b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206c6040820152640cadccee8d60db1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b657920706040820152640e4caccd2f60db1b606082015260800190565b6020808252602e908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206460408201526d657269766564206164647265737360901b606082015260800190565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b602081526000612f476020830184866132e9565b60006020828403121561333857600080fd5b81516001600160401b0381111561334e57600080fd5b8201601f8101841361335f57600080fd5b805161336d612dfe82612db8565b81815285602083850101111561338257600080fd5b613393826020830160208601613093565b95945050505050565b600082516133ae818460208701613093565b9190910192915050565b6000808335601e198436030181126133cf57600080fd5b8301803591506001600160401b038211156133e957600080fd5b602001915036819003821315612cfc57600080fd5b8183823760009101908152919050565b60208082526028908201527f4950546f6b656e5374616b696e673a2056616c696461746f7220646f6573206e6040820152671bdd08195e1a5cdd60c21b606082015260800190565b60008261347357634e487b7160e01b600052601260045260246000fd5b500690565b634e487b7160e01b600052601160045260246000fd5b8181038181111561178057611780613478565b8082018082111561178057611780613478565b6040815260006134c760408301856130b7565b90508260208301529392505050565b600181811c908216806134ea57607f821691505b60208210810361350a57634e487b7160e01b600052602260045260246000fd5b50919050565b6000808585111561352057600080fd5b8386111561352d57600080fd5b5050820193919092039150565b60208082526024908201527f4950546f6b656e5374616b696e673a205374616b6520616d6f756e7420746f6f604082015263206c6f7760e01b606082015260800190565b601f8211156135ca576000816000526020600020601f850160051c810160208610156135a75750805b601f850160051c820191505b818110156135c6578281556001016135b3565b5050505b505050565b81516001600160401b038111156135e8576135e8612d72565b6135fc816135f684546134d6565b8461357e565b602080601f83116001811461363157600084156136195750858301515b600019600386901b1c1916600185901b1785556135c6565b600085815260208120601f198616915b8281101561366057888601518255948401946001909101908401613641565b508582101561367e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60c0815260006136a160c08301896130b7565b82810360208401526136b381896130b7565b6040840197909752505063ffffffff9384166060820152918316608083015290911660a09091015292915050565b6020808252602a908201527f4950546f6b656e5374616b696e673a20496e73756666696369656e74207374616040820152691ad95908185b5bdd5b9d60b21b606082015260800190565b60808152600061373e60808301896130b7565b828103602084015261375181888a6132e9565b905082810360408401526137668186886132e9565b915050826060830152979650505050505050565b60608152600061378d60608301876130b7565b82810360208401526137a08186886132e9565b91505082604083015295945050505050565b634e487b7160e01b600052603160045260246000fdfea2646970667358221220cbf37ead0066eb6657879ff233410633a0ad7abf7542d7cf2a2155f0dd30195b64736f6c63430008180033", + "balance": "0x0" + }, + "333333fffffffffffffffffffffffffffffffffe": { + "code": "0x6080604052600436106101f95760003560e01c806386eec4a11161010d578063c24ae586116100a0578063eee5cead1161006f578063eee5cead14610652578063f188768414610672578063f2fde38b14610688578063fc2e5932146106a8578063fc56c2a2146106bb57600080fd5b8063c24ae586146105a7578063d2e1f5b8146105df578063e30c397814610614578063eb4af0451461063257600080fd5b80639855c8b5116100dc5780639855c8b5146104ff578063a1cb18461461051f578063b8db983e1461053f578063bda16b151461057357600080fd5b806386eec4a1146104755780638d3e1e41146104885780638da5cb5b146104ba5780638f37ec19146104ec57600080fd5b80635706750311610190578063715018a61161015f578063715018a6146103de578063787f82c8146103f357806379ba5097146104135780637b6e842c1461042857806383dffd6f1461044857600080fd5b806357067503146103505780635a69825d146103885780635d5ab9681461039e5780636ea3a228146103be57600080fd5b80632ebc6034116101cc5780632ebc6034146102be57806339ec4df91461030757806348903e381461031d57806353972c2a1461033057600080fd5b8063057b9296146101fe578063060ceab01461022057806317e42e12146102495780632d1e973e14610269575b600080fd5b34801561020a57600080fd5b5061021e610219366004612d1f565b6106ef565b005b34801561022c57600080fd5b5061023660055481565b6040519081526020015b60405180910390f35b34801561025557600080fd5b5061021e610264366004612d1f565b610854565b34801561027557600080fd5b50610236610284366004612e35565b8151602081840181018051600882529282019482019490942091909352815180830184018051928152908401929093019190912091525481565b3480156102ca57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b60405163ffffffff9091168152602001610240565b34801561031357600080fd5b5061023660035481565b61021e61032b366004612e98565b6109a6565b34801561033c57600080fd5b5061021e61034b366004612ed9565b610b0e565b34801561035c57600080fd5b5061023661036b366004612f13565b805160208183018101805160078252928201919093012091525481565b34801561039457600080fd5b5061023660045481565b3480156103aa57600080fd5b5061021e6103b9366004612f4f565b610e2c565b3480156103ca57600080fd5b5061021e6103d9366004612fc2565b61104e565b3480156103ea57600080fd5b5061021e6110f5565b3480156103ff57600080fd5b5061021e61040e366004612d1f565b611109565b34801561041f57600080fd5b5061021e6113bf565b34801561043457600080fd5b5061021e610443366004612ed9565b611403565b34801561045457600080fd5b50610468610463366004612e98565b611753565b6040516102409190612fdb565b61021e610483366004613028565b611786565b34801561049457600080fd5b506104a86104a3366004612f13565b61199a565b604051610240969594939291906130e3565b3480156104c657600080fd5b506000546001600160a01b03165b6040516001600160a01b039091168152602001610240565b61021e6104fa366004613028565b611a76565b34801561050b57600080fd5b5061021e61051a366004612fc2565b611c38565b34801561052b57600080fd5b5061021e61053a366004612f4f565b611ce2565b34801561054b57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b34801561057f57600080fd5b506102367f000000000000000000000000000000000000000000000000000000000000000081565b3480156105b357600080fd5b506102366105c2366004612f13565b8051602081830181018051600a8252928201919093012091525481565b3480156105eb57600080fd5b506105ff6105fa366004612fc2565b611ee6565b60408051928352602083019190915201610240565b34801561062057600080fd5b506001546001600160a01b03166104d4565b34801561063e57600080fd5b5061021e61064d366004612fc2565b611f26565b34801561065e57600080fd5b5061021e61066d366004612fc2565b611fcb565b34801561067e57600080fd5b5061023660025481565b34801561069457600080fd5b5061021e6106a336600461312b565b61204e565b61021e6106b636600461315a565b6120bf565b3480156106c757600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b8282336041821461071b5760405162461bcd60e51b8152600401610712906131fb565b60405180910390fd5b8282600081811061072e5761072e613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146107645760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610778848461222d565b6001600160a01b03161461079e5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906107da908a908a90600401613312565b600060405180830381865af41580156107f7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261081f9190810190613326565b905061084a85600983604051610835919061339c565b9081526040519081900360200190209061225a565b5050505050505050565b828233604182146108775760405162461bcd60e51b8152600401610712906131fb565b8282600081811061088a5761088a613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146108c05760405162461bcd60e51b815260040161071290613256565b806001600160a01b03166108d4848461222d565b6001600160a01b0316146108fa5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90610936908a908a90600401613312565b600060405180830381865af4158015610953573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261097b9190810190613326565b905061084a85600983604051610991919061339c565b9081526040519081900360200190209061226f565b8181602181146109c85760405162461bcd60e51b8152600401610712906131fb565b818160008181106109db576109db613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610a29575081816000818110610a0e57610a0e613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610a455760405162461bcd60e51b815260040161071290613256565b610b0884848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250506040805180820190915260098152683b30b634b230ba37b960b91b602082015291507f000000000000000000000000000000000000000000000000000000000000000090507f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000612284565b50505050565b610b1881806133b8565b60418114610b385760405162461bcd60e51b8152600401610712906131fb565b81816000818110610b4b57610b4b613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610b815760405162461bcd60e51b815260040161071290613256565b610b8e60208401846133b8565b60218114610bae5760405162461bcd60e51b8152600401610712906131fb565b81816000818110610bc157610bc1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610c0f575081816000818110610bf457610bf4613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610c2b5760405162461bcd60e51b815260040161071290613256565b60068282604051610c3d9291906133fe565b9081526040519081900360200190205460ff16610c6c5760405162461bcd60e51b81526004016107129061340e565b610c7960408601866133b8565b60218114610c995760405162461bcd60e51b8152600401610712906131fb565b81816000818110610cac57610cac613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610cfa575081816000818110610cdf57610cdf613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610d165760405162461bcd60e51b815260040161071290613256565b60068282604051610d289291906133fe565b9081526040519081900360200190205460ff16610d575760405162461bcd60e51b81526004016107129061340e565b60007300000000000000000000000000000000000256f1636889b16e610d7d8a806133b8565b6040518363ffffffff1660e01b8152600401610d9a929190613312565b600060405180830381865af4158015610db7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610ddf9190810190613326565b90506000610df08960600135611ee6565b509050610dfd82336124e6565b610e2182610e0e60208c018c6133b8565b610e1b60408e018e6133b8565b86612571565b505050505050505050565b84843360418214610e4f5760405162461bcd60e51b8152600401610712906131fb565b82826000818110610e6257610e62613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610e985760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610eac848461222d565b6001600160a01b031614610ed25760405162461bcd60e51b81526004016107129061329b565b858560218114610ef45760405162461bcd60e51b8152600401610712906131fb565b81816000818110610f0757610f07613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610f55575081816000818110610f3a57610f3a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610f715760405162461bcd60e51b815260040161071290613256565b60068282604051610f839291906133fe565b9081526040519081900360200190205460ff16610fb25760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90610fee908e908e90600401613312565b600060405180830381865af415801561100b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110339190810190613326565b9050611041818a8a8a61272f565b5050505050505050505050565b61105661288f565b600081116110bb5760405162461bcd60e51b815260206004820152602c60248201527f4950546f6b656e5374616b696e673a206d696e556e7374616b65416d6f756e7460448201526b02063616e6e6f7420626520360a41b6064820152608401610712565b6110e57f000000000000000000000000000000000000000000000000000000000000000082613456565b6110ef908261348e565b60035550565b6110fd61288f565b61110760006128bc565b565b8282336041821461112c5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061113f5761113f613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146111755760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611189848461222d565b6001600160a01b0316146111af5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906111eb908a908a90600401613312565b600060405180830381865af4158015611208573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526112309190810190613326565b90506000600782604051611244919061339c565b908152602001604051809103902054116112b25760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2044656c656761746f72206d7573742068604482015268617665207374616b6560b81b6064820152608401610712565b42600554600a836040516112c6919061339c565b9081526020016040518091039020546112df91906134a1565b106113485760405162461bcd60e51b815260206004820152603360248201527f4950546f6b656e5374616b696e673a205769746864726177616c20616464726560448201527239b99031b430b733b29031b7b7b616b237bbb760691b6064820152608401610712565b42600a82604051611359919061339c565b9081526020016040518091039020819055507f9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca818660601b6bffffffffffffffffffffffff19166040516113ae9291906134b4565b60405180910390a150505050505050565b60015433906001600160a01b031681146113f75760405163118cdaa760e01b81526001600160a01b0382166004820152602401610712565b611400816128bc565b50565b61140d81806133b8565b336041821461142e5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061144157611441613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146114775760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661148b848461222d565b6001600160a01b0316146114b15760405162461bcd60e51b81526004016107129061329b565b6114be60208501856133b8565b602181146114de5760405162461bcd60e51b8152600401610712906131fb565b818160008181106114f1576114f1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061153f57508181600081811061152457611524613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b61155b5760405162461bcd60e51b815260040161071290613256565b6006828260405161156d9291906133fe565b9081526040519081900360200190205460ff1661159c5760405162461bcd60e51b81526004016107129061340e565b6115a960408701876133b8565b602181146115c95760405162461bcd60e51b8152600401610712906131fb565b818160008181106115dc576115dc613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061162a57508181600081811061160f5761160f613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6116465760405162461bcd60e51b815260040161071290613256565b600682826040516116589291906133fe565b9081526040519081900360200190205460ff166116875760405162461bcd60e51b81526004016107129061340e565b60006116968960600135611ee6565b50905060007300000000000000000000000000000000000256f1636889b16e6116bf8c806133b8565b6040518363ffffffff1660e01b81526004016116dc929190613312565b600060405180830381865af41580156116f9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526117219190810190613326565b90506117478161173460208d018d6133b8565b61174160408f018f6133b8565b87612571565b50505050505050505050565b606061177d6009848460405161176a9291906133fe565b90815260200160405180910390206128d5565b90505b92915050565b838333604182146117a95760405162461bcd60e51b8152600401610712906131fb565b828260008181106117bc576117bc613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146117f25760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611806848461222d565b6001600160a01b03161461182c5760405162461bcd60e51b81526004016107129061329b565b84846021811461184e5760405162461bcd60e51b8152600401610712906131fb565b8181600081811061186157611861613240565b9050013560f81c60f81b6001600160f81b031916600260f81b14806118af57508181600081811061189457611894613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6118cb5760405162461bcd60e51b815260040161071290613256565b600682826040516118dd9291906133fe565b9081526040519081900360200190205460ff1661190c5760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90611948908d908d90600401613312565b600060405180830381865af4158015611965573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261198d9190810190613326565b90506117478189896128e9565b80516020818301810180516006825292820191909301209152805460018201805460ff90921692916119cb906134d6565b80601f01602080910402602001604051908101604052809291908181526020018280546119f7906134d6565b8015611a445780601f10611a1957610100808354040283529160200191611a44565b820191906000526020600020905b815481529060010190602001808311611a2757829003601f168201915b50505050600283015460039093015491929163ffffffff80821692506401000000008204811691600160401b90041686565b838360218114611a985760405162461bcd60e51b8152600401610712906131fb565b81816000818110611aab57611aab613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611af9575081816000818110611ade57611ade613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611b155760405162461bcd60e51b815260040161071290613256565b838360218114611b375760405162461bcd60e51b8152600401610712906131fb565b81816000818110611b4a57611b4a613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611b98575081816000818110611b7d57611b7d613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611bb45760405162461bcd60e51b815260040161071290613256565b60068282604051611bc69291906133fe565b9081526040519081900360200190205460ff16611bf55760405162461bcd60e51b81526004016107129061340e565b61084a88888080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508a92508991506128e99050565b611c4061288f565b60008111611ca85760405162461bcd60e51b815260206004820152602f60248201527f4950546f6b656e5374616b696e673a206d696e526564656c6567617465416d6f60448201526e0756e742063616e6e6f74206265203608c1b6064820152608401610712565b611cd27f000000000000000000000000000000000000000000000000000000000000000082613456565b611cdc908261348e565b60045550565b848460218114611d045760405162461bcd60e51b8152600401610712906131fb565b81816000818110611d1757611d17613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611d65575081816000818110611d4a57611d4a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611d815760405162461bcd60e51b815260040161071290613256565b848460218114611da35760405162461bcd60e51b8152600401610712906131fb565b81816000818110611db657611db6613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611e04575081816000818110611de957611de9613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611e205760405162461bcd60e51b815260040161071290613256565b60068282604051611e329291906133fe565b9081526040519081900360200190205460ff16611e615760405162461bcd60e51b81526004016107129061340e565b611ea289898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152503392506124e6915050565b610e2189898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a915089905061272f565b600080611f137f000000000000000000000000000000000000000000000000000000000000000084613456565b9050611f1f818461348e565b9150915091565b611f2e61288f565b60008111611f915760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a206d696e5374616b65416d6f756e7420636044820152690616e6e6f7420626520360b41b6064820152608401610712565b611fbb7f000000000000000000000000000000000000000000000000000000000000000082613456565b611fc5908261348e565b60025550565b611fd361288f565b600081116120495760405162461bcd60e51b815260206004820152603e60248201527f4950546f6b656e5374616b696e673a206e65775769746864726177616c41646460448201527f726573734368616e6765496e74657276616c2063616e6e6f74206265203000006064820152608401610712565b600555565b61205661288f565b600180546001600160a01b0383166001600160a01b031990911681179091556120876000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b868633604182146120e25760405162461bcd60e51b8152600401610712906131fb565b828260008181106120f5576120f5613240565b9050013560f81c60f81b6001600160f81b031916600460f81b1461212b5760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661213f848461222d565b6001600160a01b0316146121655760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906121a1908e908e90600401613312565b600060405180830381865af41580156121be573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526121e69190810190613326565b9050611041818a8a8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b91508a9050612284565b600061223c8260018186613510565b60405161224a9291906133fe565b6040519081900390209392505050565b600061177d836001600160a01b038416612a00565b600061177d836001600160a01b038416612a4f565b600685604051612294919061339c565b9081526040519081900360200190205460ff16156123055760405162461bcd60e51b815260206004820152602860248201527f4950546f6b656e5374616b696e673a2056616c696461746f7220616c72656164604482015267792065786973747360c01b6064820152608401610712565b60008061231134611ee6565b91509150600082116123355760405162461bcd60e51b81526004016107129061353a565b6040518060c001604052806001151581526020018781526020018381526020018663ffffffff1681526020018563ffffffff1681526020018463ffffffff16815250600688604051612387919061339c565b908152604051602091819003820190208251815460ff19169015151781559082015160018201906123b890826135cf565b506040828101516002830155606083015160039092018054608085015160a09095015163ffffffff908116600160401b026bffffffff0000000000000000199682166401000000000267ffffffffffffffff199093169190951617179390931691909117909155518290600790612430908a9061339c565b9081526020016040518091039020600082825461244d91906134a1565b9250508190555081600888604051612465919061339c565b908152602001604051809103902088604051612481919061339c565b9081526020016040518091039020600082825461249e91906134a1565b909155506124ad905081612b42565b7f5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a188787848888886040516113ae9695949392919061368e565b61250f816009846040516124fa919061339c565b90815260405190819003602001902090612bed565b61256d5760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2043616c6c6572206973206e6f7420616e6044820152681037b832b930ba37b960b91b6064820152608401610712565b5050565b80600887604051612582919061339c565b908152602001604051809103902086866040516125a09291906133fe565b90815260200160405180910390205410156125cd5760405162461bcd60e51b8152600401610712906136e1565b80600686866040516125e09291906133fe565b90815260200160405180910390206002016000828254612600919061348e565b92505081905550806006848460405161261a9291906133fe565b9081526020016040518091039020600201600082825461263a91906134a1565b9250508190555080600887604051612652919061339c565b908152602001604051809103902086866040516126709291906133fe565b9081526020016040518091039020600082825461268d919061348e565b92505081905550806008876040516126a5919061339c565b908152602001604051809103902084846040516126c39291906133fe565b908152602001604051809103902060008282546126e091906134a1565b90915550506040517fb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a9061271f9088908890889088908890889061372b565b60405180910390a1505050505050565b80600885604051612740919061339c565b9081526020016040518091039020848460405161275e9291906133fe565b908152602001604051809103902054101561278b5760405162461bcd60e51b8152600401610712906136e1565b806006848460405161279e9291906133fe565b908152602001604051809103902060020160008282546127be919061348e565b92505081905550806007856040516127d6919061339c565b908152602001604051809103902060008282546127f3919061348e565b925050819055508060088560405161280b919061339c565b908152602001604051809103902084846040516128299291906133fe565b90815260200160405180910390206000828254612846919061348e565b90915550506040517f0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b599061288190869086908690869061377a565b60405180910390a150505050565b6000546001600160a01b031633146111075760405163118cdaa760e01b8152336004820152602401610712565b600180546001600160a01b031916905561140081612c0f565b606060006128e283612c5f565b9392505050565b6000806128f534611ee6565b9150915060025482101561291b5760405162461bcd60e51b81526004016107129061353a565b816006858560405161292e9291906133fe565b9081526040519081900360200181206002018054909201909155829060079061295890889061339c565b9081526040519081900360200181208054909201909155829060089061297f90889061339c565b9081526020016040518091039020858560405161299d9291906133fe565b908152604051908190036020019020805490910190556129bc81612b42565b7fe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0858585856040516129f1949392919061377a565b60405180910390a15050505050565b6000818152600183016020526040812054612a4757508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611780565b506000611780565b60008181526001830160205260408120548015612b38576000612a7360018361348e565b8554909150600090612a879060019061348e565b9050808214612aec576000866000018281548110612aa757612aa7613240565b9060005260206000200154905080876000018481548110612aca57612aca613240565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080612afd57612afd6137b2565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611780565b6000915050611780565b604051600090339083908381818185875af1925050503d8060008114612b84576040519150601f19603f3d011682016040523d82523d6000602084013e612b89565b606091505b505090508061256d5760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a204661696c656420746f20726566756e64604482015269103932b6b0b4b73232b960b11b6064820152608401610712565b6001600160a01b0381166000908152600183016020526040812054151561177d565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b606081600001805480602002602001604051908101604052809291908181526020018280548015612caf57602002820191906000526020600020905b815481526020019060010190808311612c9b575b50505050509050919050565b60008083601f840112612ccd57600080fd5b5081356001600160401b03811115612ce457600080fd5b602083019150836020828501011115612cfc57600080fd5b9250929050565b80356001600160a01b0381168114612d1a57600080fd5b919050565b600080600060408486031215612d3457600080fd5b83356001600160401b03811115612d4a57600080fd5b612d5686828701612cbb565b9094509250612d69905060208501612d03565b90509250925092565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715612db057612db0612d72565b604052919050565b60006001600160401b03821115612dd157612dd1612d72565b50601f01601f191660200190565b600082601f830112612df057600080fd5b8135612e03612dfe82612db8565b612d88565b818152846020838601011115612e1857600080fd5b816020850160208301376000918101602001919091529392505050565b60008060408385031215612e4857600080fd5b82356001600160401b0380821115612e5f57600080fd5b612e6b86838701612ddf565b93506020850135915080821115612e8157600080fd5b50612e8e85828601612ddf565b9150509250929050565b60008060208385031215612eab57600080fd5b82356001600160401b03811115612ec157600080fd5b612ecd85828601612cbb565b90969095509350505050565b600060208284031215612eeb57600080fd5b81356001600160401b03811115612f0157600080fd5b8201608081850312156128e257600080fd5b600060208284031215612f2557600080fd5b81356001600160401b03811115612f3b57600080fd5b612f4784828501612ddf565b949350505050565b600080600080600060608688031215612f6757600080fd5b85356001600160401b0380821115612f7e57600080fd5b612f8a89838a01612cbb565b90975095506020880135915080821115612fa357600080fd5b50612fb088828901612cbb565b96999598509660400135949350505050565b600060208284031215612fd457600080fd5b5035919050565b6020808252825182820181905260009190848201906040850190845b8181101561301c5783516001600160a01b031683529284019291840191600101612ff7565b50909695505050505050565b6000806000806040858703121561303e57600080fd5b84356001600160401b038082111561305557600080fd5b61306188838901612cbb565b9096509450602087013591508082111561307a57600080fd5b5061308787828801612cbb565b95989497509550505050565b60005b838110156130ae578181015183820152602001613096565b50506000910152565b600081518084526130cf816020860160208601613093565b601f01601f19169290920160200192915050565b861515815260c0602082015260006130fe60c08301886130b7565b60408301969096525063ffffffff9384166060820152918316608083015290911660a09091015292915050565b60006020828403121561313d57600080fd5b61177d82612d03565b803563ffffffff81168114612d1a57600080fd5b600080600080600080600060a0888a03121561317557600080fd5b87356001600160401b038082111561318c57600080fd5b6131988b838c01612cbb565b909950975060208a01359150808211156131b157600080fd5b506131be8a828b01612cbb565b90965094506131d1905060408901613146565b92506131df60608901613146565b91506131ed60808901613146565b905092959891949750929550565b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206c6040820152640cadccee8d60db1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b657920706040820152640e4caccd2f60db1b606082015260800190565b6020808252602e908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206460408201526d657269766564206164647265737360901b606082015260800190565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b602081526000612f476020830184866132e9565b60006020828403121561333857600080fd5b81516001600160401b0381111561334e57600080fd5b8201601f8101841361335f57600080fd5b805161336d612dfe82612db8565b81815285602083850101111561338257600080fd5b613393826020830160208601613093565b95945050505050565b600082516133ae818460208701613093565b9190910192915050565b6000808335601e198436030181126133cf57600080fd5b8301803591506001600160401b038211156133e957600080fd5b602001915036819003821315612cfc57600080fd5b8183823760009101908152919050565b60208082526028908201527f4950546f6b656e5374616b696e673a2056616c696461746f7220646f6573206e6040820152671bdd08195e1a5cdd60c21b606082015260800190565b60008261347357634e487b7160e01b600052601260045260246000fd5b500690565b634e487b7160e01b600052601160045260246000fd5b8181038181111561178057611780613478565b8082018082111561178057611780613478565b6040815260006134c760408301856130b7565b90508260208301529392505050565b600181811c908216806134ea57607f821691505b60208210810361350a57634e487b7160e01b600052602260045260246000fd5b50919050565b6000808585111561352057600080fd5b8386111561352d57600080fd5b5050820193919092039150565b60208082526024908201527f4950546f6b656e5374616b696e673a205374616b6520616d6f756e7420746f6f604082015263206c6f7760e01b606082015260800190565b601f8211156135ca576000816000526020600020601f850160051c810160208610156135a75750805b601f850160051c820191505b818110156135c6578281556001016135b3565b5050505b505050565b81516001600160401b038111156135e8576135e8612d72565b6135fc816135f684546134d6565b8461357e565b602080601f83116001811461363157600084156136195750858301515b600019600386901b1c1916600185901b1785556135c6565b600085815260208120601f198616915b8281101561366057888601518255948401946001909101908401613641565b508582101561367e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60c0815260006136a160c08301896130b7565b82810360208401526136b381896130b7565b6040840197909752505063ffffffff9384166060820152918316608083015290911660a09091015292915050565b6020808252602a908201527f4950546f6b656e5374616b696e673a20496e73756666696369656e74207374616040820152691ad95908185b5bdd5b9d60b21b606082015260800190565b60808152600061373e60808301896130b7565b828103602084015261375181888a6132e9565b905082810360408401526137668186886132e9565b915050826060830152979650505050505050565b60608152600061378d60608301876130b7565b82810360208401526137a08186886132e9565b91505082604083015295945050505050565b634e487b7160e01b600052603160045260246000fdfea2646970667358221220cbf37ead0066eb6657879ff233410633a0ad7abf7542d7cf2a2155f0dd30195b64736f6c63430008180033", + "balance": "0x0" + }, + "3c44cdddb6a900fa2b585dd299e03d12fa4293bc": { + "balance": "0xd3c21bcecceda1000000" + }, + "70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "balance": "0xd3c21bcecceda1000000" + }, + "90f79bf6eb2c4f870365e785982e1f101e93b906": { + "balance": "0xd3c21bcecceda1000000" + }, + "976ea74026e726554db657fa54763abd0c3a0aa9": { + "balance": "0xd3c21bcecceda1000000" + }, + "9965507d1a55bcc2695c58ba16fb37d819b0a4dc": { + "balance": "0xd3c21bcecceda1000000" + }, + "a0ee7a142d267c1f36714e4a8f75612f20a79720": { + "balance": "0xd3c21bcecceda1000000" + }, + "f39fd6e51aad88f6f4ce6ab8827279cfffb92266": { + "balance": "0xd3c21bcecceda1000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x3b9aca00", + "excessBlobGas": null, + "blobGasUsed": null +} \ No newline at end of file diff --git a/client/genutil/genutil.go b/client/genutil/genutil.go new file mode 100644 index 00000000..7023b8f8 --- /dev/null +++ b/client/genutil/genutil.go @@ -0,0 +1,261 @@ +package genutil + +import ( + "encoding/json" + "os" + "time" + + "cosmossdk.io/math" + "cosmossdk.io/x/tx/signing" + + "github.com/cometbft/cometbft/crypto" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cosmosstd "github.com/cosmos/cosmos-sdk/std" + sdk "github.com/cosmos/cosmos-sdk/types" + authcodec "github.com/cosmos/cosmos-sdk/x/auth/codec" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + atypes "github.com/cosmos/cosmos-sdk/x/auth/types" + btypes "github.com/cosmos/cosmos-sdk/x/bank/types" + dtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + gtypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + sltypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + sttypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/gogoproto/proto" + "github.com/ethereum/go-ethereum/common" + + evmenginetypes "github.com/piplabs/story/client/x/evmengine/types" + evmstakingtypes "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/buildinfo" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/netconf" +) + +// slashingWindows overrides the default slashing signed_blocks_window from 100 to 1000 +// since Iliad block period (+-1s) is very fast, roughly 10x normal period of 10s. +const slashingBlocksWindow = 1000 + +func MakeGenesis( + network netconf.ID, + genesisTime time.Time, + executionBlockHash common.Hash, + valPubkeys ...crypto.PubKey, +) (*gtypes.AppGenesis, error) { + cdc := getCodec() + txConfig := authtx.NewTxConfig(cdc, nil) + + // Step 1: Create the default genesis app state for all modules. + appState1 := defaultAppState(network.Static().MaxValidators, executionBlockHash, cdc.MustMarshalJSON) + appState1Bz, err := json.MarshalIndent(appState1, "", " ") + if err != nil { + return nil, errors.Wrap(err, "marshal app state") + } + + // Step 2: Create the app genesis object and store it to disk. + appGen := >ypes.AppGenesis{ + AppName: "iliad", + AppVersion: buildinfo.Version(), + GenesisTime: genesisTime.UTC(), + ChainID: network.Static().IliadConsensusChainIDStr(), + InitialHeight: 1, + Consensus: defaultConsensusGenesis(), + AppState: appState1Bz, + } + + // Use this temp file as "disk cache", since the genutil functions require a file path + tempFile, err := os.CreateTemp("", "") + if err != nil { + return nil, errors.Wrap(err, "create temp file") + } + if err := genutil.ExportGenesisFile(appGen, tempFile.Name()); err != nil { + return nil, errors.Wrap(err, "export genesis file") + } + + // Step 3: Create the genesis validators; genesis account and a MsgCreateValidator. + valTxs := make([]sdk.Tx, 0, len(valPubkeys)) + for _, pubkey := range valPubkeys { + tx, err := addValidator(txConfig, pubkey, cdc, tempFile.Name()) + if err != nil { + return nil, errors.Wrap(err, "add validator") + } + valTxs = append(valTxs, tx) + } + + // Step 4: Collect the MsgCreateValidator txs and update the app state (again). + appState2, err := collectGenTxs(cdc, txConfig, tempFile.Name(), valTxs) + if err != nil { + return nil, errors.Wrap(err, "collect genesis transactions") + } + appGen.AppState, err = json.MarshalIndent(appState2, "", " ") + if err != nil { + return nil, errors.Wrap(err, "marshal app state") + } + + // Step 5: Validate + if err := appGen.ValidateAndComplete(); err != nil { + return nil, errors.Wrap(err, "validate and complete genesis") + } + + return appGen, validateGenesis(cdc, appState2) +} + +func defaultConsensusGenesis() *gtypes.ConsensusGenesis { + pb := DefaultConsensusParams().ToProto() + resp := gtypes.NewConsensusGenesis(pb, nil) + // NewConsensusGenesis has a bug, it doesn't set VoteExtensionsEnableHeight + resp.Params.ABCI.VoteExtensionsEnableHeight = pb.Abci.VoteExtensionsEnableHeight + + return resp +} + +func validateGenesis(cdc codec.Codec, appState map[string]json.RawMessage) error { + // Staking module + ststate := sttypes.GetGenesisStateFromAppState(cdc, appState) + if err := staking.ValidateGenesis(ststate); err != nil { + return errors.Wrap(err, "validate staking genesis") + } + + // Slashing module + var slstate sltypes.GenesisState + if err := cdc.UnmarshalJSON(appState[sltypes.ModuleName], &slstate); err != nil { + return errors.Wrap(err, "unmarshal slashing genesis") + } + if err := sltypes.ValidateGenesis(slstate); err != nil { + return errors.Wrap(err, "validate slashing genesis") + } + + // Bank module + bstate := btypes.GetGenesisStateFromAppState(cdc, appState) + if err := bstate.Validate(); err != nil { + return errors.Wrap(err, "validate bank genesis") + } + + // Distribution module + dstate := new(dtypes.GenesisState) + if err := cdc.UnmarshalJSON(appState[dtypes.ModuleName], dstate); err != nil { + return errors.Wrap(err, "unmarshal distribution genesis") + } + if err := dtypes.ValidateGenesis(dstate); err != nil { + return errors.Wrap(err, "validate distribution genesis") + } + + // Auth module + astate := atypes.GetGenesisStateFromAppState(cdc, appState) + if err := atypes.ValidateGenesis(astate); err != nil { + return errors.Wrap(err, "validate auth genesis") + } + + return nil +} + +func collectGenTxs(cdc codec.Codec, txConfig client.TxConfig, genFile string, genTXs []sdk.Tx, +) (map[string]json.RawMessage, error) { + appState, _, err := gtypes.GenesisStateFromGenFile(genFile) + if err != nil { + return nil, errors.Wrap(err, "unmarshal genesis state") + } + + appState, err = genutil.SetGenTxsInAppGenesisState(cdc, txConfig.TxJSONEncoder(), appState, genTXs) + if err != nil { + return nil, errors.Wrap(err, "set genesis transactions") + } + + return appState, nil +} + +func addValidator(txConfig client.TxConfig, pubkey crypto.PubKey, cdc codec.Codec, genFile string) (sdk.Tx, error) { + // We use the validator pubkey as the account address + addr, err := k1util.PubKeyToAddress(pubkey) + if err != nil { + return nil, err + } + + // Add validator with 1 power (1e18 $STAKE ~= 1 ether $STAKE) + amount := sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction) + + err = genutil.AddGenesisAccount(cdc, addr.Bytes(), false, genFile, amount.String(), "", 0, 0, "") + if err != nil { + return nil, errors.Wrap(err, "add genesis account") + } + + pub, err := k1util.PubKeyToCosmos(pubkey) + if err != nil { + return nil, err + } + + var zero = math.LegacyZeroDec() + + msg, err := sttypes.NewMsgCreateValidator( + sdk.ValAddress(addr.Bytes()).String(), + pub, + amount, + sttypes.Description{Moniker: addr.Hex()}, + sttypes.NewCommissionRates(zero, zero, zero), + sdk.DefaultPowerReduction) + if err != nil { + return nil, errors.Wrap(err, "create validator message") + } + + builder := txConfig.NewTxBuilder() + + if err := builder.SetMsgs(msg); err != nil { + return nil, errors.Wrap(err, "set message") + } + + return builder.GetTx(), nil +} + +// defaultAppState returns the default genesis application state. +func defaultAppState( + maxVals uint32, + executionBlockHash common.Hash, + marshal func(proto.Message) []byte, +) map[string]json.RawMessage { + stakingGenesis := sttypes.DefaultGenesisState() + stakingGenesis.Params.MaxValidators = maxVals + + slashingGenesis := sltypes.DefaultGenesisState() + slashingGenesis.Params.SignedBlocksWindow = slashingBlocksWindow + + evmengGenesis := evmenginetypes.NewGenesisState(evmenginetypes.Params{ExecutionBlockHash: executionBlockHash.Bytes()}) + + return map[string]json.RawMessage{ + sttypes.ModuleName: marshal(stakingGenesis), + sltypes.ModuleName: marshal(slashingGenesis), + atypes.ModuleName: marshal(atypes.DefaultGenesisState()), + btypes.ModuleName: marshal(btypes.DefaultGenesisState()), + dtypes.ModuleName: marshal(dtypes.DefaultGenesisState()), + evmenginetypes.ModuleName: marshal(evmengGenesis), + evmstakingtypes.ModuleName: marshal(evmstakingtypes.DefaultGenesisState()), + } +} + +func getCodec() *codec.ProtoCodec { + // TODO: Use depinject to get all of this. + sdkConfig := sdk.GetConfig() + reg, err := codectypes.NewInterfaceRegistryWithOptions(codectypes.InterfaceRegistryOptions{ + ProtoFiles: proto.HybridResolver, + SigningOptions: signing.Options{ + AddressCodec: authcodec.NewBech32Codec(sdkConfig.GetBech32AccountAddrPrefix()), + ValidatorAddressCodec: authcodec.NewBech32Codec(sdkConfig.GetBech32ValidatorAddrPrefix()), + }, + }) + if err != nil { + panic(err) + } + + cosmosstd.RegisterInterfaces(reg) + atypes.RegisterInterfaces(reg) + sttypes.RegisterInterfaces(reg) + sltypes.RegisterInterfaces(reg) + btypes.RegisterInterfaces(reg) + dtypes.RegisterInterfaces(reg) + evmstakingtypes.RegisterInterfaces(reg) + evmenginetypes.RegisterInterfaces(reg) + + return codec.NewProtoCodec(reg) +} diff --git a/client/genutil/genutil_internal_test.go b/client/genutil/genutil_internal_test.go new file mode 100644 index 00000000..82cb3ebb --- /dev/null +++ b/client/genutil/genutil_internal_test.go @@ -0,0 +1,43 @@ +package genutil + +import ( + "testing" + + "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" + + etypes "github.com/piplabs/story/client/x/evmengine/types" +) + +func TestDefaultConsensusParams(t *testing.T) { + t.Parallel() + cons := defaultConsensusGenesis() + require.EqualValues(t, 1, cons.Params.ABCI.VoteExtensionsEnableHeight) + require.EqualValues(t, types.ABCIPubKeyTypeSecp256k1, cons.Params.Validator.PubKeyTypes[0]) +} + +func TestEncodeTXs(t *testing.T) { + t.Parallel() + msgs := []sdk.Msg{ + &etypes.MsgExecutionPayload{ + Authority: authtypes.NewModuleAddress("evm").String(), + }, + } + + cdc := getCodec() + txConfig := authtx.NewTxConfig(cdc, nil) + + b := txConfig.NewTxBuilder() + err := b.SetMsgs(msgs...) + require.NoError(t, err) + + tx := b.GetTx() + + require.Len(t, tx.GetMsgs(), 1) + msgsV2, err := tx.GetMsgsV2() + require.NoError(t, err) + require.Len(t, msgsV2, 1) +} diff --git a/client/genutil/testdata/TestMakeGenesis.golden b/client/genutil/testdata/TestMakeGenesis.golden new file mode 100644 index 00000000..6ab7c3f7 --- /dev/null +++ b/client/genutil/testdata/TestMakeGenesis.golden @@ -0,0 +1,250 @@ +{ + "app_name": "iliad", + "app_version": "v0.1.5", + "genesis_time": "1970-01-01T00:00:01Z", + "chain_id": "iliad-1001651", + "initial_height": 1, + "app_hash": null, + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1lmet2guatml9h9qsm2pvc525xk0mrkfytt32xa", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1anz94xtvltxrfr3ejllev7fegc8ntgkjl9h273", + "pub_key": null, + "account_number": "1", + "sequence": "0" + } + ] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "balances": [ + { + "address": "story1anz94xtvltxrfr3ejllev7fegc8ntgkjl9h273", + "coins": [ + { + "denom": "stake", + "amount": "1000000" + } + ] + }, + { + "address": "story1lmet2guatml9h9qsm2pvc525xk0mrkfytt32xa", + "coins": [ + { + "denom": "stake", + "amount": "1000000" + } + ] + } + ], + "supply": [ + { + "denom": "stake", + "amount": "2000000" + } + ], + "denom_metadata": [], + "send_enabled": [] + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.000000000000000000", + "bonus_proposer_reward": "0.000000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "evmengine": { + "params": { + "execution_block_hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABibG9ja2hhc2g=" + } + }, + "evmstaking": { + "params": { + "max_withdrawal_per_block": 4, + "max_sweep_per_block": 64, + "min_partial_withdrawal_amount": "600000" + } + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xFeF2b5239D5efe5b9410dA82cC5154359FB1d924", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1000000", + "delegator_address": "", + "validator_address": "storyvaloper1lmet2guatml9h9qsm2pvc525xk0mrkfy9y9tdk", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A1xEGMRhExV5BfGP3sWqAHJPLu+p2l8g20ZBg7KGnpO8" + }, + "value": { + "denom": "stake", + "amount": "1000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xECC45A996cfacC348E3997fF967939460F35a2D2", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1000000", + "delegator_address": "", + "validator_address": "storyvaloper1anz94xtvltxrfr3ejllev7fegc8ntgkj32rt46", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "Az1zc5dYc9IwXuBjyaIF09SGA5ITmu5mMoVPMfOw7Vsq" + }, + "value": { + "denom": "stake", + "amount": "1000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + } + ] + }, + "slashing": { + "params": { + "signed_blocks_window": "1000", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "1814400s", + "max_validators": 10, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "stake", + "min_commission_rate": "0.000000000000000000" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false + } + }, + "consensus": { + "params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "secp256k1" + ] + }, + "version": { + "app": "0" + }, + "abci": { + "vote_extensions_enable_height": "1" + } + } + } +} \ No newline at end of file diff --git a/client/main.go b/client/main.go new file mode 100644 index 00000000..fa102f7c --- /dev/null +++ b/client/main.go @@ -0,0 +1,11 @@ +// Command iliad is the main entry point for the iliad consensus client. +package main + +import ( + iliadcmd "github.com/piplabs/story/client/cmd" + libcmd "github.com/piplabs/story/lib/cmd" +) + +func main() { + libcmd.Main(iliadcmd.New()) +} diff --git a/client/server/auth.go b/client/server/auth.go new file mode 100644 index 00000000..3288502f --- /dev/null +++ b/client/server/auth.go @@ -0,0 +1,72 @@ +//nolint:wrapcheck // The api server is our server, so we don't need to wrap it +package server + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/gorilla/mux" + + "github.com/piplabs/story/client/server/utils" +) + +func (s *Server) initAuthRoute() { + s.httpMux.HandleFunc("/auth/accounts", utils.AutoWrap(s.aminoCodec, s.GetAccounts)) + s.httpMux.HandleFunc("/auth/accounts/{address}", utils.SimpleWrap(s.aminoCodec, s.GetAccountsByAddress)) +} + +// GetAccounts returns all the existing accounts. +func (s *Server) GetAccounts(req *getAccountsRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQueryServer(s.store.GetAccountKeeper()).Accounts(queryContext, &authtypes.QueryAccountsRequest{ + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + if err != nil { + return nil, err + } + + for _, account := range queryResp.Accounts { + err = s.prepareUnpackInterfaces(utils.WrapTypeAny[types.AccountI](account)) + if err != nil { + return nil, err + } + } + + return queryResp, nil +} + +// GetAccountsByAddress returns account details based on address. +func (s *Server) GetAccountsByAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQueryServer(s.store.GetAccountKeeper()).Account(queryContext, &authtypes.QueryAccountRequest{ + Address: mux.Vars(r)["address"], + }) + + if err != nil { + return nil, err + } + + err = s.prepareUnpackInterfaces(utils.WrapTypeAny[types.AccountI](queryResp.Account)) + if err != nil { + return nil, err + } + + return queryResp, nil +} diff --git a/client/server/bank.go b/client/server/bank.go new file mode 100644 index 00000000..db77384e --- /dev/null +++ b/client/server/bank.go @@ -0,0 +1,80 @@ +//nolint:wrapcheck // The api server is our server, so we don't need to wrap it +package server + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/types/query" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/gorilla/mux" + + "github.com/piplabs/story/client/server/utils" +) + +func (s *Server) initBankRoute() { + s.httpMux.HandleFunc("/bank/supply/by_denom", utils.AutoWrap(s.aminoCodec, s.GetSupplyByDenom)) + + s.httpMux.HandleFunc("/bank/balances/{address}", utils.AutoWrap(s.aminoCodec, s.GetBalancesByAddress)) + s.httpMux.HandleFunc("/bank/balances/{address}/by_denom", utils.AutoWrap(s.aminoCodec, s.GetBalancesByAddressDenom)) +} + +// GetSupplyByDenom queries the supply of a single coin. +func (s *Server) GetSupplyByDenom(req *getSupplyByDenomRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := s.store.GetBankKeeper().SupplyOf(queryContext, &banktypes.QuerySupplyOfRequest{ + Denom: req.Denom, + }) + if err != nil { + return nil, err + } + + return queryResp, err +} + +// GetBalancesByAddress queries the balance of all coins for a single account. +func (s *Server) GetBalancesByAddress(req *getBalancesByAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := s.store.GetBankKeeper().AllBalances(queryContext, &banktypes.QueryAllBalancesRequest{ + Address: mux.Vars(r)["address"], + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + ResolveDenom: req.ResolveDenom, + }) + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetBalancesByAddressDenom queries the balance of a single coin for a single account. +func (s *Server) GetBalancesByAddressDenom(req *getBalancesByAddressDenomRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := s.store.GetBankKeeper().Balance(queryContext, &banktypes.QueryBalanceRequest{ + Address: mux.Vars(r)["address"], + Denom: req.Denom, + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} diff --git a/client/server/cometbft.go b/client/server/cometbft.go new file mode 100644 index 00000000..2cdbc226 --- /dev/null +++ b/client/server/cometbft.go @@ -0,0 +1,54 @@ +package server + +import ( + "errors" + "net/http" + "slices" + + abcitypes "github.com/cometbft/cometbft/abci/types" + + "github.com/piplabs/story/client/server/utils" + liberrors "github.com/piplabs/story/lib/errors" +) + +func (s *Server) initComeBFTRoute() { + s.httpMux.HandleFunc("/comebft/block_events", utils.AutoWrap(s.aminoCodec, s.GetComebftBlockEvents)) +} + +func (s *Server) GetComebftBlockEvents(req *getComebftBlockEventsRequest, r *http.Request) (resp any, err error) { + if req.To-req.From > 100 { + return nil, errors.New("search max 100 blocks") + } + + if len(req.EventTypeFilter) == 0 { + return nil, errors.New("event filter empty") + } + + curBlock, err := s.cl.Block(r.Context(), nil) + if err != nil { + return nil, liberrors.Wrap(err, "failed to get the current block") + } + + allRetBlock := make([]*getComebftBlockEventsBlockResults, 0) + for i := req.From; i < min(req.To, curBlock.Block.Height); i++ { + results, err := s.cl.BlockResults(r.Context(), &i) + if err != nil { + return nil, liberrors.Wrap(err, "failed to get block result") + } + + events := slices.DeleteFunc(results.FinalizeBlockEvents, func(event abcitypes.Event) bool { + return !slices.Contains(req.EventTypeFilter, event.Type) + }) + + if len(events) > 0 { + allRetBlock = append(allRetBlock, &getComebftBlockEventsBlockResults{ + Height: results.Height, + FinalizeBlockEvents: slices.DeleteFunc(results.FinalizeBlockEvents, func(event abcitypes.Event) bool { + return !slices.Contains(req.EventTypeFilter, event.Type) + }), + }) + } + } + + return allRetBlock, nil +} diff --git a/client/server/distribution.go b/client/server/distribution.go new file mode 100644 index 00000000..e5315216 --- /dev/null +++ b/client/server/distribution.go @@ -0,0 +1,224 @@ +//nolint:wrapcheck // The api server is our server, so we don't need to wrap it. +package server + +import ( + "errors" + "net/http" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/gorilla/mux" + + "github.com/piplabs/story/client/server/utils" +) + +func (s *Server) initDistributionRoute() { + s.httpMux.HandleFunc("/distribution/validators/{validator_address}", utils.SimpleWrap(s.aminoCodec, s.GetDistributionValidatorByValidatorAddress)) + s.httpMux.HandleFunc("/distribution/validators/{validator_address}/commission", utils.SimpleWrap(s.aminoCodec, s.GetValidatorCommissionByValidatorAddress)) + s.httpMux.HandleFunc("/distribution/validators/{validator_address}/outstanding_rewards", utils.SimpleWrap(s.aminoCodec, s.GetValidatorOutstandingRewardsByValidatorAddress)) + s.httpMux.HandleFunc("/distribution/validators/{validator_address}/slashes", utils.AutoWrap(s.aminoCodec, s.GetValidatorSlashesByValidatorAddress)) + s.httpMux.HandleFunc("/distribution/all_validators/outstanding_rewards", utils.AutoWrap(s.aminoCodec, s.GetAllValidatorOutstandingRewards)) + + s.httpMux.HandleFunc("/distribution/delegators/{delegator_address}/validators", utils.SimpleWrap(s.aminoCodec, s.GetDistributionValidatorsByDelegatorAddress)) + s.httpMux.HandleFunc("/distribution/delegators/{delegator_address}/rewards", utils.SimpleWrap(s.aminoCodec, s.GetDelegatorRewardsByDelegatorAddress)) + s.httpMux.HandleFunc("/distribution/delegators/{delegator_address}/rewards/{validator_address}", utils.SimpleWrap(s.aminoCodec, s.GetDelegatorRewardsByDelegatorAddressValidatorAddress)) + s.httpMux.HandleFunc("/distribution/delegators/{delegator_address}/withdraw_address", utils.SimpleWrap(s.aminoCodec, s.GetDelegatorWithdrawAddressByDelegatorAddress)) +} + +// GetDistributionValidatorByValidatorAddress queries validator commission and self-delegation rewards for validator. +func (s *Server) GetDistributionValidatorByValidatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).ValidatorDistributionInfo(queryContext, &distributiontypes.QueryValidatorDistributionInfoRequest{ + ValidatorAddress: mux.Vars(r)["validator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetValidatorCommissionByValidatorAddress queries accumulated commission for a validator. +func (s *Server) GetValidatorCommissionByValidatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).ValidatorCommission(queryContext, &distributiontypes.QueryValidatorCommissionRequest{ + ValidatorAddress: mux.Vars(r)["validator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetValidatorOutstandingRewardsByValidatorAddress queries rewards of a validator address. +func (s *Server) GetValidatorOutstandingRewardsByValidatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).ValidatorOutstandingRewards(queryContext, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ + ValidatorAddress: mux.Vars(r)["validator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetAllValidatorOutstandingRewards queries rewards of all validators. +func (s *Server) GetAllValidatorOutstandingRewards(req *getAllValidatorOutstandingRewardsRequest, r *http.Request) (resp any, err error) { + if req.To-req.From > 100 { + return nil, errors.New("search max 100 blocks") + } + + curBlock, err := s.cl.Block(r.Context(), nil) + if err != nil { + return nil, errors.Join(errors.New("curbock fetch fail"), err) + } + + querier := keeper.NewQuerier(s.store.GetDistrKeeper()) + result := make([]*getAllValidatorOutstandingRewardsRequestBlockResults, 0) + + for i := req.From; i < min(req.To, curBlock.Block.Height); i++ { + queryContext, err := s.store.CreateQueryContext(i, false) + if err != nil { + return nil, errors.Join(errors.New("create query context fail"), err) + } + + blockResult := &getAllValidatorOutstandingRewardsRequestBlockResults{ + Height: i, + Validators: make(map[string]sdk.DecCoins), + } + //nolint: contextcheck // false positive + querier.IterateValidatorOutstandingRewards(queryContext, func(val sdk.ValAddress, rewards distributiontypes.ValidatorOutstandingRewards) (stop bool) { + if len(rewards.Rewards) > 0 { + blockResult.Validators[val.String()] = rewards.Rewards + } + + return false + }) + + if len(blockResult.Validators) > 0 { + result = append(result, blockResult) + } + } + + return result, nil +} + +// GetValidatorSlashesByValidatorAddress queries slash events of a validator. +func (s *Server) GetValidatorSlashesByValidatorAddress(req *getValidatorSlashesByValidatorAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).ValidatorSlashes(queryContext, &distributiontypes.QueryValidatorSlashesRequest{ + ValidatorAddress: mux.Vars(r)["validator_address"], + StartingHeight: req.StartingHeight, + EndingHeight: req.EndingHeight, + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetDistributionValidatorsByDelegatorAddress queries the validators of a delegator. +func (s *Server) GetDistributionValidatorsByDelegatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).DelegatorValidators(queryContext, &distributiontypes.QueryDelegatorValidatorsRequest{ + DelegatorAddress: mux.Vars(r)["delegator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetDelegatorRewardsByDelegatorAddress queries the total rewards accrued by each validator. +func (s *Server) GetDelegatorRewardsByDelegatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).DelegationTotalRewards(queryContext, &distributiontypes.QueryDelegationTotalRewardsRequest{ + DelegatorAddress: mux.Vars(r)["delegator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetDelegatorRewardsByDelegatorAddressValidatorAddress queries the total rewards accrued by a delegation. +func (s *Server) GetDelegatorRewardsByDelegatorAddressValidatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + muxVars := mux.Vars(r) + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).DelegationRewards(queryContext, &distributiontypes.QueryDelegationRewardsRequest{ + DelegatorAddress: muxVars["delegator_address"], + ValidatorAddress: muxVars["validator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetDelegatorWithdrawAddressByDelegatorAddress queries withdraw address of a delegator. +func (s *Server) GetDelegatorWithdrawAddressByDelegatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetDistrKeeper()).DelegatorWithdrawAddress(queryContext, &distributiontypes.QueryDelegatorWithdrawAddressRequest{ + DelegatorAddress: mux.Vars(r)["delegator_address"], + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} diff --git a/client/server/payload.go b/client/server/payload.go new file mode 100644 index 00000000..94854121 --- /dev/null +++ b/client/server/payload.go @@ -0,0 +1,90 @@ +package server + +import ( + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type pagination struct { + Key string `mapstructure:"key"` + Offset uint64 `mapstructure:"offset"` + Limit uint64 `mapstructure:"limit"` + CountTotal bool `mapstructure:"count_total"` + Reverse bool `mapstructure:"reverse"` +} + +type getValidatorsRequest struct { + Status string `mapstructure:"status"` + Pagination pagination `mapstructure:"pagination"` +} + +type getValidatorDelegationsByValidatorAddressRequest struct { + Pagination pagination `mapstructure:"pagination"` +} + +type getDelegationsByDelegatorAddressRequest struct { + Pagination pagination `mapstructure:"pagination"` +} + +type getRedelegationsByDelegatorAddressRequest struct { + SrcValidatorAddr string `mapstructure:"src_validator_addr"` + DstValidatorAddr string `mapstructure:"dst_validator_addr"` + Pagination pagination `mapstructure:"pagination"` +} + +type getUnbondingDelegationsByDelegatorAddressRequest struct { + Pagination pagination `mapstructure:"pagination"` +} + +type getValidatorsByDelegatorAddressRequest struct { + Pagination pagination `mapstructure:"pagination"` +} + +type getAccountsRequest struct { + Pagination pagination `mapstructure:"pagination"` +} + +type getSupplyByDenomRequest struct { + Denom string `mapstructure:"denom"` +} + +type getBalancesByAddressRequest struct { + ResolveDenom bool `mapstructure:"resolve_denom"` + Pagination pagination `mapstructure:"pagination"` +} + +type getBalancesByAddressDenomRequest struct { + Denom string `mapstructure:"denom"` + Pagination pagination `mapstructure:"pagination"` +} + +type getValidatorSlashesByValidatorAddressRequest struct { + StartingHeight uint64 `mapstructure:"starting_height"` + EndingHeight uint64 `mapstructure:"ending_height"` + Pagination pagination `mapstructure:"pagination"` +} + +type getComebftBlockEventsRequest struct { + From int64 `mapstructure:"from"` + To int64 `mapstructure:"to"` + EventTypeFilter []string `mapstructure:"event_type_filter"` +} + +type getComebftBlockEventsBlockResults struct { + Height int64 `json:"height"` + FinalizeBlockEvents []abci.Event `json:"finalize_block_events"` +} + +type getAllValidatorOutstandingRewardsRequest struct { + From int64 `mapstructure:"from"` + To int64 `mapstructure:"to"` +} + +type getAllValidatorOutstandingRewardsRequestBlockResults struct { + Height int64 `json:"height"` + Validators map[string]sdk.DecCoins `json:"validators"` +} + +type getModuleVersionsRequest struct { + ModuleName string `mapstructure:"module_name"` +} diff --git a/client/server/server.go b/client/server/server.go new file mode 100644 index 00000000..6e9b76c6 --- /dev/null +++ b/client/server/server.go @@ -0,0 +1,145 @@ +//nolint:wrapcheck // The api server is our server, so we don't need to wrap it. +package server + +import ( + "context" + "errors" + "net/http" + "strconv" + "time" + + "cosmossdk.io/x/tx/signing" + upgradekeeper "cosmossdk.io/x/upgrade/keeper" + + rpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/std" + sdk "github.com/cosmos/cosmos-sdk/types" + authcodec "github.com/cosmos/cosmos-sdk/x/auth/codec" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/gogoproto/proto" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" +) + +type Store interface { + CreateQueryContext(height int64, prove bool) (sdk.Context, error) + GetStakingKeeper() *stakingkeeper.Keeper + GetAccountKeeper() authkeeper.AccountKeeper + GetBankKeeper() bankkeeper.Keeper + GetDistrKeeper() distrkeeper.Keeper + GetUpgradeKeeper() *upgradekeeper.Keeper +} + +type Server struct { + errChan chan error + store Store + cl rpcclient.Client + + httpMux *mux.Router + httpServer *http.Server + protoCodec *codec.ProtoCodec + aminoCodec *codec.LegacyAmino +} + +func NewServer(store Store, cl rpcclient.Client, listenAddress string, enableUnsafeCORS bool) (*Server, error) { + s := &Server{ + errChan: make(chan error), + store: store, + httpMux: mux.NewRouter(), + cl: cl, + } + + if err := s.registerCodec(); err != nil { + return nil, err + } + s.registerHandle() + + var svrHandler http.Handler = s.httpMux + if enableUnsafeCORS { + svrHandler = handlers.CORS()(s.httpMux) + } + s.httpServer = &http.Server{ + Addr: listenAddress, + Handler: svrHandler, + ReadHeaderTimeout: 60 * time.Second, + } + + return s, nil +} + +func (s *Server) registerCodec() error { + sdkConfig := sdk.GetConfig() + reg, err := codectypes.NewInterfaceRegistryWithOptions(codectypes.InterfaceRegistryOptions{ + ProtoFiles: proto.HybridResolver, + SigningOptions: signing.Options{ + AddressCodec: authcodec.NewBech32Codec(sdkConfig.GetBech32AccountAddrPrefix()), + ValidatorAddressCodec: authcodec.NewBech32Codec(sdkConfig.GetBech32ValidatorAddrPrefix()), + }, + }) + if err != nil { + return err + } + + s.protoCodec = codec.NewProtoCodec(reg) + s.aminoCodec = codec.NewLegacyAmino() + + // IMPORTANT: register related types so that we could unpack values from Any. + std.RegisterInterfaces(s.protoCodec.InterfaceRegistry()) + std.RegisterLegacyAminoCodec(s.aminoCodec) + authtypes.RegisterInterfaces(s.protoCodec.InterfaceRegistry()) + authtypes.RegisterLegacyAminoCodec(s.aminoCodec) + + return nil +} + +func (s *Server) prepareUnpackInterfaces(v codectypes.UnpackInterfacesMessage) error { + err := codectypes.UnpackInterfaces(v, s.protoCodec) + if err != nil { + return err + } + + return codectypes.UnpackInterfaces(v, codectypes.AminoJSONPacker{Cdc: s.aminoCodec.Amino}) +} + +func (s *Server) registerHandle() { + s.initStakingRoute() + s.initAuthRoute() + s.initBankRoute() + s.initDistributionRoute() + s.initComeBFTRoute() + s.initUpgradeRoute() +} + +func (s *Server) createQueryContextByHeader(r *http.Request) (sdk.Context, error) { + height, err := strconv.ParseInt(r.Header.Get("X-Height"), 10, 64) + if err != nil { + height = 0 + } + + return s.store.CreateQueryContext(height, false) +} + +func (s *Server) Start() error { + go func() { + if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.errChan <- err + } + }() + + select { + case <-time.After(time.Second): + return nil + case err := <-s.errChan: + return err + } +} + +func (s *Server) Stop(ctx context.Context) error { + return s.httpServer.Shutdown(ctx) +} diff --git a/client/server/staking.go b/client/server/staking.go new file mode 100644 index 00000000..9fe17472 --- /dev/null +++ b/client/server/staking.go @@ -0,0 +1,276 @@ +//nolint:wrapcheck // The api server is our server, so we don't need to wrap it. +package server + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/gorilla/mux" + + "github.com/piplabs/story/client/server/utils" +) + +func (s *Server) initStakingRoute() { + s.httpMux.HandleFunc("/staking/pool", utils.SimpleWrap(s.aminoCodec, s.GetStakingPool)) + + s.httpMux.HandleFunc("/staking/validators", utils.AutoWrap(s.aminoCodec, s.GetValidators)) + s.httpMux.HandleFunc("/staking/validators/{validator_addr}", utils.SimpleWrap(s.aminoCodec, s.GetValidatorByValidatorAddress)) + s.httpMux.HandleFunc("/staking/validators/{validator_addr}/delegations", utils.AutoWrap(s.aminoCodec, s.GetValidatorDelegationsByValidatorAddress)) + s.httpMux.HandleFunc("/staking/validators/{validator_addr}/delegations/{delegator_addr}", utils.SimpleWrap(s.aminoCodec, s.GetDelegationByValidatorAddressDelegatorAddress)) + + s.httpMux.HandleFunc("/staking/delegations/{delegator_addr}", utils.AutoWrap(s.aminoCodec, s.GetDelegationsByDelegatorAddress)) + s.httpMux.HandleFunc("/staking/delegators/{delegator_addr}/redelegations", utils.AutoWrap(s.aminoCodec, s.GetRedelegationsByDelegatorAddress)) + s.httpMux.HandleFunc("/staking/delegators/{delegator_addr}/unbonding_delegations", utils.AutoWrap(s.aminoCodec, s.GetUnbondingDelegationsByDelegatorAddress)) + s.httpMux.HandleFunc("/staking/delegators/{delegator_addr}/validators", utils.AutoWrap(s.aminoCodec, s.GetValidatorsByDelegatorAddress)) + s.httpMux.HandleFunc("/staking/delegators/{delegator_addr}/validators/{validator_addr}", utils.SimpleWrap(s.aminoCodec, s.GetValidatorsByDelegatorAddressValidatorAddress)) +} + +// GetStakingPool queries the staking pool info. +func (s *Server) GetStakingPool(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).Pool(queryContext, &stakingtypes.QueryPoolRequest{}) + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetValidators queries all validators that match the given status. +func (s *Server) GetValidators(req *getValidatorsRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).Validators(queryContext, &stakingtypes.QueryValidatorsRequest{ + Status: req.Status, + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + for _, validator := range queryResp.Validators { + err = s.prepareUnpackInterfaces(validator) + if err != nil { + return nil, err + } + } + + return queryResp, nil +} + +// GetValidatorByValidatorAddress queries validator info for given validator address. +func (s *Server) GetValidatorByValidatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).Validator(queryContext, &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: mux.Vars(r)["validator_addr"], + }) + + if err != nil { + return nil, err + } + + err = s.prepareUnpackInterfaces(queryResp.Validator) + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetValidatorDelegationsByValidatorAddress queries delegate info for given validator. +func (s *Server) GetValidatorDelegationsByValidatorAddress(req *getValidatorDelegationsByValidatorAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).ValidatorDelegations(queryContext, &stakingtypes.QueryValidatorDelegationsRequest{ + ValidatorAddr: mux.Vars(r)["validator_addr"], + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetDelegationByValidatorAddressDelegatorAddress queries delegate info for given validator delegator pair. +func (s *Server) GetDelegationByValidatorAddressDelegatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + muxVars := mux.Vars(r) + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).Delegation(queryContext, &stakingtypes.QueryDelegationRequest{ + ValidatorAddr: muxVars["validator_addr"], + DelegatorAddr: muxVars["delegator_addr"], + }) + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetDelegationsByDelegatorAddress queries all delegations of a given delegator address. +func (s *Server) GetDelegationsByDelegatorAddress(req *getDelegationsByDelegatorAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).DelegatorDelegations(queryContext, &stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: mux.Vars(r)["delegator_addr"], + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetRedelegationsByDelegatorAddress queries redelegations of given address. +func (s *Server) GetRedelegationsByDelegatorAddress(req *getRedelegationsByDelegatorAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).Redelegations(queryContext, &stakingtypes.QueryRedelegationsRequest{ + DelegatorAddr: mux.Vars(r)["delegator_addr"], + SrcValidatorAddr: req.SrcValidatorAddr, + DstValidatorAddr: req.DstValidatorAddr, + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetUnbondingDelegationsByDelegatorAddress queries all unbonding delegations of a given delegator address. +func (s *Server) GetUnbondingDelegationsByDelegatorAddress(req *getUnbondingDelegationsByDelegatorAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).DelegatorUnbondingDelegations(queryContext, &stakingtypes.QueryDelegatorUnbondingDelegationsRequest{ + DelegatorAddr: mux.Vars(r)["delegator_addr"], + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + return queryResp, nil +} + +// GetValidatorsByDelegatorAddress queries all validators info for given delegator address. +func (s *Server) GetValidatorsByDelegatorAddress(req *getValidatorsByDelegatorAddressRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).DelegatorValidators(queryContext, &stakingtypes.QueryDelegatorValidatorsRequest{ + DelegatorAddr: mux.Vars(r)["delegator_addr"], + Pagination: &query.PageRequest{ + Key: []byte(req.Pagination.Key), + Offset: req.Pagination.Offset, + Limit: req.Pagination.Limit, + CountTotal: req.Pagination.CountTotal, + Reverse: req.Pagination.Reverse, + }, + }) + + if err != nil { + return nil, err + } + + for _, validator := range queryResp.Validators { + err = s.prepareUnpackInterfaces(validator) + if err != nil { + return nil, err + } + } + + return queryResp, nil +} + +// GetValidatorsByDelegatorAddressValidatorAddress queries validator info for given delegator validator pair. +func (s *Server) GetValidatorsByDelegatorAddressValidatorAddress(r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + muxVars := mux.Vars(r) + queryResp, err := keeper.NewQuerier(s.store.GetStakingKeeper()).DelegatorValidator(queryContext, &stakingtypes.QueryDelegatorValidatorRequest{ + DelegatorAddr: muxVars["delegator_addr"], + ValidatorAddr: muxVars["validator_addr"], + }) + + if err != nil { + return nil, err + } + + err = s.prepareUnpackInterfaces(queryResp.Validator) + if err != nil { + return nil, err + } + + return queryResp, nil +} diff --git a/client/server/upgrade.go b/client/server/upgrade.go new file mode 100644 index 00000000..4766d5bc --- /dev/null +++ b/client/server/upgrade.go @@ -0,0 +1,31 @@ +//nolint:wrapcheck // The api server is our server, so we don't need to wrap it +package server + +import ( + "net/http" + + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/piplabs/story/client/server/utils" +) + +func (s *Server) initUpgradeRoute() { + s.httpMux.HandleFunc("/upgrade/module_versions", utils.AutoWrap(s.aminoCodec, s.ModuleVersions)) +} + +// GetAccounts returns all the existing accounts. +func (s *Server) ModuleVersions(req *getModuleVersionsRequest, r *http.Request) (resp any, err error) { + queryContext, err := s.createQueryContextByHeader(r) + if err != nil { + return nil, err + } + + queryResp, err := s.store.GetUpgradeKeeper().ModuleVersions(queryContext, &upgradetypes.QueryModuleVersionsRequest{ + ModuleName: req.ModuleName, + }) + if err != nil { + return nil, err + } + + return queryResp, nil +} diff --git a/client/server/utils/any.go b/client/server/utils/any.go new file mode 100644 index 00000000..c49706fe --- /dev/null +++ b/client/server/utils/any.go @@ -0,0 +1,23 @@ +//nolint:wrapcheck // Internal utils, don't need to wrap it. +package utils + +import codectypes "github.com/cosmos/cosmos-sdk/codec/types" + +type unpackAny[T any] struct { + Any *codectypes.Any +} + +// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces, where +// UnpackInterfacesMessage is meant to extend protobuf types (which implement +// proto.Message) to support a post-deserialization phase which unpacks +// types packed within Any's using the whitelist provided by AnyUnpacker. +func (t *unpackAny[T]) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + var value T + return unpacker.UnpackAny(t.Any, &value) +} + +// WrapTypeAny implements a wrap function for variables with type Any to become UnpackInterfacesMessage, +// which is meant to unpack values packed within Any's using the AnyUnpacker. +func WrapTypeAny[T any](v *codectypes.Any) codectypes.UnpackInterfacesMessage { + return &unpackAny[T]{Any: v} +} diff --git a/client/server/utils/error.go b/client/server/utils/error.go new file mode 100644 index 00000000..4b776762 --- /dev/null +++ b/client/server/utils/error.go @@ -0,0 +1,31 @@ +package utils + +import "errors" + +type HTTPError struct { + error + + errorCode uint32 +} + +func (e HTTPError) Unwrap() error { + return e.error +} + +func WrapHTTPError(err error) *HTTPError { + return WrapHTTPErrorWithCode(500, err) +} + +func WrapHTTPErrorWithCode(errorCode uint32, err error) *HTTPError { + return &HTTPError{ + error: err, + errorCode: errorCode, + } +} + +func NewHTTPError(errorCode uint32, err string) *HTTPError { + return &HTTPError{ + error: errors.New(err), + errorCode: errorCode, + } +} diff --git a/client/server/utils/querymap.go b/client/server/utils/querymap.go new file mode 100644 index 00000000..ed74ae51 --- /dev/null +++ b/client/server/utils/querymap.go @@ -0,0 +1,120 @@ +//nolint:wrapcheck // Internal utils, don't need to wrap it. +package utils + +import ( + "math/big" + "net/url" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/mitchellh/mapstructure" +) + +// QueryMapToVal implements an all-in-one decoder to decode requests' query parameters to +// structured value. +func QueryMapToVal(query url.Values, val any) error { + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Metadata: nil, + Result: val, + WeaklyTypedInput: true, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + stringArrayToNative(), + stringArrayToNativePtr(), + ), + }) + if err != nil { + return err + } + + return decoder.Decode(buildMap(query)) +} + +func stringArrayToNativePtr() mapstructure.DecodeHookFunc { + return func(f reflect.Type, t reflect.Type, data any) (any, error) { + if f.Kind() != reflect.Slice || t.Kind() != reflect.Ptr { + return data, nil + } + + as, ok := data.([]string) + if !ok || len(as) == 0 { + return data, nil + } + + from := as[0] + t = t.Elem() + switch t.String() { + case "big.Int": + if i, ok := big.NewInt(0).SetString(from, 10); ok { + return i, nil + } + default: + return data, nil + } + + return data, nil + } +} + +func stringArrayToNative() mapstructure.DecodeHookFunc { + return func(f reflect.Kind, t reflect.Kind, data any) (any, error) { + if f != reflect.Slice { + return data, nil + } + + as, ok := data.([]string) + if !ok || len(as) == 0 { + return data, nil + } + + from := as[0] + switch t { + case reflect.Bool: + return strconv.ParseBool(from) + case reflect.Uint64: + return strconv.ParseUint(from, 10, 64) + case reflect.Int: + return strconv.ParseInt(from, 10, 64) + case reflect.Uint: + parseUint, err := strconv.ParseUint(from, 10, 64) + return uint(parseUint), err + case reflect.Float32: + return strconv.ParseFloat(from, 32) + case reflect.Float64: + return strconv.ParseFloat(from, 64) + case reflect.String: + return from, nil + default: + return data, nil + } + } +} + +func buildMap(query url.Values, prefix ...string) (ret map[string]any) { + fullPrefix := strings.Join(prefix, ".") + if len(fullPrefix) > 0 { + fullPrefix += "." + } + + ret = make(map[string]any) + prefixMapped := make(map[string]bool) + for k, vs := range query { + if rk := strings.TrimPrefix(k, fullPrefix); fullPrefix == "" || rk != k { + rk = strings.TrimSuffix(rk, "[]") + if prefixIndex := strings.IndexByte(rk, '.'); prefixIndex != -1 { + rk = rk[:prefixIndex] + if !prefixMapped[rk] { + prefixMapped[rk] = true + ret[rk] = buildMap(query, append(slices.Clone(prefix), rk)...) + } + } else { + ret[rk] = vs + } + } + } + + return ret +} diff --git a/client/server/utils/wrap.go b/client/server/utils/wrap.go new file mode 100644 index 00000000..94951bce --- /dev/null +++ b/client/server/utils/wrap.go @@ -0,0 +1,91 @@ +package utils + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/go-playground/validator/v10" +) + +var validate = validator.New() + +type SimpleWrapFunc func(r *http.Request) (resp any, err error) +type AutoSimpleInterfaceWrapFunc[T any] func(typ *T, r *http.Request) (resp any, err error) +type RespFunc func(w http.ResponseWriter, r *http.Request) + +type Response struct { + Code int `json:"code"` + Msg json.RawMessage `json:"msg"` + Error string `json:"error"` +} + +// SimpleWrap implements a response encoder for requests without any query paramenter. +// +//nolint:nestif // Complicated scenarios. +func SimpleWrap(codec *codec.LegacyAmino, api SimpleWrapFunc) RespFunc { + return func(w http.ResponseWriter, r *http.Request) { + msg, err := api(r) + resp := &Response{} + if err != nil { + resp.Code = 500 + resp.Error = err.Error() + } else { + var msgByte []byte + if codec != nil { + msgByte, err = codec.MarshalJSON(msg) + } else { + msgByte, err = json.Marshal(msg) + } + + if err == nil { + resp.Code = 200 + resp.Msg = msgByte + } else { + resp.Code = 502 + resp.Error = err.Error() + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.Code) + _ = json.NewEncoder(w).Encode(resp) + } +} + +// AutoWrap implements a response encoder for requests with query paramenters. +func AutoWrap[T any](codec *codec.LegacyAmino, api AutoSimpleInterfaceWrapFunc[T]) RespFunc { + typ := reflect.TypeOf(new(T)).Elem() + return SimpleWrap(codec, func(r *http.Request) (resp any, err error) { + val := reflect.New(typ).Interface() + + if r.URL.RawQuery != "" { + err = QueryMapToVal(r.URL.Query(), val) + if err != nil { + return nil, NewHTTPError(http.StatusUnprocessableEntity, fmt.Sprintf("decode `%s` query err: %v", typ.String(), err)) + } + } + + if r.ContentLength > 0 { + err = json.NewDecoder(r.Body).Decode(val) + if err != nil { + return nil, NewHTTPError(http.StatusUnprocessableEntity, fmt.Sprintf("decode `%s` body err: %v", typ.String(), err)) + } + } + + err = validate.Struct(val) + if err != nil { + return nil, WrapHTTPErrorWithCode(http.StatusUnprocessableEntity, err) + } + + valT, ok := val.(*T) + if !ok { + return nil, WrapHTTPErrorWithCode(http.StatusUnprocessableEntity, errors.New("type assertion failed")) + } + + return api(valT, r) + }) +} diff --git a/client/x/evmengine/keeper/abci.go b/client/x/evmengine/keeper/abci.go new file mode 100644 index 00000000..edf8b041 --- /dev/null +++ b/client/x/evmengine/keeper/abci.go @@ -0,0 +1,256 @@ +package keeper + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "runtime/debug" + "time" + + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + etypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// PrepareProposal returns a proposal for the next block. +// Note returning an error results in a panic cometbft and CONSENSUS_FAILURE log. +func (k *Keeper) PrepareProposal(ctx sdk.Context, req *abci.RequestPrepareProposal) ( + *abci.ResponsePrepareProposal, error, +) { + defer func() { + if r := recover(); r != nil { + log.Error(ctx, "PrepareProposal panic", nil, "recover", r) + fmt.Println("panic stacktrace: \n" + string(debug.Stack())) + panic(r) + } + }() + if len(req.Txs) > 0 { + return nil, errors.New("unexpected transactions in proposal") + } + + if req.Height == 1 { + // Current issue is that InitChain doesn't reset the gas meter. + // So if the first block contains any transactions, we get a app_hash_mismatch + // Since the proposal calculates the incorrect gas for the first block after InitChain. + // The gas meter is reset at the end of the 1st block, so we can then start including txs. + + log.Warn(ctx, "Creating empty initial block due to gas issue", nil) + return &abci.ResponsePrepareProposal{}, nil + } + + appHash := common.BytesToHash(ctx.BlockHeader().AppHash) + + withdrawals, err := k.evmstakingKeeper.PeekEligibleWithdrawals(ctx) + if err != nil { + return nil, errors.Wrap(err, "error on withdrawals dequeue") + } + + // Either use the optimistic payload or create a new one. + payloadID, height, triggeredAt := k.getOptimisticPayload() + if uint64(req.Height) != height { + // Create a new payload (retrying on network errors). + err := retryForever(ctx, func(ctx context.Context) (bool, error) { + response, err := k.startBuild(ctx, k.validatorAddr, withdrawals, appHash, req.Time) + if err != nil { + log.Warn(ctx, "Preparing proposal failed: build new evm payload (will retry)", err) + return false, nil + } else if response.PayloadStatus.Status != engine.VALID { + return false, errors.New("status not valid") + } + + payloadID = response.PayloadID + + return true, nil + }) + if err != nil { + return nil, err + } + triggeredAt = time.Now() + } else { + log.Debug(ctx, "Using optimistic payload", "height", height, "payload", payloadID.String()) + } + + // Wait the minimum build_delay for the payload to be available. + waitTo := triggeredAt.Add(k.buildDelay) + select { + case <-ctx.Done(): + return nil, errors.Wrap(ctx.Err(), "context done") + case <-time.After(time.Until(waitTo)): + } + + // Fetch the payload (retrying on network errors). + var payloadResp *engine.ExecutionPayloadEnvelope + err = retryForever(ctx, func(ctx context.Context) (bool, error) { + var err error + payloadResp, err = k.engineCl.GetPayloadV3(ctx, *payloadID) + if err != nil { + log.Warn(ctx, "Preparing proposal failed: get evm payload (will retry)", err) + return false, nil + } + + return true, nil + }) + if err != nil { + return nil, err + } + + // Create execution payload message + payloadData, err := json.Marshal(payloadResp.ExecutionPayload) + if err != nil { + return nil, errors.Wrap(err, "encode") + } + + // First, collect all vote extension msgs from the vote provider. + // voteMsgs, err := k.voteProvider.PrepareVotes(ctx, req.LocalLastCommit) + // if err != nil { + // return nil, errors.Wrap(err, "prepare votes") + //} + + // Next, collect all prev payload evm event logs. + evmEvents, err := k.evmEvents(ctx, payloadResp.ExecutionPayload.ParentHash) + if err != nil { + return nil, errors.Wrap(err, "prepare evm event logs") + } + + // Then construct the execution payload message. + payloadMsg := &types.MsgExecutionPayload{ + Authority: authtypes.NewModuleAddress(types.ModuleName).String(), + ExecutionPayload: payloadData, + PrevPayloadEvents: evmEvents, + } + + // Combine all the votes messages and the payload message into a single transaction. + b := k.txConfig.NewTxBuilder() + if err := b.SetMsgs(payloadMsg); err != nil { // b.SetMsgs(append(voteMsgs, payloadMsg)...) + return nil, errors.Wrap(err, "set tx builder msgs") + } + + // Note this transaction is not signed. We need to ensure bypass verification somehow. + tx, err := k.txConfig.TxEncoder()(b.GetTx()) + if err != nil { + return nil, errors.Wrap(err, "encode tx builder") + } + + log.Info(ctx, "Proposing new block", + "height", req.Height, + log.Hex7("execution_block_hash", payloadResp.ExecutionPayload.BlockHash[:]), + // "vote_msgs", len(voteMsgs), + "evm_events", len(evmEvents), + ) + + return &abci.ResponsePrepareProposal{Txs: [][]byte{tx}}, nil +} + +// PostFinalize is called by our custom ABCI wrapper after a block is finalized. +// It starts an optimistic build if enabled and if we are the next proposer. +// +// This custom ABCI callback is used since we need to trigger optimistic builds +// immediately after FinalizeBlock with the latest app hash +// which isn't available from cosmosSDK otherwise. +func (k *Keeper) PostFinalize(ctx sdk.Context) error { + if !k.buildOptimistic { + return nil // Not enabled. + } + + // Extract context values + height := ctx.BlockHeight() + proposer := ctx.BlockHeader().ProposerAddress + timestamp := ctx.BlockTime() + appHash := common.BytesToHash(ctx.BlockHeader().AppHash) // This is the app hash after the block is finalized. + + // Maybe start building the next block if we are the next proposer. + isNext, err := k.isNextProposer(ctx, proposer, height) + if err != nil { + return errors.Wrap(err, "next proposer") + } else if !isNext { + return nil // Nothing to do if we are not next proposer. + } + + nextHeight := height + 1 + logAttr := slog.Int64("next_height", nextHeight) + log.Debug(ctx, "Starting optimistic EVM payload build", logAttr) + + // TODO: This will fail because ctx provided in ABCI call in CometBFT 0.37 is context.TODO() + // If optimistic block is discarded, we need to restore the dequeued withdrawals back to the queue. + // Thus, we peek here. If this optimistic block is indeed used in the next block, then we dequeue there. + // If this block is not used in the next block, the block itself is discarded and the queue is unaffected. + withdrawals, err := k.evmstakingKeeper.PeekEligibleWithdrawals(ctx) // context is "context.TODO()"" (empty) in CometBFT v0.38 + if err != nil { + log.Error(ctx, "Starting optimistic build failed; withdrawals peek", err, logAttr) + return nil + } + + fcr, err := k.startBuild(ctx, k.validatorAddr, withdrawals, appHash, timestamp) + if err != nil || isUnknown(fcr.PayloadStatus) { + log.Warn(ctx, "Starting optimistic build failed", err, logAttr) + return nil + } else if isSyncing(fcr.PayloadStatus) { + log.Warn(ctx, "Starting optimistic build failed; evm syncing", nil, logAttr) + return nil + } else if invalid, err := isInvalid(fcr.PayloadStatus); invalid { + log.Error(ctx, "Starting optimistic build failed; invalid payload [BUG]", err, logAttr) + return nil + } + + k.setOptimisticPayload(fcr.PayloadID, uint64(nextHeight)) + + return nil +} + +// startBuild triggers the building of a new execution payload on top of the current execution head. +// It returns the EngineAPI response which contains a status and payload ID. +func (k *Keeper) startBuild(ctx context.Context, feeRecipient common.Address, withdrawals []*etypes.Withdrawal, appHash common.Hash, timestamp time.Time) (engine.ForkChoiceResponse, error) { + head, err := k.getExecutionHead(ctx) + if err != nil { + return engine.ForkChoiceResponse{}, errors.Wrap(err, "latest execution block") + } + + // Use provided time as timestamp for the next block. + // Or use latest execution block timestamp + 1 if is not greater. + // Since execution blocks must have unique second-granularity timestamps. + ts := uint64(timestamp.Unix()) + if ts <= head.GetBlockTime() { + ts = head.GetBlockTime() + 1 // Subsequent blocks must have a higher timestamp. + } + + // CometBFT has instant finality, so head/safe/finalized is latest height. + fcs := engine.ForkchoiceStateV1{ + HeadBlockHash: head.Hash(), + SafeBlockHash: head.Hash(), + FinalizedBlockHash: head.Hash(), + } + + log.Debug(ctx, "Submit new EVM payload", + "timestamp", timestamp, + "withdrawals", len(withdrawals), + "app_hash", appHash.String(), + ) + + // if withdrawals is empty, replace with empty object + if len(withdrawals) == 0 { + withdrawals = []*etypes.Withdrawal{} + } + + attrs := &engine.PayloadAttributes{ + Timestamp: ts, + Random: head.Hash(), // We use head block hash as randao. + SuggestedFeeRecipient: feeRecipient, + Withdrawals: withdrawals, + BeaconRoot: &appHash, + } + + resp, err := k.engineCl.ForkchoiceUpdatedV3(ctx, fcs, attrs) + if err != nil { + return engine.ForkChoiceResponse{}, errors.Wrap(err, "forkchoice update") + } + + return resp, nil +} diff --git a/client/x/evmengine/keeper/abci_internal_test.go b/client/x/evmengine/keeper/abci_internal_test.go new file mode 100644 index 00000000..0fd30ee5 --- /dev/null +++ b/client/x/evmengine/keeper/abci_internal_test.go @@ -0,0 +1,467 @@ +package keeper + +import ( + "bytes" + "context" + "encoding/json" + "math/big" + "testing" + "time" + + "cosmossdk.io/core/store" + storetypes "cosmossdk.io/store/types" + "cosmossdk.io/x/tx/signing" + + abci "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + cmttime "github.com/cometbft/cometbft/types/time" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/runtime" + cosmosstd "github.com/cosmos/cosmos-sdk/std" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + authcodec "github.com/cosmos/cosmos-sdk/x/auth/codec" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + atypes "github.com/cosmos/cosmos-sdk/x/auth/types" + btypes "github.com/cosmos/cosmos-sdk/x/bank/types" + dtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/gogoproto/proto" + "github.com/ethereum/go-ethereum" + eengine "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" + + moduletestutil "github.com/piplabs/story/client/x/evmengine/testutil" + etypes "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/ethclient/mock" + + "go.uber.org/mock/gomock" +) + +var zeroAddr common.Address + +func TestKeeper_PrepareProposal(t *testing.T) { + t.Parallel() + + // TestRunErrScenarios tests various error scenarios in the PrepareProposal function. + // It covers cases where different errors are encountered during the preparation of a proposal, + // such as when no transactions are provided, when errors occur while fetching block information, + // or when errors occur during fork choice update. + t.Run("TestRunErrScenarios", func(t *testing.T) { + t.Parallel() + tests := []struct { + name string + mockEngine mockEngineAPI + mockClient mock.MockClient + req *abci.RequestPrepareProposal + wantErr bool + setupMocks func(esk *moduletestutil.MockEvmStakingKeeper) + }{ + { + name: "no transactions", + mockEngine: mockEngineAPI{}, + mockClient: mock.MockClient{}, + req: &abci.RequestPrepareProposal{ + Txs: nil, // Set to nil to simulate no transactions + Height: 1, // Set height to 1 for this test case + Time: time.Now(), // Set time to current time or mock a time + }, + wantErr: false, + }, + { + name: "with transactions", + mockEngine: mockEngineAPI{}, + mockClient: mock.MockClient{}, + req: &abci.RequestPrepareProposal{ + Txs: [][]byte{[]byte("tx1")}, // simulate transactions + Height: 1, + Time: time.Now(), + }, + wantErr: true, + }, + { + name: "forkchoiceUpdateV2 not valid", + mockEngine: mockEngineAPI{ + headerByTypeFunc: func(context.Context, ethclient.HeadType) (*types.Header, error) { + fuzzer := ethclient.NewFuzzer(0) + var header *types.Header + fuzzer.Fuzz(&header) + + return header, nil + }, + forkchoiceUpdatedV3Func: func(ctx context.Context, update eengine.ForkchoiceStateV1, + payloadAttributes *eengine.PayloadAttributes) (eengine.ForkChoiceResponse, error) { + return eengine.ForkChoiceResponse{ + PayloadStatus: eengine.PayloadStatusV1{ + Status: eengine.INVALID, + LatestValidHash: nil, + ValidationError: nil, + }, + PayloadID: nil, + }, nil + }, + }, + mockClient: mock.MockClient{}, + req: &abci.RequestPrepareProposal{ + Txs: nil, + Height: 2, + Time: time.Now(), + }, + wantErr: true, + setupMocks: func(esk *moduletestutil.MockEvmStakingKeeper) { + esk.EXPECT().PeekEligibleWithdrawals(gomock.Any()).Return(nil, nil) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx, storeService := setupCtxStore(t, nil) + cdc := getCodec(t) + txConfig := authtx.NewTxConfig(cdc, nil) + + ctrl := gomock.NewController(t) + ak := moduletestutil.NewMockAccountKeeper(ctrl) + esk := moduletestutil.NewMockEvmStakingKeeper(ctrl) + uk := moduletestutil.NewMockUpgradeKeeper(ctrl) + + if tt.setupMocks != nil { + tt.setupMocks(esk) + } + + var err error + tt.mockEngine.EngineClient, err = ethclient.NewEngineMock() + require.NoError(t, err) + + k, err := NewKeeper(cdc, storeService, &tt.mockEngine, &tt.mockClient, txConfig, ak, esk, uk) + require.NoError(t, err) + k.SetValidatorAddress(common.BytesToAddress([]byte("test"))) + populateGenesisHead(ctx, t, k) + + tt.req.MaxTxBytes = cmttypes.MaxBlockSizeBytes + + _, err = k.PrepareProposal(withRandomErrs(t, ctx), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("PrepareProposal() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } + }) + + t.Run("TestBuildNonOptimistic", func(t *testing.T) { + t.Parallel() + // setup dependencies + ctx, storeService := setupCtxStore(t, nil) + cdc := getCodec(t) + txConfig := authtx.NewTxConfig(cdc, nil) + + mockEngine, err := newMockEngineAPI(0) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + mockClient := mock.NewMockClient(ctrl) + ak := moduletestutil.NewMockAccountKeeper(ctrl) + esk := moduletestutil.NewMockEvmStakingKeeper(ctrl) + uk := moduletestutil.NewMockUpgradeKeeper(ctrl) + + // Expected call for PeekEligibleWithdrawals + esk.EXPECT().PeekEligibleWithdrawals(gomock.Any()).Return(nil, nil).AnyTimes() + + keeper, err := NewKeeper(cdc, storeService, &mockEngine, mockClient, txConfig, ak, esk, uk) + require.NoError(t, err) + keeper.SetValidatorAddress(common.BytesToAddress([]byte("test"))) + populateGenesisHead(ctx, t, keeper) + + // get the genesis block to build on top of + // Get the parent block we will build on top of + head, err := keeper.getExecutionHead(ctx) + require.NoError(t, err) + + req := &abci.RequestPrepareProposal{ + Txs: nil, + Height: int64(2), + Time: time.Now(), + MaxTxBytes: cmttypes.MaxBlockSizeBytes, + } + + resp, err := keeper.PrepareProposal(withRandomErrs(t, ctx), req) + require.NoError(t, err) + require.NotNil(t, resp) + + msgDelegate := stypes.NewMsgDelegate("delAddr", "valAddr", sdk.NewInt64Coin("stake", 100)) + resp.Txs[0] = appendMsgToTx(t, txConfig, resp.Txs[0], msgDelegate) + + // decode the txn and get the messages + tx, err := txConfig.TxDecoder()(resp.Txs[0]) + require.NoError(t, err) + + // assert that the message is an executable payload + for _, msg := range tx.GetMsgs() { + if _, ok := msg.(*etypes.MsgExecutionPayload); ok { + assertExecutablePayload(t, msg, req.Time.Unix(), head.Hash(), keeper.validatorAddr, head.GetBlockHeight()+1) + } + } + }) +} + +// appendMsgToTx appends the given message to the unpacked transaction and returns the new packed transaction bytes. +func appendMsgToTx(t *testing.T, txConfig client.TxConfig, txBytes []byte, msg sdk.Msg) []byte { + t.Helper() + txn, err := txConfig.TxDecoder()(txBytes) + require.NoError(t, err) + + b := txConfig.NewTxBuilder() + err = b.SetMsgs(append(txn.GetMsgs(), msg)...) + require.NoError(t, err) + + newTxBytes, err := txConfig.TxEncoder()(b.GetTx()) + require.NoError(t, err) + + return newTxBytes +} + +// assertExecutablePayload asserts that the given message is an executable payload with the expected values. +func assertExecutablePayload(t *testing.T, msg sdk.Msg, ts int64, blockHash common.Hash, validatorAddr common.Address, height uint64) { + t.Helper() + executionPayload, ok := msg.(*etypes.MsgExecutionPayload) + require.True(t, ok) + require.NotNil(t, executionPayload) + + payload := new(eengine.ExecutableData) + err := json.Unmarshal(executionPayload.GetExecutionPayload(), payload) + require.NoError(t, err) + require.Equal(t, int64(payload.Timestamp), ts) + require.Equal(t, payload.Random, blockHash) + require.Equal(t, payload.FeeRecipient, validatorAddr) + require.Empty(t, payload.Withdrawals) + require.Equal(t, payload.Number, height) + + // require.Len(t, executionPayload.PrevPayloadEvents, 1) + // evmLog := executionPayload.PrevPayloadEvents[0] + // require.Equal(t, evmLog.Address, zeroAddr.Bytes()) +} + +func ctxWithAppHash(t *testing.T, appHash common.Hash) context.Context { + t.Helper() + ctx, _ := setupCtxStore(t, &cmtproto.Header{AppHash: appHash.Bytes()}) + + return ctx +} + +func setupCtxStore(t *testing.T, header *cmtproto.Header) (sdk.Context, store.KVStoreService) { + t.Helper() + key := storetypes.NewKVStoreKey("test") + storeService := runtime.NewKVStoreService(key) + testCtx := testutil.DefaultContextWithDB(t, key, storetypes.NewTransientStoreKey("transient_test")) + if header == nil { + header = &cmtproto.Header{Time: cmttime.Now()} + } + ctx := testCtx.Ctx.WithBlockHeader(*header) + + return ctx, storeService +} + +func getCodec(t *testing.T) codec.Codec { + t.Helper() + sdkConfig := sdk.GetConfig() + reg, err := codectypes.NewInterfaceRegistryWithOptions(codectypes.InterfaceRegistryOptions{ + ProtoFiles: proto.HybridResolver, + SigningOptions: signing.Options{ + AddressCodec: authcodec.NewBech32Codec(sdkConfig.GetBech32AccountAddrPrefix()), + ValidatorAddressCodec: authcodec.NewBech32Codec(sdkConfig.GetBech32ValidatorAddrPrefix()), + }, + }) + require.NoError(t, err) + + cosmosstd.RegisterInterfaces(reg) + atypes.RegisterInterfaces(reg) + stypes.RegisterInterfaces(reg) + btypes.RegisterInterfaces(reg) + dtypes.RegisterInterfaces(reg) + etypes.RegisterInterfaces(reg) + + return codec.NewProtoCodec(reg) +} + +var _ ethclient.EngineClient = (*mockEngineAPI)(nil) +var _ etypes.EvmEventProcessor = (*mockLogProvider)(nil) +var _ etypes.VoteExtensionProvider = (*mockVEProvider)(nil) + +type mockEngineAPI struct { + ethclient.EngineClient + syncings <-chan struct{} + fuzzer *fuzz.Fuzzer + mock ethclient.EngineClient // avoid repeating the implementation but also allow for custom implementations of mocks + headerByTypeFunc func(context.Context, ethclient.HeadType) (*types.Header, error) + forkchoiceUpdatedV3Func func(context.Context, eengine.ForkchoiceStateV1, *eengine.PayloadAttributes) (eengine.ForkChoiceResponse, error) + newPayloadV3Func func(context.Context, eengine.ExecutableData, []common.Hash, *common.Hash) (eengine.PayloadStatusV1, error) +} + +// newMockEngineAPI returns a new mock engine API with a fuzzer and a mock engine client. +func newMockEngineAPI(syncings int) (mockEngineAPI, error) { + me, err := ethclient.NewEngineMock() + if err != nil { + return mockEngineAPI{}, err + } + + syncs := make(chan struct{}, syncings) + for range syncings { + syncs <- struct{}{} + } + + return mockEngineAPI{ + mock: me, + syncings: syncs, + fuzzer: ethclient.NewFuzzer(time.Now().Truncate(time.Hour * 24).Unix()), + }, nil +} + +type mockVEProvider struct{} + +func (m mockVEProvider) PrepareVotes(_ context.Context, _ abci.ExtendedCommitInfo) ([]sdk.Msg, error) { + coin := sdk.NewInt64Coin("stake", 100) + msg := stypes.NewMsgDelegate("addr", "addr", coin) + + return []sdk.Msg{msg}, nil +} + +type mockLogProvider struct { + deliverErr error +} + +func (m mockLogProvider) Name() string { + return "mock" +} + +func (m mockLogProvider) Prepare(_ context.Context, blockHash common.Hash) ([]*etypes.EVMEvent, error) { + f := fuzz.NewWithSeed(int64(blockHash[0])) + + var topic common.Hash + f.Fuzz(&topic) + + return []*etypes.EVMEvent{{ + Address: zeroAddr.Bytes(), + Topics: [][]byte{topic[:]}, + }}, nil +} + +func (m mockLogProvider) Addresses() []common.Address { + return []common.Address{zeroAddr} +} + +func (m mockLogProvider) Deliver(_ context.Context, _ common.Hash, log *etypes.EVMEvent) error { + if !bytes.Equal(log.Address, zeroAddr.Bytes()) { + panic("unexpected evm log address") + } + + return m.deliverErr +} + +func (m mockEngineAPI) maybeSync() (eengine.PayloadStatusV1, bool) { + select { + case <-m.syncings: + return eengine.PayloadStatusV1{ + Status: eengine.SYNCING, + }, true + default: + return eengine.PayloadStatusV1{}, false + } +} + +func (mockEngineAPI) FilterLogs(context.Context, ethereum.FilterQuery) ([]types.Log, error) { + return nil, nil +} + +func (m *mockEngineAPI) HeaderByType(ctx context.Context, typ ethclient.HeadType) (*types.Header, error) { + if m.headerByTypeFunc != nil { + return m.headerByTypeFunc(ctx, typ) + } + + return m.mock.HeaderByType(ctx, typ) +} + +func (m *mockEngineAPI) NewPayloadV2(ctx context.Context, params eengine.ExecutableData) (eengine.PayloadStatusV1, error) { + return m.mock.NewPayloadV2(ctx, params) +} + +//nolint:nonamedreturns // Required for defer +func (m *mockEngineAPI) NewPayloadV3(ctx context.Context, params eengine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (resp eengine.PayloadStatusV1, err error) { + if status, ok := m.maybeSync(); ok { + defer func() { + resp.Status = status.Status + }() + } + + if m.newPayloadV3Func != nil { + return m.newPayloadV3Func(ctx, params, versionedHashes, beaconRoot) + } + + return m.mock.NewPayloadV3(ctx, params, versionedHashes, beaconRoot) +} + +//nolint:nonamedreturns // Required for defer +func (m *mockEngineAPI) ForkchoiceUpdatedV3(ctx context.Context, update eengine.ForkchoiceStateV1, payloadAttributes *eengine.PayloadAttributes) (resp eengine.ForkChoiceResponse, err error) { + if status, ok := m.maybeSync(); ok { + defer func() { + resp.PayloadStatus.Status = status.Status + }() + } + + if m.forkchoiceUpdatedV3Func != nil { + return m.forkchoiceUpdatedV3Func(ctx, update, payloadAttributes) + } + + return m.mock.ForkchoiceUpdatedV3(ctx, update, payloadAttributes) +} + +func (m *mockEngineAPI) GetPayloadV3(ctx context.Context, payloadID eengine.PayloadID) (*eengine.ExecutionPayloadEnvelope, error) { + return m.mock.GetPayloadV3(ctx, payloadID) +} + +// nextBlock creates a new block with the given height, timestamp, parentHash, and feeRecipient. It also returns the +// payload for the block. It's a utility function for testing. +func (m *mockEngineAPI) nextBlock( + t *testing.T, + height uint64, + timestamp uint64, + parentHash common.Hash, + feeRecipient common.Address, + beaconRoot *common.Hash, +) (*types.Block, eengine.ExecutableData) { + t.Helper() + var header types.Header + m.fuzzer.Fuzz(&header) + header.Number = big.NewInt(int64(height)) + header.Time = timestamp + header.ParentHash = parentHash + header.Coinbase = feeRecipient + header.MixDigest = parentHash + header.ParentBeaconRoot = beaconRoot + + // Convert header to block + block := types.NewBlock(&header, nil, nil, trie.NewStackTrie(nil)) + + // Convert block to payload + env := eengine.BlockToExecutableData(block, big.NewInt(0), nil) + payload := *env.ExecutionPayload + + // Ensure the block is valid + _, err := eengine.ExecutableDataToBlock(payload, nil, beaconRoot) + require.NoError(t, err) + + return block, payload +} + +func withRandomErrs(t *testing.T, ctx sdk.Context) sdk.Context { + t.Helper() + return ctx.WithContext(ethclient.WithRandomErr(ctx, t)) +} diff --git a/client/x/evmengine/keeper/db.go b/client/x/evmengine/keeper/db.go new file mode 100644 index 00000000..b9c8f193 --- /dev/null +++ b/client/x/evmengine/keeper/db.go @@ -0,0 +1,63 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" +) + +func (h *ExecutionHead) Hash() common.Hash { + return common.BytesToHash(h.GetBlockHash()) +} + +// executionHeadID is the ID of the singleton execution head row in the database. +const executionHeadID = 1 + +// InsertGenesisHead inserts the genesis execution head into the database. +func (k *Keeper) InsertGenesisHead(ctx context.Context, executionBlockHash []byte) error { + id, err := k.headTable.InsertReturningId(ctx, &ExecutionHead{ + CreatedHeight: 0, // genesis + BlockHeight: 0, // genesis + BlockHash: executionBlockHash, + BlockTime: 0, // Timestamp isn't critical, skip it in genesis. + }) + if err != nil { + return errors.Wrap(err, "insert genesis head") + } else if id != executionHeadID { + return errors.New("unexpected genesis head id", "id", id) + } + + return nil +} + +// getExecutionHead returns the current execution head. +func (k *Keeper) getExecutionHead(ctx context.Context) (*ExecutionHead, error) { + head, err := k.headTable.Get(ctx, executionHeadID) + if err != nil { + return nil, errors.Wrap(err, "update execution head") + } + + return head, nil +} + +// updateExecutionHead updates the execution head with the given payload. +func (k *Keeper) updateExecutionHead(ctx context.Context, payload engine.ExecutableData) error { + head := &ExecutionHead{ + Id: executionHeadID, + CreatedHeight: uint64(sdk.UnwrapSDKContext(ctx).BlockHeight()), + BlockHeight: payload.Number, + BlockHash: payload.BlockHash.Bytes(), + BlockTime: payload.Timestamp, + } + + err := k.headTable.Update(ctx, head) + if err != nil { + return errors.Wrap(err, "update execution head") + } + + return nil +} diff --git a/client/x/evmengine/keeper/evmengine.cosmos_orm.go b/client/x/evmengine/keeper/evmengine.cosmos_orm.go new file mode 100644 index 00000000..b6c1ee07 --- /dev/null +++ b/client/x/evmengine/keeper/evmengine.cosmos_orm.go @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-go-cosmos-orm. DO NOT EDIT. + +package keeper + +import ( + context "context" + ormlist "cosmossdk.io/orm/model/ormlist" + ormtable "cosmossdk.io/orm/model/ormtable" + ormerrors "cosmossdk.io/orm/types/ormerrors" +) + +type ExecutionHeadTable interface { + Insert(ctx context.Context, executionHead *ExecutionHead) error + InsertReturningId(ctx context.Context, executionHead *ExecutionHead) (uint64, error) + LastInsertedSequence(ctx context.Context) (uint64, error) + Update(ctx context.Context, executionHead *ExecutionHead) error + Save(ctx context.Context, executionHead *ExecutionHead) error + Delete(ctx context.Context, executionHead *ExecutionHead) error + Has(ctx context.Context, id uint64) (found bool, err error) + // Get returns nil and an error which responds true to ormerrors.IsNotFound() if the record was not found. + Get(ctx context.Context, id uint64) (*ExecutionHead, error) + List(ctx context.Context, prefixKey ExecutionHeadIndexKey, opts ...ormlist.Option) (ExecutionHeadIterator, error) + ListRange(ctx context.Context, from, to ExecutionHeadIndexKey, opts ...ormlist.Option) (ExecutionHeadIterator, error) + DeleteBy(ctx context.Context, prefixKey ExecutionHeadIndexKey) error + DeleteRange(ctx context.Context, from, to ExecutionHeadIndexKey) error + + doNotImplement() +} + +type ExecutionHeadIterator struct { + ormtable.Iterator +} + +func (i ExecutionHeadIterator) Value() (*ExecutionHead, error) { + var executionHead ExecutionHead + err := i.UnmarshalMessage(&executionHead) + return &executionHead, err +} + +type ExecutionHeadIndexKey interface { + id() uint32 + values() []interface{} + executionHeadIndexKey() +} + +// primary key starting index.. +type ExecutionHeadPrimaryKey = ExecutionHeadIdIndexKey + +type ExecutionHeadIdIndexKey struct { + vs []interface{} +} + +func (x ExecutionHeadIdIndexKey) id() uint32 { return 0 } +func (x ExecutionHeadIdIndexKey) values() []interface{} { return x.vs } +func (x ExecutionHeadIdIndexKey) executionHeadIndexKey() {} + +func (this ExecutionHeadIdIndexKey) WithId(id uint64) ExecutionHeadIdIndexKey { + this.vs = []interface{}{id} + return this +} + +type executionHeadTable struct { + table ormtable.AutoIncrementTable +} + +func (this executionHeadTable) Insert(ctx context.Context, executionHead *ExecutionHead) error { + return this.table.Insert(ctx, executionHead) +} + +func (this executionHeadTable) Update(ctx context.Context, executionHead *ExecutionHead) error { + return this.table.Update(ctx, executionHead) +} + +func (this executionHeadTable) Save(ctx context.Context, executionHead *ExecutionHead) error { + return this.table.Save(ctx, executionHead) +} + +func (this executionHeadTable) Delete(ctx context.Context, executionHead *ExecutionHead) error { + return this.table.Delete(ctx, executionHead) +} + +func (this executionHeadTable) InsertReturningId(ctx context.Context, executionHead *ExecutionHead) (uint64, error) { + return this.table.InsertReturningPKey(ctx, executionHead) +} + +func (this executionHeadTable) LastInsertedSequence(ctx context.Context) (uint64, error) { + return this.table.LastInsertedSequence(ctx) +} + +func (this executionHeadTable) Has(ctx context.Context, id uint64) (found bool, err error) { + return this.table.PrimaryKey().Has(ctx, id) +} + +func (this executionHeadTable) Get(ctx context.Context, id uint64) (*ExecutionHead, error) { + var executionHead ExecutionHead + found, err := this.table.PrimaryKey().Get(ctx, &executionHead, id) + if err != nil { + return nil, err + } + if !found { + return nil, ormerrors.NotFound + } + return &executionHead, nil +} + +func (this executionHeadTable) List(ctx context.Context, prefixKey ExecutionHeadIndexKey, opts ...ormlist.Option) (ExecutionHeadIterator, error) { + it, err := this.table.GetIndexByID(prefixKey.id()).List(ctx, prefixKey.values(), opts...) + return ExecutionHeadIterator{it}, err +} + +func (this executionHeadTable) ListRange(ctx context.Context, from, to ExecutionHeadIndexKey, opts ...ormlist.Option) (ExecutionHeadIterator, error) { + it, err := this.table.GetIndexByID(from.id()).ListRange(ctx, from.values(), to.values(), opts...) + return ExecutionHeadIterator{it}, err +} + +func (this executionHeadTable) DeleteBy(ctx context.Context, prefixKey ExecutionHeadIndexKey) error { + return this.table.GetIndexByID(prefixKey.id()).DeleteBy(ctx, prefixKey.values()...) +} + +func (this executionHeadTable) DeleteRange(ctx context.Context, from, to ExecutionHeadIndexKey) error { + return this.table.GetIndexByID(from.id()).DeleteRange(ctx, from.values(), to.values()) +} + +func (this executionHeadTable) doNotImplement() {} + +var _ ExecutionHeadTable = executionHeadTable{} + +func NewExecutionHeadTable(db ormtable.Schema) (ExecutionHeadTable, error) { + table := db.GetTable(&ExecutionHead{}) + if table == nil { + return nil, ormerrors.TableNotFound.Wrap(string((&ExecutionHead{}).ProtoReflect().Descriptor().FullName())) + } + return executionHeadTable{table.(ormtable.AutoIncrementTable)}, nil +} + +type EvmengineStore interface { + ExecutionHeadTable() ExecutionHeadTable + + doNotImplement() +} + +type evmengineStore struct { + executionHead ExecutionHeadTable +} + +func (x evmengineStore) ExecutionHeadTable() ExecutionHeadTable { + return x.executionHead +} + +func (evmengineStore) doNotImplement() {} + +var _ EvmengineStore = evmengineStore{} + +func NewEvmengineStore(db ormtable.Schema) (EvmengineStore, error) { + executionHeadTable, err := NewExecutionHeadTable(db) + if err != nil { + return nil, err + } + + return evmengineStore{ + executionHeadTable, + }, nil +} diff --git a/client/x/evmengine/keeper/evmengine.pb.go b/client/x/evmengine/keeper/evmengine.pb.go new file mode 100644 index 00000000..fb693031 --- /dev/null +++ b/client/x/evmengine/keeper/evmengine.pb.go @@ -0,0 +1,205 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc (unknown) +// source: client/x/evmengine/keeper/evmengine.proto + +package keeper + +import ( + _ "cosmossdk.io/api/cosmos/orm/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ExecutionHead defines the execution chain head. +// It is a singleton table; it only has a single row with ID==1. +type ExecutionHead struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // Auto-incremented ID (always and only 1). + CreatedHeight uint64 `protobuf:"varint,2,opt,name=created_height,json=createdHeight,proto3" json:"created_height,omitempty"` // Consensus chain height this execution block was created in. + BlockHeight uint64 `protobuf:"varint,3,opt,name=block_height,json=blockHeight,proto3" json:"block_height,omitempty"` // Execution block height. + BlockHash []byte `protobuf:"bytes,4,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"` // Execution block hash. + BlockTime uint64 `protobuf:"varint,5,opt,name=block_time,json=blockTime,proto3" json:"block_time,omitempty"` // Execution block time. +} + +func (x *ExecutionHead) Reset() { + *x = ExecutionHead{} + if protoimpl.UnsafeEnabled { + mi := &file_client_x_evmengine_keeper_evmengine_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExecutionHead) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecutionHead) ProtoMessage() {} + +func (x *ExecutionHead) ProtoReflect() protoreflect.Message { + mi := &file_client_x_evmengine_keeper_evmengine_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecutionHead.ProtoReflect.Descriptor instead. +func (*ExecutionHead) Descriptor() ([]byte, []int) { + return file_client_x_evmengine_keeper_evmengine_proto_rawDescGZIP(), []int{0} +} + +func (x *ExecutionHead) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ExecutionHead) GetCreatedHeight() uint64 { + if x != nil { + return x.CreatedHeight + } + return 0 +} + +func (x *ExecutionHead) GetBlockHeight() uint64 { + if x != nil { + return x.BlockHeight + } + return 0 +} + +func (x *ExecutionHead) GetBlockHash() []byte { + if x != nil { + return x.BlockHash + } + return nil +} + +func (x *ExecutionHead) GetBlockTime() uint64 { + if x != nil { + return x.BlockTime + } + return 0 +} + +var File_client_x_evmengine_keeper_evmengine_proto protoreflect.FileDescriptor + +var file_client_x_evmengine_keeper_evmengine_proto_rawDesc = []byte{ + 0x0a, 0x29, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, 0x76, 0x6d, 0x65, 0x6e, + 0x67, 0x69, 0x6e, 0x65, 0x2f, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2f, 0x65, 0x76, 0x6d, 0x65, + 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x2e, 0x78, 0x2e, 0x65, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, + 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x1a, 0x17, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x6f, + 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x72, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0xb9, 0x01, 0x0a, 0x0d, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, + 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, + 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x3a, 0x10, 0xf2, 0x9e, 0xd3, 0x8e, 0x03, + 0x0a, 0x0a, 0x06, 0x0a, 0x02, 0x69, 0x64, 0x10, 0x01, 0x18, 0x01, 0x42, 0xf1, 0x01, 0x0a, 0x1d, + 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x78, 0x2e, 0x65, 0x76, 0x6d, + 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x42, 0x0e, 0x45, + 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x69, 0x6c, 0x69, 0x61, 0x64, 0x2f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, + 0x6e, 0x65, 0x2f, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0xa2, 0x02, 0x04, 0x43, 0x58, 0x45, 0x4b, + 0xaa, 0x02, 0x19, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x58, 0x2e, 0x45, 0x76, 0x6d, 0x65, + 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x4b, 0x65, 0x65, 0x70, 0x65, 0x72, 0xca, 0x02, 0x19, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5c, 0x58, 0x5c, 0x45, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, + 0x65, 0x5c, 0x4b, 0x65, 0x65, 0x70, 0x65, 0x72, 0xe2, 0x02, 0x25, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5c, 0x58, 0x5c, 0x45, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5c, 0x4b, 0x65, + 0x65, 0x70, 0x65, 0x72, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0xea, 0x02, 0x1c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x3a, 0x58, 0x3a, 0x3a, 0x45, 0x76, + 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x3a, 0x3a, 0x4b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_client_x_evmengine_keeper_evmengine_proto_rawDescOnce sync.Once + file_client_x_evmengine_keeper_evmengine_proto_rawDescData = file_client_x_evmengine_keeper_evmengine_proto_rawDesc +) + +func file_client_x_evmengine_keeper_evmengine_proto_rawDescGZIP() []byte { + file_client_x_evmengine_keeper_evmengine_proto_rawDescOnce.Do(func() { + file_client_x_evmengine_keeper_evmengine_proto_rawDescData = protoimpl.X.CompressGZIP(file_client_x_evmengine_keeper_evmengine_proto_rawDescData) + }) + return file_client_x_evmengine_keeper_evmengine_proto_rawDescData +} + +var file_client_x_evmengine_keeper_evmengine_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_client_x_evmengine_keeper_evmengine_proto_goTypes = []interface{}{ + (*ExecutionHead)(nil), // 0: client.x.evmengine.keeper.ExecutionHead +} +var file_client_x_evmengine_keeper_evmengine_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_client_x_evmengine_keeper_evmengine_proto_init() } +func file_client_x_evmengine_keeper_evmengine_proto_init() { + if File_client_x_evmengine_keeper_evmengine_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_client_x_evmengine_keeper_evmengine_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExecutionHead); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_client_x_evmengine_keeper_evmengine_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_client_x_evmengine_keeper_evmengine_proto_goTypes, + DependencyIndexes: file_client_x_evmengine_keeper_evmengine_proto_depIdxs, + MessageInfos: file_client_x_evmengine_keeper_evmengine_proto_msgTypes, + }.Build() + File_client_x_evmengine_keeper_evmengine_proto = out.File + file_client_x_evmengine_keeper_evmengine_proto_rawDesc = nil + file_client_x_evmengine_keeper_evmengine_proto_goTypes = nil + file_client_x_evmengine_keeper_evmengine_proto_depIdxs = nil +} diff --git a/client/x/evmengine/keeper/evmengine.proto b/client/x/evmengine/keeper/evmengine.proto new file mode 100644 index 00000000..bc6d5160 --- /dev/null +++ b/client/x/evmengine/keeper/evmengine.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package client.x.evmengine.keeper; + +import "cosmos/orm/v1/orm.proto"; + +option go_package = "client/x/evmengine/keeper"; + +// ExecutionHead defines the execution chain head. +// It is a singleton table; it only has a single row with ID==1. +message ExecutionHead { + option (cosmos.orm.v1.table) = { + id: 1; + primary_key: { fields: "id", auto_increment: true } + }; + + uint64 id = 1; // Auto-incremented ID (always and only 1). + uint64 created_height = 2; // Consensus chain height this execution block was created in. + uint64 block_height = 3; // Execution block height. + bytes block_hash = 4; // Execution block hash. + uint64 block_time = 5; // Execution block time. +} \ No newline at end of file diff --git a/client/x/evmengine/keeper/evmmsgs.go b/client/x/evmengine/keeper/evmmsgs.go new file mode 100644 index 00000000..5b2d28f8 --- /dev/null +++ b/client/x/evmengine/keeper/evmmsgs.go @@ -0,0 +1,71 @@ +package keeper + +import ( + "bytes" + "context" + "slices" + "sort" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/client/genutil/evm/predeploys" + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" +) + +// evmEvents returns selected EVM log events from the provided block hash. +func (k *Keeper) evmEvents(ctx context.Context, blockHash common.Hash) ([]*types.EVMEvent, error) { + var events []*types.EVMEvent + + logs, err := k.engineCl.FilterLogs(ctx, ethereum.FilterQuery{ + BlockHash: &blockHash, + // only IPTokenStaking contract + Addresses: []common.Address{ + common.HexToAddress(predeploys.IPTokenStaking), + common.HexToAddress(predeploys.IPTokenSlashing), + common.HexToAddress(predeploys.UpgradeEntrypoint), + }, + }) + if err != nil { + return nil, errors.Wrap(err, "filter logs") + } + + ll := make([]*types.EVMEvent, 0, len(logs)) + for _, l := range logs { + topics := make([][]byte, 0, len(l.Topics)) + for _, t := range l.Topics { + topics = append(topics, t.Bytes()) + } + ll = append(ll, &types.EVMEvent{ + Address: l.Address.Bytes(), + Topics: topics, + Data: l.Data, + }) + } + + for _, log := range ll { + if err := log.Verify(); err != nil { + return nil, errors.Wrap(err, "verify log") + } + } + events = append(events, ll...) + + // Sort by Address > Topics > Data + // This avoids dependency on runtime ordering. + sort.Slice(events, func(i, j int) bool { + if cmp := bytes.Compare(events[i].Address, events[j].Address); cmp != 0 { + return cmp < 0 + } + + topicI := slices.Concat(events[i].Topics...) + topicJ := slices.Concat(events[j].Topics...) + if cmp := bytes.Compare(topicI, topicJ); cmp != 0 { + return cmp < 0 + } + + return bytes.Compare(events[i].Data, events[j].Data) < 0 + }) + + return events, nil +} diff --git a/client/x/evmengine/keeper/genesis.go b/client/x/evmengine/keeper/genesis.go new file mode 100644 index 00000000..4622ee04 --- /dev/null +++ b/client/x/evmengine/keeper/genesis.go @@ -0,0 +1,42 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" +) + +func (k *Keeper) InitGenesis(ctx context.Context, gs *types.GenesisState) error { + if err := k.ValidateGenesis(gs); err != nil { + return err + } + if err := k.SetParams(ctx, gs.Params); err != nil { + return err + } + + if err := k.InsertGenesisHead(ctx, gs.Params.ExecutionBlockHash); err != nil { + panic(errors.Wrap(err, "insert genesis head")) + } + + return nil +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func (k *Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { + params, err := k.GetParams(ctx) + if err != nil { + panic(err) + } + + return &types.GenesisState{ + Params: params, + } +} + +//nolint:revive // TODO: validate genesis +func (k *Keeper) ValidateGenesis(gs *types.GenesisState) error { + return types.ValidateExecutionBlockHash(gs.Params.ExecutionBlockHash) +} diff --git a/client/x/evmengine/keeper/helpers.go b/client/x/evmengine/keeper/helpers.go new file mode 100644 index 00000000..8a095cb6 --- /dev/null +++ b/client/x/evmengine/keeper/helpers.go @@ -0,0 +1,34 @@ +package keeper + +import ( + "context" + "sync" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/expbackoff" +) + +// backoffFunc aliased for testing. +var ( + backoffFuncMu sync.RWMutex + backoffFunc = expbackoff.New +) + +func retryForever(ctx context.Context, fn func(ctx context.Context) (bool, error)) error { + backoffFuncMu.RLock() + backoff := backoffFunc(ctx) + backoffFuncMu.RUnlock() + for { + ok, err := fn(ctx) + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "retry canceled") + } else if err != nil { + return err + } else if !ok { + backoff() + continue + } + + return nil + } +} diff --git a/client/x/evmengine/keeper/hooks.go b/client/x/evmengine/keeper/hooks.go new file mode 100644 index 00000000..83b9a049 --- /dev/null +++ b/client/x/evmengine/keeper/hooks.go @@ -0,0 +1,75 @@ +package keeper + +import ( + "context" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/slashing/types" + + "github.com/piplabs/story/lib/log" +) + +var _ types.StakingHooks = Hooks{} + +// Hooks implements the staking hooks. It just logs at this point. +type Hooks struct{} + +// AfterValidatorBonded updates the signing info start height or create a new signing info. +func (Hooks) AfterValidatorBonded(ctx context.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Validator bonded", "cons_addr", consAddr, "val_addr", valAddr) + return nil +} + +// AfterValidatorRemoved deletes the address-pubkey relation when a validator is removed,. +func (Hooks) AfterValidatorRemoved(ctx context.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Validator removed", "cons_addr", consAddr, "val_addr", valAddr) + return nil +} + +// AfterValidatorCreated adds the address-pubkey relation when a validator is created. +func (Hooks) AfterValidatorCreated(ctx context.Context, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Validator created", "val_addr", valAddr) + return nil +} + +func (Hooks) AfterValidatorBeginUnbonding(ctx context.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Validator begin unbonding", "cons_addr", consAddr, "val_addr", valAddr) + return nil +} + +func (Hooks) BeforeValidatorModified(ctx context.Context, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Validator modified", "val_addr", valAddr) + return nil +} + +func (Hooks) BeforeDelegationCreated(ctx context.Context, accAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Delegation created", "acc_addr", accAddr, "val_addr", valAddr) + return nil +} + +func (Hooks) BeforeDelegationSharesModified(ctx context.Context, accAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Delegation shares modified", "acc_addr", accAddr, "val_addr", valAddr) + return nil +} + +func (Hooks) BeforeDelegationRemoved(ctx context.Context, accAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Delegation removed", "acc_addr", accAddr, "val_addr", valAddr) + return nil +} + +func (Hooks) AfterDelegationModified(ctx context.Context, accAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + log.Debug(ctx, "šŸ“š Delegation modified", "acc_addr", accAddr, "val_addr", valAddr) + return nil +} + +func (Hooks) BeforeValidatorSlashed(ctx context.Context, valAddr sdk.ValAddress, amount sdkmath.LegacyDec) error { + log.Debug(ctx, "šŸ“š Validator slashed", "val_addr", valAddr, "amount", amount) + return nil +} + +func (Hooks) AfterUnbondingInitiated(ctx context.Context, id uint64) error { + log.Debug(ctx, "šŸ“š Unbonding initiated", "id", id) + return nil +} diff --git a/client/x/evmengine/keeper/keeper.go b/client/x/evmengine/keeper/keeper.go new file mode 100644 index 00000000..1737f7f0 --- /dev/null +++ b/client/x/evmengine/keeper/keeper.go @@ -0,0 +1,213 @@ +package keeper + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "time" + + ormv1alpha1 "cosmossdk.io/api/cosmos/orm/v1alpha1" + "cosmossdk.io/core/store" + "cosmossdk.io/orm/model/ormdb" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + grpc1 "github.com/cosmos/gogoproto/grpc" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/client/comet" + "github.com/piplabs/story/client/genutil/evm/predeploys" + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/k1util" +) + +type Keeper struct { + cdc codec.BinaryCodec + storeService store.KVStoreService + headTable ExecutionHeadTable + engineCl ethclient.EngineClient + txConfig client.TxConfig + cmtAPI comet.API + buildDelay time.Duration + buildOptimistic bool + validatorAddr common.Address + + accountKeeper types.AccountKeeper + evmstakingKeeper types.EvmStakingKeeper + upgradeKeeper types.UpgradeKeeper + + upgradeContract *bindings.UpgradeEntrypoint + + // mutablePayload contains the previous optimistically triggered payload. + // It is optimistic because the validator set can change, + // so we might not actually be the next proposer. + mutablePayload struct { + sync.Mutex + ID *engine.PayloadID + Height uint64 + UpdatedAt time.Time + } +} + +func NewKeeper( + cdc codec.BinaryCodec, + storeService store.KVStoreService, + engineCl ethclient.EngineClient, + ethCl ethclient.Client, + txConfig client.TxConfig, + ak types.AccountKeeper, + esk types.EvmStakingKeeper, + uk types.UpgradeKeeper, +) (*Keeper, error) { + schema := &ormv1alpha1.ModuleSchemaDescriptor{SchemaFile: []*ormv1alpha1.ModuleSchemaDescriptor_FileEntry{ + {Id: 1, ProtoFileName: File_client_x_evmengine_keeper_evmengine_proto.Path()}, + }} + + modDB, err := ormdb.NewModuleDB(schema, ormdb.ModuleDBOptions{KVStoreService: storeService}) + if err != nil { + return nil, errors.Wrap(err, "create module db") + } + + dbStore, err := NewEvmengineStore(modDB) + if err != nil { + return nil, errors.Wrap(err, "create evmengine store") + } + + upgradeContract, err := bindings.NewUpgradeEntrypoint(common.HexToAddress(predeploys.UpgradeEntrypoint), ethCl) + if err != nil { + panic(fmt.Sprintf("failed to bind to the UpgradeEntrypoint contract: %s", err)) + } + + return &Keeper{ + cdc: cdc, + storeService: storeService, + headTable: dbStore.ExecutionHeadTable(), + engineCl: engineCl, + txConfig: txConfig, + accountKeeper: ak, + evmstakingKeeper: esk, + upgradeKeeper: uk, + upgradeContract: upgradeContract, + }, nil +} + +// SetCometAPI sets the comet API client. +func (k *Keeper) SetCometAPI(c comet.API) { + k.cmtAPI = c +} + +// SetBuildDelay sets the build delay parameter. +func (k *Keeper) SetBuildDelay(d time.Duration) { + k.buildDelay = d +} + +// SetBuildOptimistic sets the optimistic build parameter. +func (k *Keeper) SetBuildOptimistic(b bool) { + k.buildOptimistic = b +} + +// SetValidatorAddress sets the validator address. +func (k *Keeper) SetValidatorAddress(addr common.Address) { + k.validatorAddr = addr +} + +// RegisterProposalService registers the proposal service on the provided router. +// This implements abci.ProcessProposal verification of new proposals. +func (k *Keeper) RegisterProposalService(server grpc1.Server) { + types.RegisterMsgServiceServer(server, NewProposalServer(k)) +} + +// parseAndVerifyProposedPayload parses and returns the proposed payload +// if comparing it against the latest execution block succeeds. +// + +func (k *Keeper) parseAndVerifyProposedPayload(ctx context.Context, msg *types.MsgExecutionPayload) (engine.ExecutableData, error) { + // Parse the payload. + var payload engine.ExecutableData + if err := json.Unmarshal(msg.ExecutionPayload, &payload); err != nil { + return engine.ExecutableData{}, errors.Wrap(err, "unmarshal payload") + } + + // Fetch the latest execution head from the local keeper DB. + head, err := k.getExecutionHead(ctx) + if err != nil { + return engine.ExecutableData{}, errors.Wrap(err, "latest execution block") + } + + // Ensure the parent hash and block height matches + if payload.Number != head.GetBlockHeight()+1 { + return engine.ExecutableData{}, errors.New("invalid proposed payload number", "proposed", payload.Number, "head", head.GetBlockHeight()) + } else if payload.ParentHash != head.Hash() { + return engine.ExecutableData{}, errors.New("invalid proposed payload parent hash", "proposed", payload.ParentHash, "head", head.Hash()) + } + + // Ensure the payload timestamp is after latest execution block and before or equaled to the current consensus block. + minTimestamp := head.GetBlockTime() + 1 + maxTimestamp := uint64(sdk.UnwrapSDKContext(ctx).BlockTime().Unix()) + if maxTimestamp < minTimestamp { // Execution block minimum takes precedence + maxTimestamp = minTimestamp + } + if payload.Timestamp < minTimestamp || payload.Timestamp > maxTimestamp { + return engine.ExecutableData{}, errors.New("invalid payload timestamp", + "proposed", payload.Timestamp, "min", minTimestamp, "max", maxTimestamp, + ) + } + + // Ensure the Randao Digest is equaled to parent hash as this is our workaround at this point. + if payload.Random != head.Hash() { + return engine.ExecutableData{}, errors.New("invalid payload random", "proposed", payload.Random, "latest", head.Hash()) + } + + return payload, nil +} + +// isNextProposer returns true if the local node is the proposer +// for the next block. It also returns the next block height. +// +// Note that the validator set can change, so this is an optimistic check. +func (k *Keeper) isNextProposer(ctx context.Context, currentProposer []byte, currentHeight int64) (bool, error) { + valset, ok, err := k.cmtAPI.Validators(ctx, currentHeight) + if err != nil { + return false, err + } else if !ok { + return false, errors.New("validators not available") + } + + idx, _ := valset.GetByAddress(currentProposer) + if idx < 0 { + return false, errors.New("proposer not in validator set") + } + + nextIdx := int(idx+1) % len(valset.Validators) + nextProposer := valset.Validators[nextIdx] + nextAddr, err := k1util.PubKeyToAddress(nextProposer.PubKey) + if err != nil { + return false, err + } + + isNextProposer := nextAddr == k.validatorAddr + + return isNextProposer, nil +} + +func (k *Keeper) setOptimisticPayload(id *engine.PayloadID, height uint64) { + k.mutablePayload.Lock() + defer k.mutablePayload.Unlock() + + k.mutablePayload.ID = id + k.mutablePayload.Height = height + k.mutablePayload.UpdatedAt = time.Now() +} + +func (k *Keeper) getOptimisticPayload() (*engine.PayloadID, uint64, time.Time) { + k.mutablePayload.Lock() + defer k.mutablePayload.Unlock() + + return k.mutablePayload.ID, k.mutablePayload.Height, k.mutablePayload.UpdatedAt +} diff --git a/client/x/evmengine/keeper/keeper_internal_test.go b/client/x/evmengine/keeper/keeper_internal_test.go new file mode 100644 index 00000000..e06875a6 --- /dev/null +++ b/client/x/evmengine/keeper/keeper_internal_test.go @@ -0,0 +1,222 @@ +package keeper + +import ( + "context" + "testing" + "time" + + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/client/comet" + moduletestutil "github.com/piplabs/story/client/x/evmengine/testutil" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient/mock" + "github.com/piplabs/story/lib/k1util" + + "go.uber.org/mock/gomock" +) + +func TestKeeper_isNextProposer(t *testing.T) { + t.Parallel() + type args struct { + height int64 + validatorsFunc func(context.Context, int64) (*cmttypes.ValidatorSet, bool, error) + current int + next int + header func(height int64, address []byte) cmtproto.Header + } + height := int64(1) + tests := []struct { + name string + args args + want bool + wantHeight uint64 + wantErr bool + }{ + { + name: "is next proposer", + args: args{ + height: height, + current: 0, + next: 1, + header: func(height int64, address []byte) cmtproto.Header { + return cmtproto.Header{Height: height, ProposerAddress: address} + }, + }, + want: true, + wantHeight: 2, + wantErr: false, + }, + { + name: "proposer false", + args: args{ + height: height, + current: 0, + next: 2, + header: func(height int64, address []byte) cmtproto.Header { + return cmtproto.Header{Height: height, ProposerAddress: address} + }, + }, + want: false, + wantHeight: 2, + wantErr: false, + }, + { + name: "validatorsFunc error", + args: args{ + height: height, + current: 0, + next: 1, + validatorsFunc: func(ctx context.Context, i int64) (*cmttypes.ValidatorSet, bool, error) { + return nil, false, errors.New("error") + }, + header: func(height int64, address []byte) cmtproto.Header { + return cmtproto.Header{Height: height, ProposerAddress: address} + }, + }, + want: false, + wantErr: true, + }, + { + name: "validatorsFunc not ok", + args: args{ + height: height, + current: 0, + next: 1, + validatorsFunc: func(ctx context.Context, i int64) (*cmttypes.ValidatorSet, bool, error) { + return nil, false, nil + }, + header: func(height int64, address []byte) cmtproto.Header { + return cmtproto.Header{Height: height, ProposerAddress: address} + }, + }, + want: false, + wantErr: true, + }, + { + name: "invalid val index", + args: args{ + height: height, + current: 0, + next: 1, + + header: func(height int64, address []byte) cmtproto.Header { + return cmtproto.Header{Height: height, ProposerAddress: []byte("invalid")} + }, + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + cdc := getCodec(t) + txConfig := authtx.NewTxConfig(cdc, nil) + mockEngine, err := newMockEngineAPI(0) + require.NoError(t, err) + + cmtAPI := newMockCometAPI(t, tt.args.validatorsFunc) + header := tt.args.header(height, cmtAPI.validatorSet.Validators[tt.args.current].Address) + + nxtAddr, err := k1util.PubKeyToAddress(cmtAPI.validatorSet.Validators[tt.args.next].PubKey) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + mockClient := mock.NewMockClient(ctrl) + ak := moduletestutil.NewMockAccountKeeper(ctrl) + esk := moduletestutil.NewMockEvmStakingKeeper(ctrl) + uk := moduletestutil.NewMockUpgradeKeeper(ctrl) + + ctx, storeService := setupCtxStore(t, &header) + + keeper, err := NewKeeper(cdc, storeService, &mockEngine, mockClient, txConfig, ak, esk, uk) + require.NoError(t, err) + keeper.SetCometAPI(cmtAPI) + keeper.SetValidatorAddress(nxtAddr) + populateGenesisHead(ctx, t, keeper) + + got, err := keeper.isNextProposer(ctx, ctx.BlockHeader().ProposerAddress, ctx.BlockHeader().Height) + if (err != nil) != tt.wantErr { + t.Errorf("isNextProposer() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("isNextProposer() got = %v, want %v", got, tt.want) + } + // make sure that height passed into Validators is correct + require.Equal(t, tt.args.height, cmtAPI.height) + }) + } +} + +var _ comet.API = (*mockCometAPI)(nil) + +type mockCometAPI struct { + comet.API + fuzzer *fuzz.Fuzzer + validatorSet *cmttypes.ValidatorSet + validatorsFunc func(context.Context, int64) (*cmttypes.ValidatorSet, bool, error) + height int64 +} + +func newMockCometAPI(t *testing.T, valFun func(context.Context, int64) (*cmttypes.ValidatorSet, bool, error)) *mockCometAPI { + t.Helper() + fuzzer := newFuzzer(0) + valSet := fuzzValidators(t, fuzzer) + + return &mockCometAPI{ + fuzzer: fuzzer, + validatorSet: valSet, + validatorsFunc: valFun, + } +} + +func fuzzValidators(t *testing.T, fuzzer *fuzz.Fuzzer) *cmttypes.ValidatorSet { + t.Helper() + var validators []*cmttypes.Validator + + fuzzer.NilChance(0).NumElements(3, 7).Fuzz(&validators) + + valSet := new(cmttypes.ValidatorSet) + err := valSet.UpdateWithChangeSet(validators) + require.NoError(t, err) + + return valSet +} + +func (m *mockCometAPI) Validators(ctx context.Context, height int64) (*cmttypes.ValidatorSet, bool, error) { + m.height = height + if m.validatorsFunc != nil { + return m.validatorsFunc(ctx, height) + } + + return m.validatorSet, true, nil +} + +// newFuzzer - create a new custom cmttypes.Validator fuzzer. +func newFuzzer(seed int64) *fuzz.Fuzzer { + if seed == 0 { + seed = time.Now().UnixNano() + } + + f := fuzz.NewWithSeed(seed).NilChance(0) + f.Funcs( + func(v *cmttypes.Validator, c fuzz.Continue) { + privKey := k1.GenPrivKey() + v.PubKey = privKey.PubKey() + v.VotingPower = 200 + val := cmttypes.NewValidator(v.PubKey, v.VotingPower) + + *v = *val + }, + ) + + return f +} diff --git a/client/x/evmengine/keeper/msg_server.go b/client/x/evmengine/keeper/msg_server.go new file mode 100644 index 00000000..01591507 --- /dev/null +++ b/client/x/evmengine/keeper/msg_server.go @@ -0,0 +1,205 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + etypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/log" +) + +type msgServer struct { + *Keeper + types.UnimplementedMsgServiceServer +} + +// NewMsgServerImpl returns an implementation of the MsgServer interface for the provided Keeper. +func NewMsgServerImpl(keeper *Keeper) types.MsgServiceServer { + return &msgServer{Keeper: keeper} +} + +// ExecutionPayload handles a new execution payload included in the current finalized block. +// This is called as part of FinalizeBlock ABCI++ method. +func (s msgServer) ExecutionPayload(ctx context.Context, msg *types.MsgExecutionPayload) (*types.ExecutionPayloadResponse, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + if sdkCtx.ExecMode() != sdk.ExecModeFinalize { + return nil, errors.New("only allowed in finalize mode") + } + + payload, err := s.parseAndVerifyProposedPayload(ctx, msg) + if err != nil { + return nil, err + } + + // TODO: should we compare and reject in a finalized block? + //// Ensure that the withdrawals in the payload are from the front indices of the queue. + // if err := s.compareWithdrawals(ctx, payload.Withdrawals); err != nil { + // return nil, errors.Wrap(err, "compare local and received withdrawals") + //} + + // TODO: We dequeue with assumption that the top items of the queue are the ones that are processed in the block. + // TODO: We might need to check that the withdrawals in the finalized block are the same as the ones dequeued. + // But there's no way to reject the block at this point, so we can only log an error. + log.Debug(ctx, "Dequeueing eligible withdrawals [BEFORE]", "len", len(payload.Withdrawals)) + if _, err := s.evmstakingKeeper.DequeueEligibleWithdrawals(ctx); err != nil { + return nil, errors.Wrap(err, "error on withdrawals dequeue") + } + log.Debug(ctx, "Dequeueing eligible withdrawals [AFTER]", "len", len(payload.Withdrawals)) + + err = retryForever(ctx, func(ctx context.Context) (bool, error) { + status, err := pushPayload(ctx, s.engineCl, payload) + if err != nil || isUnknown(status) { + // We need to retry forever on networking errors, but can't easily identify them, so retry all errors. + log.Warn(ctx, "Processing finalized payload failed: push new payload to evm (will retry)", err, + "status", status.Status) + + return false, nil // Retry + } else if invalid, err := isInvalid(status); invalid { + // This should never happen. This node will stall now. + log.Error(ctx, "Processing finalized payload failed; payload invalid [BUG]", err) + + return false, err // Don't retry, error out. + } else if isSyncing(status) { + log.Warn(ctx, "Processing finalized payload; evm syncing", nil) + } + + return true, nil // We are done, don't retry + }) + if err != nil { + return nil, err + } + + // CometBFT has instant finality, so head/safe/finalized is latest height. + fcs := engine.ForkchoiceStateV1{ + HeadBlockHash: payload.BlockHash, + SafeBlockHash: payload.BlockHash, + FinalizedBlockHash: payload.BlockHash, + } + + err = retryForever(ctx, func(ctx context.Context) (bool, error) { + fcr, err := s.engineCl.ForkchoiceUpdatedV3(ctx, fcs, nil) + if err != nil || isUnknown(fcr.PayloadStatus) { + // We need to retry forever on networking errors, but can't easily identify them, so retry all errors. + log.Warn(ctx, "Processing finalized payload failed: evm fork choice update (will retry)", err, + "status", fcr.PayloadStatus.Status) + + return false, nil // Retry + } else if isSyncing(fcr.PayloadStatus) { + log.Warn(ctx, "Processing finalized payload halted while evm syncing (will retry)", nil, "payload_height", payload.Number) + + return false, nil // Retry + } else if invalid, err := isInvalid(fcr.PayloadStatus); invalid { + // This should never happen. This node will stall now. + log.Error(ctx, "Processing finalized payload failed; forkchoice update invalid [BUG]", err, + "payload_height", payload.Number) + + return false, err // Don't retry + } + + return true, nil + }) + if err != nil { + return nil, err + } + + // Deliver all the previous payload log events + if err := s.evmstakingKeeper.ProcessStakingEvents(ctx, payload.Number-1, msg.PrevPayloadEvents); err != nil { + return nil, errors.Wrap(err, "deliver staking-related event logs") + } + if err := s.ProcessUpgradeEvents(ctx, payload.Number-1, msg.PrevPayloadEvents); err != nil { + return nil, errors.Wrap(err, "deliver upgrade-related event logs") + } + + if err := s.updateExecutionHead(ctx, payload); err != nil { + return nil, errors.Wrap(err, "update execution head") + } + + return &types.ExecutionPayloadResponse{}, nil +} + +//nolint:unused // compareWithdrawals compares the given actual withdrawals with the expected withdrawals from the queue. +func (s msgServer) compareWithdrawals(ctx context.Context, actualWithdrawals etypes.Withdrawals) error { + expectedWithdrawals, err := s.evmstakingKeeper.PeekEligibleWithdrawals(ctx) + if err != nil { + return errors.Wrap(err, "peek withdrawals") + } + + if len(actualWithdrawals) != len(expectedWithdrawals) { + return errors.New("invalid withdrawals length") + } + + for i, withdrawal := range actualWithdrawals { + if withdrawal.Index != expectedWithdrawals[i].Index { + return errors.New("invalid withdrawal index") + } + // skip the Validator index equality check (always 0) + if withdrawal.Address != expectedWithdrawals[i].Address { + return errors.New("invalid withdrawal address") + } + if withdrawal.Amount != expectedWithdrawals[i].Amount { + return errors.New("invalid withdrawal amount") + } + } + + return nil +} + +// pushPayload pushes the given Engine API payload to EL and returns the engine payload status or an error. +func pushPayload(ctx context.Context, engineCl ethclient.EngineClient, payload engine.ExecutableData) (engine.PayloadStatusV1, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + appHash := common.BytesToHash(sdkCtx.BlockHeader().AppHash) + if appHash == (common.Hash{}) { + return engine.PayloadStatusV1{}, errors.New("app hash is empty") + } + + emptyVersionHashes := make([]common.Hash, 0) // Cannot use nil. + + // Push it back to the execution client (mark it as possible new head). + status, err := engineCl.NewPayloadV3(ctx, payload, emptyVersionHashes, &appHash) + if err != nil { + return engine.PayloadStatusV1{}, errors.Wrap(err, "new payload") + } + + return status, nil +} + +var _ types.MsgServiceServer = msgServer{} + +func isUnknown(status engine.PayloadStatusV1) bool { + if status.Status == engine.VALID || + status.Status == engine.INVALID || + status.Status == engine.SYNCING || + status.Status == engine.ACCEPTED { + return false + } + + return true +} + +func isSyncing(status engine.PayloadStatusV1) bool { + return status.Status == engine.SYNCING || status.Status == engine.ACCEPTED +} + +func isInvalid(status engine.PayloadStatusV1) (bool, error) { + if status.Status != engine.INVALID { + return false, nil + } + + valErr := "nil" + if status.ValidationError != nil { + valErr = *status.ValidationError + } + + hash := "nil" + if status.LatestValidHash != nil { + hash = status.LatestValidHash.Hex() + } + + return true, errors.New("payload invalid", "validation_err", valErr, "last_valid_hash", hash) +} diff --git a/client/x/evmengine/keeper/msg_server_internal_test.go b/client/x/evmengine/keeper/msg_server_internal_test.go new file mode 100644 index 00000000..ab0206f4 --- /dev/null +++ b/client/x/evmengine/keeper/msg_server_internal_test.go @@ -0,0 +1,266 @@ +package keeper + +import ( + "context" + "encoding/json" + "reflect" + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + etypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + moduletestutil "github.com/piplabs/story/client/x/evmengine/testutil" + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/ethclient/mock" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/tutil" + + "go.uber.org/mock/gomock" +) + +func Test_msgServer_ExecutionPayload(t *testing.T) { + t.Parallel() + fastBackoffForT() + + cdc := getCodec(t) + txConfig := authtx.NewTxConfig(cdc, nil) + + mockEngine, err := newMockEngineAPI(2) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + mockClient := mock.NewMockClient(ctrl) + ak := moduletestutil.NewMockAccountKeeper(ctrl) + esk := moduletestutil.NewMockEvmStakingKeeper(ctrl) + uk := moduletestutil.NewMockUpgradeKeeper(ctrl) + + // Expected call for PeekEligibleWithdrawals + esk.EXPECT().DequeueEligibleWithdrawals(gomock.Any()).Return(nil, nil).AnyTimes() + esk.EXPECT().ProcessStakingEvents(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + + cmtAPI := newMockCometAPI(t, nil) + // set the header and proposer so we have the correct next proposer + header := cmtproto.Header{Height: 1, AppHash: tutil.RandomHash().Bytes()} + header.ProposerAddress = cmtAPI.validatorSet.Validators[0].Address + nxtAddr, err := k1util.PubKeyToAddress(cmtAPI.validatorSet.Validators[1].PubKey) + require.NoError(t, err) + + ctx, storeService := setupCtxStore(t, &header) + ctx = ctx.WithExecMode(sdk.ExecModeFinalize) + + evmLogProc := mockLogProvider{deliverErr: errors.New("test error")} + keeper, err := NewKeeper(cdc, storeService, &mockEngine, mockClient, txConfig, ak, esk, uk) + require.NoError(t, err) + keeper.SetCometAPI(cmtAPI) + keeper.SetValidatorAddress(nxtAddr) + populateGenesisHead(ctx, t, keeper) + + msgSrv := NewMsgServerImpl(keeper) + + var payloadData []byte + var payloadID engine.PayloadID + var latestHeight uint64 + var block *etypes.Block + newPayload := func(ctx context.Context) { + // get latest block to build on top + latestBlock, err := mockEngine.HeaderByType(ctx, ethclient.HeadLatest) + require.NoError(t, err) + latestHeight := latestBlock.Number.Uint64() + + sdkCtx := sdk.UnwrapSDKContext(ctx) + appHash := common.BytesToHash(sdkCtx.BlockHeader().AppHash) + + b, execPayload := mockEngine.nextBlock(t, latestHeight+1, uint64(time.Now().Unix()), latestBlock.Hash(), keeper.validatorAddr, &appHash) + block = b + + payloadID, err = ethclient.MockPayloadID(execPayload, &appHash) + require.NoError(t, err) + + // Create execution payload message + payloadData, err = json.Marshal(execPayload) + require.NoError(t, err) + } + + assertExecutionPayload := func(ctx context.Context) { + events, err := evmLogProc.Prepare(ctx, block.Hash()) + require.NoError(t, err) + + resp, err := msgSrv.ExecutionPayload(ctx, &types.MsgExecutionPayload{ + Authority: authtypes.NewModuleAddress(types.ModuleName).String(), + ExecutionPayload: payloadData, + PrevPayloadEvents: events, + }) + require.NoError(t, err) + require.NotNil(t, resp) + + gotPayload, err := mockEngine.GetPayloadV3(ctx, payloadID) + require.NoError(t, err) + // make sure height is increasing in engine, blocks being built + require.Equal(t, gotPayload.ExecutionPayload.Number, latestHeight+1) + require.Equal(t, gotPayload.ExecutionPayload.BlockHash, block.Hash()) + require.Equal(t, gotPayload.ExecutionPayload.FeeRecipient, keeper.validatorAddr) + require.Empty(t, gotPayload.ExecutionPayload.Withdrawals) + } + + newPayload(ctx) + assertExecutionPayload(ctx) + + // now lets run optimistic flow + // ctx = ctx.WithBlockTime(ctx.BlockTime().Add(time.Second)) + // newPayload(ctx) + // keeper.SetBuildOptimistic(true) + // assertExecutionPayload(ctx) +} + +// populateGenesisHead inserts the mock genesis execution head into the database. +func populateGenesisHead(ctx context.Context, t *testing.T, keeper *Keeper) { + t.Helper() + genesisBlock, err := ethclient.MockGenesisBlock() + require.NoError(t, err) + + require.NoError(t, keeper.InsertGenesisHead(ctx, genesisBlock.Hash().Bytes())) +} + +func Test_pushPayload(t *testing.T) { + t.Parallel() + + newPayload := func(ctx context.Context, mockEngine mockEngineAPI, address common.Address) (engine.ExecutableData, engine.PayloadID) { + // get latest block to build on top + latestBlock, err := mockEngine.HeaderByType(ctx, ethclient.HeadLatest) + require.NoError(t, err) + latestHeight := latestBlock.Number.Uint64() + + sdkCtx := sdk.UnwrapSDKContext(ctx) + appHash := common.BytesToHash(sdkCtx.BlockHeader().AppHash) + + _, execPayload := mockEngine.nextBlock(t, latestHeight+1, uint64(time.Now().Unix()), latestBlock.Hash(), address, &appHash) + payloadID, err := ethclient.MockPayloadID(execPayload, &appHash) + require.NoError(t, err) + + return execPayload, payloadID + } + type args struct { + transformPayload func(*engine.ExecutableData) + newPayloadV3Func func(context.Context, engine.ExecutableData, []common.Hash, *common.Hash) (engine.PayloadStatusV1, error) + } + tests := []struct { + name string + args args + wantErr bool + wantStatus string + }{ + { + name: "new payload error", + args: args{ + newPayloadV3Func: func(context.Context, engine.ExecutableData, []common.Hash, *common.Hash) (engine.PayloadStatusV1, error) { + return engine.PayloadStatusV1{}, errors.New("error") + }, + }, + wantErr: true, + wantStatus: "", + }, + { + name: "new payload invalid", + args: args{ + newPayloadV3Func: func(context.Context, engine.ExecutableData, []common.Hash, *common.Hash) (engine.PayloadStatusV1, error) { + return engine.PayloadStatusV1{ + Status: engine.INVALID, + LatestValidHash: nil, + ValidationError: nil, + }, nil + }, + }, + wantErr: false, + wantStatus: engine.INVALID, + }, + { + name: "new payload invalid val err", + args: args{ + newPayloadV3Func: func(context.Context, engine.ExecutableData, []common.Hash, *common.Hash) (engine.PayloadStatusV1, error) { + return engine.PayloadStatusV1{ + Status: engine.INVALID, + LatestValidHash: nil, + ValidationError: func() *string { s := "error"; return &s }(), + }, nil + }, + }, + wantErr: false, + wantStatus: engine.INVALID, + }, + { + name: "new payload syncing", + args: args{ + newPayloadV3Func: func(context.Context, engine.ExecutableData, []common.Hash, *common.Hash) (engine.PayloadStatusV1, error) { + return engine.PayloadStatusV1{ + Status: engine.SYNCING, + LatestValidHash: nil, + ValidationError: nil, + }, nil + }, + }, + wantErr: false, + wantStatus: engine.SYNCING, + }, + { + name: "new payload accepted", + args: args{ + newPayloadV3Func: func(context.Context, engine.ExecutableData, []common.Hash, *common.Hash) (engine.PayloadStatusV1, error) { + return engine.PayloadStatusV1{ + Status: engine.ACCEPTED, + LatestValidHash: nil, + ValidationError: nil, + }, nil + }, + }, + wantErr: false, + wantStatus: engine.ACCEPTED, + }, + { + name: "valid payload", + args: args{}, + wantErr: false, + wantStatus: engine.VALID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + appHash := tutil.RandomHash() + ctx := ctxWithAppHash(t, appHash) + + mockEngine, err := newMockEngineAPI(0) + require.NoError(t, err) + + mockEngine.newPayloadV3Func = tt.args.newPayloadV3Func + payload, payloadID := newPayload(ctx, mockEngine, common.Address{}) + if tt.args.transformPayload != nil { + tt.args.transformPayload(&payload) + } + + status, err := pushPayload(ctx, &mockEngine, payload) + if (err != nil) != tt.wantErr { + t.Errorf("pushPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + require.Equal(t, tt.wantStatus, status.Status) + + if status.Status == engine.VALID { + want, err := mockEngine.GetPayloadV3(ctx, payloadID) + require.NoError(t, err) + if !reflect.DeepEqual(payload, *want.ExecutionPayload) { + t.Errorf("pushPayload() got = %v, want %v", payload, want) + } + } + }) + } +} diff --git a/client/x/evmengine/keeper/params.go b/client/x/evmengine/keeper/params.go new file mode 100644 index 00000000..3a42d4f0 --- /dev/null +++ b/client/x/evmengine/keeper/params.go @@ -0,0 +1,53 @@ +package keeper + +import ( + "context" + + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" +) + +// ExecutionBlockHash returns the genesis execution block hash. +func (k *Keeper) ExecutionBlockHash(ctx context.Context) ([]byte, error) { + params, err := k.GetParams(ctx) + if err != nil { + return nil, err + } + + return params.ExecutionBlockHash, nil +} + +// This method performs no validation of the parameters. +func (k *Keeper) SetParams(ctx context.Context, params types.Params) error { + store := k.storeService.OpenKVStore(ctx) + bz, err := k.cdc.Marshal(¶ms) + if err != nil { + return errors.Wrap(err, "marshal params") + } + + err = store.Set(types.ParamsKey, bz) + if err != nil { + return errors.Wrap(err, "set params") + } + + return nil +} + +func (k *Keeper) GetParams(ctx context.Context) (params types.Params, err error) { + store := k.storeService.OpenKVStore(ctx) + bz, err := store.Get(types.ParamsKey) + if err != nil { + return params, errors.Wrap(err, "get params") + } + + if bz == nil { + return params, nil + } + + err = k.cdc.Unmarshal(bz, ¶ms) + if err != nil { + return params, errors.Wrap(err, "unmarshal params") + } + + return params, nil +} diff --git a/client/x/evmengine/keeper/proposal_server.go b/client/x/evmengine/keeper/proposal_server.go new file mode 100644 index 00000000..2f758c09 --- /dev/null +++ b/client/x/evmengine/keeper/proposal_server.go @@ -0,0 +1,120 @@ +package keeper + +import ( + "context" + + "github.com/cosmos/gogoproto/proto" + etypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +type proposalServer struct { + *Keeper + types.UnimplementedMsgServiceServer +} + +// ExecutionPayload handles a new execution payload proposed in a block. +func (s proposalServer) ExecutionPayload(ctx context.Context, msg *types.MsgExecutionPayload) (*types.ExecutionPayloadResponse, error) { + payload, err := s.parseAndVerifyProposedPayload(ctx, msg) + if err != nil { + return nil, errors.Wrap(err, "unmarshal payload") + } + + // Ensure that the withdrawals in the payload are from the front indices of the queue. + if err := s.compareWithdrawals(ctx, payload.Withdrawals); err != nil { + return nil, errors.Wrap(err, "compare local and received withdrawals") + } + + // Push the payload to the EVM. + err = retryForever(ctx, func(ctx context.Context) (bool, error) { + status, err := pushPayload(ctx, s.engineCl, payload) + if err != nil || isUnknown(status) { + // We need to retry forever on networking errors, but can't easily identify them, so retry all errors. + log.Warn(ctx, "Verifying proposal failed: push new payload to evm (will retry)", err, + "status", status.Status) + + return false, nil // Retry + } else if invalid, err := isInvalid(status); invalid { + return false, errors.Wrap(err, "invalid payload, rejecting proposal") // Don't retry + } else if isSyncing(status) { + // If this is initial sync, we need to continue and set a target head to sync to, so don't retry. + log.Warn(ctx, "Can't properly verifying proposal: evm syncing", err, + "payload_height", payload.Number) + } + + return true, nil // We are done, don't retry. + }) + if err != nil { + return nil, err + } + + // Collect local view of the evm logs from the previous payload. + evmEvents, err := s.evmEvents(ctx, payload.ParentHash) + if err != nil { + return nil, errors.Wrap(err, "prepare evm event logs") + } + + // Ensure the proposed evm event logs are equal to the local view. + if err := evmEventsEqual(evmEvents, msg.PrevPayloadEvents); err != nil { + return nil, errors.Wrap(err, "verify prev payload events") + } + + return &types.ExecutionPayloadResponse{}, nil +} + +// NewProposalServer returns an implementation of the MsgServer interface +// for the provided Keeper. +func NewProposalServer(keeper *Keeper) types.MsgServiceServer { + return &proposalServer{Keeper: keeper} +} + +var _ types.MsgServiceServer = proposalServer{} + +// TODO: benchmark this function, might be adding to overhead. Esp. the for loop. If so, parallelize since array checks are independent. +func evmEventsEqual(a, b []*types.EVMEvent) error { + if len(a) != len(b) { + return errors.New("count mismatch") + } + + for i := range a { + if !proto.Equal(a[i], b[i]) { + return errors.New("log mismatch", "index", i) + } + } + + return nil +} + +// compareWithdrawals compares the local peek and received withdrawals. +func (s proposalServer) compareWithdrawals(ctx context.Context, actualWithdrawals etypes.Withdrawals) error { + expectedWithdrawals, err := s.evmstakingKeeper.PeekEligibleWithdrawals(ctx) + if err != nil { + return errors.Wrap(err, "peek withdrawals") + } + + log.Debug(ctx, "Comparing local and received withdrawals", + "local", len(expectedWithdrawals), + "received", len(actualWithdrawals), + ) + if len(actualWithdrawals) != len(expectedWithdrawals) { + return errors.New("invalid withdrawals length") + } + + for i, withdrawal := range actualWithdrawals { + if withdrawal.Index != expectedWithdrawals[i].Index { + return errors.New("invalid withdrawal index") + } + // skip the Validator index equality check (always 0) + if withdrawal.Address != expectedWithdrawals[i].Address { + return errors.New("invalid withdrawal address") + } + if withdrawal.Amount != expectedWithdrawals[i].Amount { + return errors.New("invalid withdrawal amount") + } + } + + return nil +} diff --git a/client/x/evmengine/keeper/proposal_server_internal_test.go b/client/x/evmengine/keeper/proposal_server_internal_test.go new file mode 100644 index 00000000..cf093770 --- /dev/null +++ b/client/x/evmengine/keeper/proposal_server_internal_test.go @@ -0,0 +1,110 @@ +package keeper + +import ( + "context" + "encoding/json" + "testing" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + etypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + moduletestutil "github.com/piplabs/story/client/x/evmengine/testutil" + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/ethclient/mock" + "github.com/piplabs/story/lib/expbackoff" + "github.com/piplabs/story/lib/tutil" + + "go.uber.org/mock/gomock" +) + +func Test_proposalServer_ExecutionPayload(t *testing.T) { + t.Parallel() + cdc := getCodec(t) + txConfig := authtx.NewTxConfig(cdc, nil) + + mockEngine, err := newMockEngineAPI(0) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + mockClient := mock.NewMockClient(ctrl) + ak := moduletestutil.NewMockAccountKeeper(ctrl) + esk := moduletestutil.NewMockEvmStakingKeeper(ctrl) + uk := moduletestutil.NewMockUpgradeKeeper(ctrl) + + esk.EXPECT().PeekEligibleWithdrawals(gomock.Any()).Return(nil, nil).AnyTimes() + + sdkCtx, storeService := setupCtxStore(t, &cmtproto.Header{AppHash: tutil.RandomHash().Bytes()}) + sdkCtx = sdkCtx.WithExecMode(sdk.ExecModeFinalize) + + keeper, err := NewKeeper(cdc, storeService, &mockEngine, mockClient, txConfig, ak, esk, uk) + require.NoError(t, err) + populateGenesisHead(sdkCtx, t, keeper) + propSrv := NewProposalServer(keeper) + + keeper.SetValidatorAddress(common.BytesToAddress([]byte("test"))) + + var payloadData []byte + var payloadID engine.PayloadID + var latestHeight uint64 + var block *etypes.Block + newPayload := func(ctx context.Context) { + // get latest block to build on top + latestBlock, err := mockEngine.HeaderByType(ctx, ethclient.HeadLatest) + require.NoError(t, err) + latestHeight = latestBlock.Number.Uint64() + + sdkCtx := sdk.UnwrapSDKContext(ctx) + appHash := common.BytesToHash(sdkCtx.BlockHeader().AppHash) + + b, execPayload := mockEngine.nextBlock( + t, + latestHeight+1, + uint64(sdkCtx.BlockHeader().Time.Unix()), + latestBlock.Hash(), + keeper.validatorAddr, + &appHash, + ) + block = b + + payloadID, err = ethclient.MockPayloadID(execPayload, &appHash) + require.NoError(t, err) + + // Create execution payload message + payloadData, err = json.Marshal(execPayload) + require.NoError(t, err) + } + + assertExecutionPayload := func(ctx context.Context) { + resp, err := propSrv.ExecutionPayload(ctx, &types.MsgExecutionPayload{ + Authority: authtypes.NewModuleAddress(types.ModuleName).String(), + ExecutionPayload: payloadData, + }) + require.NoError(t, err) + require.NotNil(t, resp) + + gotPayload, err := mockEngine.GetPayloadV3(ctx, payloadID) + require.NoError(t, err) + require.Equal(t, latestHeight+1, gotPayload.ExecutionPayload.Number) + require.Equal(t, block.Hash(), gotPayload.ExecutionPayload.BlockHash) + require.Equal(t, keeper.validatorAddr, gotPayload.ExecutionPayload.FeeRecipient) + require.Empty(t, gotPayload.ExecutionPayload.Withdrawals) + } + + newPayload(sdkCtx) + assertExecutionPayload(sdkCtx) +} + +func fastBackoffForT() { + backoffFuncMu.Lock() + defer backoffFuncMu.Unlock() + backoffFunc = func(context.Context, ...func(*expbackoff.Config)) func() { + return func() {} + } +} diff --git a/client/x/evmengine/keeper/upgrades.go b/client/x/evmengine/keeper/upgrades.go new file mode 100644 index 00000000..7239b07c --- /dev/null +++ b/client/x/evmengine/keeper/upgrades.go @@ -0,0 +1,52 @@ +package keeper + +import ( + "context" + + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + clog "github.com/piplabs/story/lib/log" +) + +func (k *Keeper) ProcessUpgradeEvents(ctx context.Context, height uint64, logs []*types.EVMEvent) error { + for _, evmLog := range logs { + if err := evmLog.Verify(); err != nil { + return errors.Wrap(err, "verify log [BUG]") // This shouldn't happen + } + ethlog := evmLog.ToEthLog() + + //nolint:gocritic,revive // more cases later + switch ethlog.Topics[0] { + case types.SoftwareUpgradeEvent.ID: + ev, err := k.upgradeContract.ParseSoftwareUpgrade(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse SubmitProposal log", err) + continue + } + if err = k.ProcessSoftwareUpgrade(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process submit proposal", err) + continue + } + } + } + + clog.Debug(ctx, "Processed governance events", "height", height, "count", len(logs)) + + return nil +} + +func (k *Keeper) ProcessSoftwareUpgrade(ctx context.Context, ev *bindings.UpgradeEntrypointSoftwareUpgrade) error { + err := k.upgradeKeeper.ScheduleUpgrade(ctx, upgradetypes.Plan{ + Name: ev.Name, + Info: ev.Info, + Height: ev.Height, + }) + if err != nil { + return errors.Wrap(err, "process software upgrade: schedule upgrade") + } + + return nil +} diff --git a/client/x/evmengine/module/depinject.go b/client/x/evmengine/module/depinject.go new file mode 100644 index 00000000..25b66a39 --- /dev/null +++ b/client/x/evmengine/module/depinject.go @@ -0,0 +1,74 @@ +package module + +import ( + "cosmossdk.io/core/appmodule" + "cosmossdk.io/core/store" + "cosmossdk.io/depinject" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/piplabs/story/client/x/evmengine/keeper" + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/ethclient" +) + +//nolint:gochecknoinits // Cosmos-style +func init() { + appmodule.Register( + &Module{}, + appmodule.Provide( + ProvideModule, + ), + ) +} + +type ModuleInputs struct { + depinject.In + + StoreService store.KVStoreService + Cdc codec.Codec + Config *Module + TXConfig client.TxConfig + EngineCl ethclient.EngineClient + EthCl ethclient.Client + AccountKeeper types.AccountKeeper + EvmStakingKeeper types.EvmStakingKeeper + UpgradeKeeper types.UpgradeKeeper +} + +type ModuleOutputs struct { + depinject.Out + + EngEVMKeeper *keeper.Keeper + Module appmodule.AppModule + Hooks stakingtypes.StakingHooksWrapper +} + +func ProvideModule(in ModuleInputs) (ModuleOutputs, error) { + k, err := keeper.NewKeeper( + in.Cdc, + in.StoreService, + in.EngineCl, + in.EthCl, + in.TXConfig, + in.AccountKeeper, + in.EvmStakingKeeper, + in.UpgradeKeeper, + ) + if err != nil { + return ModuleOutputs{}, err + } + + m := NewAppModule( + in.Cdc, + k, + ) + + return ModuleOutputs{ + EngEVMKeeper: k, + Module: m, + Hooks: stakingtypes.StakingHooksWrapper{StakingHooks: keeper.Hooks{}}, + }, nil +} diff --git a/client/x/evmengine/module/module.go b/client/x/evmengine/module/module.go new file mode 100644 index 00000000..a19bb828 --- /dev/null +++ b/client/x/evmengine/module/module.go @@ -0,0 +1,125 @@ +package module + +import ( + "encoding/json" + "fmt" + + "cosmossdk.io/core/appmodule" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + + "github.com/piplabs/story/client/x/evmengine/keeper" + "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/lib/errors" +) + +var ( + _ module.AppModuleBasic = AppModule{} + _ module.HasName = AppModule{} + _ module.HasGenesis = AppModule{} + _ module.HasServices = AppModule{} + + _ appmodule.AppModule = AppModule{} +) + +// ConsensusVersion defines the current module consensus version. +const ConsensusVersion = 1 + +// ---------------------------------------------------------------------------- +// AppModuleBasic +// ---------------------------------------------------------------------------- + +// AppModuleBasic implements the AppModuleBasic interface that defines the +// independent methods a Cosmos SDK module needs to implement. +type AppModuleBasic struct { + cdc codec.BinaryCodec +} + +func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { + return AppModuleBasic{cdc: cdc} +} + +// Name returns the name of the module as a string. +func (AppModuleBasic) Name() string { + return types.ModuleName +} + +// RegisterLegacyAminoCodec registers the amino codec for the module, which is used +// to marshal and unmarshal structs to/from []byte in order to persist them in the module's KVStore. +func (AppModuleBasic) RegisterLegacyAminoCodec(*codec.LegacyAmino) {} + +// RegisterInterfaces registers a module's interface types and their concrete implementations as proto.Message. +func (AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) { + types.RegisterInterfaces(reg) +} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(client.Context, *runtime.ServeMux) {} + +// ---------------------------------------------------------------------------- +// AppModule +// ---------------------------------------------------------------------------- + +// AppModule implements the AppModule interface that defines the inter-dependent methods that modules need to implement. +type AppModule struct { + AppModuleBasic + + keeper *keeper.Keeper +} + +func NewAppModule( + cdc codec.Codec, + keeper *keeper.Keeper, +) AppModule { + return AppModule{ + AppModuleBasic: NewAppModuleBasic(cdc), + keeper: keeper, + } +} + +// IsOnePerModuleType implements the depinject.OnePerModuleType interface. +func (AppModule) IsOnePerModuleType() {} + +// IsAppModule implements the appmodule.AppModule interface. +func (AppModule) IsAppModule() {} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion } + +func (AppModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(types.DefaultGenesisState()) +} + +// RegisterServices registers a gRPC query service to respond to the module-specific gRPC queries. +func (am AppModule) RegisterServices(cfg module.Configurator) { + types.RegisterMsgServiceServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) +} + +// ValidateGenesis performs genesis state validation for the bank module. +func (am AppModule) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { + var gs types.GenesisState + if err := cdc.UnmarshalJSON(bz, &gs); err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to unmarshal %s genesis state", types.ModuleName)) + } + + return am.keeper.ValidateGenesis(&gs) +} + +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) { + var gs types.GenesisState + cdc.MustUnmarshalJSON(data, &gs) + + if err := am.keeper.InitGenesis(ctx, &gs); err != nil { + panic(fmt.Sprintf("failed to initialize %s genesis state: %v", types.ModuleName, err)) + } +} + +// ExportGenesis returns the exported genesis state as raw bytes for the module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(am.keeper.ExportGenesis(ctx)) +} diff --git a/client/x/evmengine/module/module.proto b/client/x/evmengine/module/module.proto new file mode 100644 index 00000000..ab7b6477 --- /dev/null +++ b/client/x/evmengine/module/module.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package client.x.evmengine.module; + +import "cosmos/app/v1alpha1/module.proto"; + +option go_package = "client/x/evmengine/module"; + +// Module is the config object for the module. +message Module { + option (cosmos.app.v1alpha1.module) = { + go_import: "github.com/piplabs/story/client/x/evmengine" + }; + + // authority defines the custom module authority. If not set, defaults to the governance module. + string authority = 1; +} \ No newline at end of file diff --git a/client/x/evmengine/module/module.pulsar.go b/client/x/evmengine/module/module.pulsar.go new file mode 100644 index 00000000..14221dad --- /dev/null +++ b/client/x/evmengine/module/module.pulsar.go @@ -0,0 +1,580 @@ +// Code generated by protoc-gen-go-pulsar. DO NOT EDIT. +package module + +import ( + _ "cosmossdk.io/api/cosmos/app/v1alpha1" + fmt "fmt" + runtime "github.com/cosmos/cosmos-proto/runtime" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoiface "google.golang.org/protobuf/runtime/protoiface" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + io "io" + reflect "reflect" + sync "sync" +) + +var ( + md_Module protoreflect.MessageDescriptor + fd_Module_authority protoreflect.FieldDescriptor +) + +func init() { + file_client_x_evmengine_module_module_proto_init() + md_Module = File_client_x_evmengine_module_module_proto.Messages().ByName("Module") + fd_Module_authority = md_Module.Fields().ByName("authority") +} + +var _ protoreflect.Message = (*fastReflection_Module)(nil) + +type fastReflection_Module Module + +func (x *Module) ProtoReflect() protoreflect.Message { + return (*fastReflection_Module)(x) +} + +func (x *Module) slowProtoReflect() protoreflect.Message { + mi := &file_client_x_evmengine_module_module_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_Module_messageType fastReflection_Module_messageType +var _ protoreflect.MessageType = fastReflection_Module_messageType{} + +type fastReflection_Module_messageType struct{} + +func (x fastReflection_Module_messageType) Zero() protoreflect.Message { + return (*fastReflection_Module)(nil) +} +func (x fastReflection_Module_messageType) New() protoreflect.Message { + return new(fastReflection_Module) +} +func (x fastReflection_Module_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_Module +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_Module) Descriptor() protoreflect.MessageDescriptor { + return md_Module +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_Module) Type() protoreflect.MessageType { + return _fastReflection_Module_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_Module) New() protoreflect.Message { + return new(fastReflection_Module) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_Module) Interface() protoreflect.ProtoMessage { + return (*Module)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_Module) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.Authority != "" { + value := protoreflect.ValueOfString(x.Authority) + if !f(fd_Module_authority, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_Module) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "client.x.evmengine.module.Module.authority": + return x.Authority != "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmengine.module.Module")) + } + panic(fmt.Errorf("message client.x.evmengine.module.Module does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "client.x.evmengine.module.Module.authority": + x.Authority = "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmengine.module.Module")) + } + panic(fmt.Errorf("message client.x.evmengine.module.Module does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_Module) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "client.x.evmengine.module.Module.authority": + value := x.Authority + return protoreflect.ValueOfString(value) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmengine.module.Module")) + } + panic(fmt.Errorf("message client.x.evmengine.module.Module does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "client.x.evmengine.module.Module.authority": + x.Authority = value.Interface().(string) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmengine.module.Module")) + } + panic(fmt.Errorf("message client.x.evmengine.module.Module does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "client.x.evmengine.module.Module.authority": + panic(fmt.Errorf("field authority of message client.x.evmengine.module.Module is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmengine.module.Module")) + } + panic(fmt.Errorf("message client.x.evmengine.module.Module does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_Module) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "client.x.evmengine.module.Module.authority": + return protoreflect.ValueOfString("") + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmengine.module.Module")) + } + panic(fmt.Errorf("message client.x.evmengine.module.Module does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_Module) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in client.x.evmengine.module.Module", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_Module) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_Module) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*Module) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + l = len(x.Authority) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*Module) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.Authority) > 0 { + i -= len(x.Authority) + copy(dAtA[i:], x.Authority) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Authority))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*Module) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: Module: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: Module: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.0 +// protoc (unknown) +// source: client/x/evmengine/module/module.proto + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Module is the config object for the module. +type Module struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // authority defines the custom module authority. If not set, defaults to the governance module. + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` +} + +func (x *Module) Reset() { + *x = Module{} + if protoimpl.UnsafeEnabled { + mi := &file_client_x_evmengine_module_module_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Module) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Module) ProtoMessage() {} + +// Deprecated: Use Module.ProtoReflect.Descriptor instead. +func (*Module) Descriptor() ([]byte, []int) { + return file_client_x_evmengine_module_module_proto_rawDescGZIP(), []int{0} +} + +func (x *Module) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +var File_client_x_evmengine_module_module_proto protoreflect.FileDescriptor + +var file_client_x_evmengine_module_module_proto_rawDesc = []byte{ + 0x0a, 0x26, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, 0x76, 0x6d, 0x65, 0x6e, + 0x67, 0x69, 0x6e, 0x65, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x2e, 0x78, 0x2e, 0x65, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x1a, 0x20, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x2f, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x61, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x3a, 0x39, 0xba, + 0xc0, 0x96, 0xda, 0x01, 0x33, 0x0a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x69, 0x6c, 0x69, 0x61, 0x64, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, + 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x42, 0xe0, 0x01, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, + 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x78, 0x2e, 0x65, 0x76, 0x6d, 0x65, 0x6e, 0x67, + 0x69, 0x6e, 0x65, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x42, 0x0b, 0x4d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2a, 0x63, 0x6f, 0x73, 0x6d, 0x6f, + 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2f, 0x6d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0xa2, 0x02, 0x04, 0x43, 0x58, 0x45, 0x4d, 0xaa, 0x02, 0x19, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x58, 0x2e, 0x45, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, + 0x65, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0xca, 0x02, 0x19, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5c, 0x58, 0x5c, 0x45, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5c, 0x4d, 0x6f, + 0x64, 0x75, 0x6c, 0x65, 0xe2, 0x02, 0x25, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5c, 0x58, 0x5c, + 0x45, 0x76, 0x6d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1c, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x3a, 0x58, 0x3a, 0x3a, 0x45, 0x76, 0x6d, 0x65, 0x6e, 0x67, + 0x69, 0x6e, 0x65, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_client_x_evmengine_module_module_proto_rawDescOnce sync.Once + file_client_x_evmengine_module_module_proto_rawDescData = file_client_x_evmengine_module_module_proto_rawDesc +) + +func file_client_x_evmengine_module_module_proto_rawDescGZIP() []byte { + file_client_x_evmengine_module_module_proto_rawDescOnce.Do(func() { + file_client_x_evmengine_module_module_proto_rawDescData = protoimpl.X.CompressGZIP(file_client_x_evmengine_module_module_proto_rawDescData) + }) + return file_client_x_evmengine_module_module_proto_rawDescData +} + +var file_client_x_evmengine_module_module_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_client_x_evmengine_module_module_proto_goTypes = []interface{}{ + (*Module)(nil), // 0: client.x.evmengine.module.Module +} +var file_client_x_evmengine_module_module_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_client_x_evmengine_module_module_proto_init() } +func file_client_x_evmengine_module_module_proto_init() { + if File_client_x_evmengine_module_module_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_client_x_evmengine_module_module_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Module); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_client_x_evmengine_module_module_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_client_x_evmengine_module_module_proto_goTypes, + DependencyIndexes: file_client_x_evmengine_module_module_proto_depIdxs, + MessageInfos: file_client_x_evmengine_module_module_proto_msgTypes, + }.Build() + File_client_x_evmengine_module_module_proto = out.File + file_client_x_evmengine_module_module_proto_rawDesc = nil + file_client_x_evmengine_module_module_proto_goTypes = nil + file_client_x_evmengine_module_module_proto_depIdxs = nil +} diff --git a/client/x/evmengine/testutil/expected_keepers_mocks.go b/client/x/evmengine/testutil/expected_keepers_mocks.go new file mode 100644 index 00000000..55352c99 --- /dev/null +++ b/client/x/evmengine/testutil/expected_keepers_mocks.go @@ -0,0 +1,221 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client/x/evmengine/types/expected_keepers.go +// +// Generated by this command: +// +// mockgen -source=client/x/evmengine/types/expected_keepers.go -package testutil -destination client/x/evmengine/testutil/expected_keepers_mocks.go +// + +// Package testutil is a generated GoMock package. +package testutil + +import ( + context "context" + reflect "reflect" + + types "cosmossdk.io/x/upgrade/types" + types0 "github.com/cosmos/cosmos-sdk/types" + types1 "github.com/ethereum/go-ethereum/core/types" + types2 "github.com/piplabs/story/client/x/evmengine/types" + bindings "github.com/piplabs/story/contracts/bindings" + gomock "go.uber.org/mock/gomock" +) + +// MockAccountKeeper is a mock of AccountKeeper interface. +type MockAccountKeeper struct { + ctrl *gomock.Controller + recorder *MockAccountKeeperMockRecorder +} + +// MockAccountKeeperMockRecorder is the mock recorder for MockAccountKeeper. +type MockAccountKeeperMockRecorder struct { + mock *MockAccountKeeper +} + +// NewMockAccountKeeper creates a new mock instance. +func NewMockAccountKeeper(ctrl *gomock.Controller) *MockAccountKeeper { + mock := &MockAccountKeeper{ctrl: ctrl} + mock.recorder = &MockAccountKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder { + return m.recorder +} + +// GetModuleAddress mocks base method. +func (m *MockAccountKeeper) GetModuleAddress(moduleName string) types0.AccAddress { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModuleAddress", moduleName) + ret0, _ := ret[0].(types0.AccAddress) + return ret0 +} + +// GetModuleAddress indicates an expected call of GetModuleAddress. +func (mr *MockAccountKeeperMockRecorder) GetModuleAddress(moduleName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAddress", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAddress), moduleName) +} + +// MockEvmStakingKeeper is a mock of EvmStakingKeeper interface. +type MockEvmStakingKeeper struct { + ctrl *gomock.Controller + recorder *MockEvmStakingKeeperMockRecorder +} + +// MockEvmStakingKeeperMockRecorder is the mock recorder for MockEvmStakingKeeper. +type MockEvmStakingKeeperMockRecorder struct { + mock *MockEvmStakingKeeper +} + +// NewMockEvmStakingKeeper creates a new mock instance. +func NewMockEvmStakingKeeper(ctrl *gomock.Controller) *MockEvmStakingKeeper { + mock := &MockEvmStakingKeeper{ctrl: ctrl} + mock.recorder = &MockEvmStakingKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEvmStakingKeeper) EXPECT() *MockEvmStakingKeeperMockRecorder { + return m.recorder +} + +// DequeueEligibleWithdrawals mocks base method. +func (m *MockEvmStakingKeeper) DequeueEligibleWithdrawals(ctx context.Context) (types1.Withdrawals, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DequeueEligibleWithdrawals", ctx) + ret0, _ := ret[0].(types1.Withdrawals) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DequeueEligibleWithdrawals indicates an expected call of DequeueEligibleWithdrawals. +func (mr *MockEvmStakingKeeperMockRecorder) DequeueEligibleWithdrawals(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DequeueEligibleWithdrawals", reflect.TypeOf((*MockEvmStakingKeeper)(nil).DequeueEligibleWithdrawals), ctx) +} + +// ParseDepositLog mocks base method. +func (m *MockEvmStakingKeeper) ParseDepositLog(ethlog types1.Log) (*bindings.IPTokenStakingDeposit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseDepositLog", ethlog) + ret0, _ := ret[0].(*bindings.IPTokenStakingDeposit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseDepositLog indicates an expected call of ParseDepositLog. +func (mr *MockEvmStakingKeeperMockRecorder) ParseDepositLog(ethlog any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseDepositLog", reflect.TypeOf((*MockEvmStakingKeeper)(nil).ParseDepositLog), ethlog) +} + +// ParseWithdrawLog mocks base method. +func (m *MockEvmStakingKeeper) ParseWithdrawLog(ethlog types1.Log) (*bindings.IPTokenStakingWithdraw, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseWithdrawLog", ethlog) + ret0, _ := ret[0].(*bindings.IPTokenStakingWithdraw) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseWithdrawLog indicates an expected call of ParseWithdrawLog. +func (mr *MockEvmStakingKeeperMockRecorder) ParseWithdrawLog(ethlog any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseWithdrawLog", reflect.TypeOf((*MockEvmStakingKeeper)(nil).ParseWithdrawLog), ethlog) +} + +// PeekEligibleWithdrawals mocks base method. +func (m *MockEvmStakingKeeper) PeekEligibleWithdrawals(ctx context.Context) (types1.Withdrawals, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeekEligibleWithdrawals", ctx) + ret0, _ := ret[0].(types1.Withdrawals) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PeekEligibleWithdrawals indicates an expected call of PeekEligibleWithdrawals. +func (mr *MockEvmStakingKeeperMockRecorder) PeekEligibleWithdrawals(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeekEligibleWithdrawals", reflect.TypeOf((*MockEvmStakingKeeper)(nil).PeekEligibleWithdrawals), ctx) +} + +// ProcessDeposit mocks base method. +func (m *MockEvmStakingKeeper) ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingDeposit) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessDeposit", ctx, ev) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessDeposit indicates an expected call of ProcessDeposit. +func (mr *MockEvmStakingKeeperMockRecorder) ProcessDeposit(ctx, ev any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessDeposit", reflect.TypeOf((*MockEvmStakingKeeper)(nil).ProcessDeposit), ctx, ev) +} + +// ProcessStakingEvents mocks base method. +func (m *MockEvmStakingKeeper) ProcessStakingEvents(ctx context.Context, height uint64, logs []*types2.EVMEvent) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessStakingEvents", ctx, height, logs) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessStakingEvents indicates an expected call of ProcessStakingEvents. +func (mr *MockEvmStakingKeeperMockRecorder) ProcessStakingEvents(ctx, height, logs any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessStakingEvents", reflect.TypeOf((*MockEvmStakingKeeper)(nil).ProcessStakingEvents), ctx, height, logs) +} + +// ProcessWithdraw mocks base method. +func (m *MockEvmStakingKeeper) ProcessWithdraw(ctx context.Context, ev *bindings.IPTokenStakingWithdraw) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessWithdraw", ctx, ev) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessWithdraw indicates an expected call of ProcessWithdraw. +func (mr *MockEvmStakingKeeperMockRecorder) ProcessWithdraw(ctx, ev any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessWithdraw", reflect.TypeOf((*MockEvmStakingKeeper)(nil).ProcessWithdraw), ctx, ev) +} + +// MockUpgradeKeeper is a mock of UpgradeKeeper interface. +type MockUpgradeKeeper struct { + ctrl *gomock.Controller + recorder *MockUpgradeKeeperMockRecorder +} + +// MockUpgradeKeeperMockRecorder is the mock recorder for MockUpgradeKeeper. +type MockUpgradeKeeperMockRecorder struct { + mock *MockUpgradeKeeper +} + +// NewMockUpgradeKeeper creates a new mock instance. +func NewMockUpgradeKeeper(ctrl *gomock.Controller) *MockUpgradeKeeper { + mock := &MockUpgradeKeeper{ctrl: ctrl} + mock.recorder = &MockUpgradeKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUpgradeKeeper) EXPECT() *MockUpgradeKeeperMockRecorder { + return m.recorder +} + +// ScheduleUpgrade mocks base method. +func (m *MockUpgradeKeeper) ScheduleUpgrade(ctx context.Context, plan types.Plan) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ScheduleUpgrade", ctx, plan) + ret0, _ := ret[0].(error) + return ret0 +} + +// ScheduleUpgrade indicates an expected call of ScheduleUpgrade. +func (mr *MockUpgradeKeeperMockRecorder) ScheduleUpgrade(ctx, plan any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleUpgrade", reflect.TypeOf((*MockUpgradeKeeper)(nil).ScheduleUpgrade), ctx, plan) +} diff --git a/client/x/evmengine/types/codec.go b/client/x/evmengine/types/codec.go new file mode 100644 index 00000000..8f677940 --- /dev/null +++ b/client/x/evmengine/types/codec.go @@ -0,0 +1,10 @@ +package types + +import ( + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" +) + +func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { + msgservice.RegisterMsgServiceDesc(registry, &_MsgService_serviceDesc) +} diff --git a/client/x/evmengine/types/cpayload.go b/client/x/evmengine/types/cpayload.go new file mode 100644 index 00000000..6953e914 --- /dev/null +++ b/client/x/evmengine/types/cpayload.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" +) + +// VoteExtensionProvider abstracts logic that provides consensus payload messages +// from the last commits vote extensions. +// +// EVMEngine calls this during PreparePayload to collect all vote extensions msgs to include in +// the consensus block. +type VoteExtensionProvider interface { + PrepareVotes(ctx context.Context, commit abci.ExtendedCommitInfo) ([]sdk.Msg, error) +} + +// EvmEventProcessor abstracts logic that processes EVM log events of the +// previous execution payload (current head) identified by +// the provided block hash. +// +// EVMEngine calls this during PreparePayload to collect all EVM-log-events to include in +// the consensus block. It is also called during ProcessPayload to verify the proposed EVM events. +type EvmEventProcessor interface { + Name() string + Prepare(ctx context.Context, blockHash common.Hash) ([]*EVMEvent, error) + Addresses() []common.Address + Deliver(ctx context.Context, blockHash common.Hash, log *EVMEvent) error +} diff --git a/client/x/evmengine/types/expected_keepers.go b/client/x/evmengine/types/expected_keepers.go new file mode 100644 index 00000000..59ca321f --- /dev/null +++ b/client/x/evmengine/types/expected_keepers.go @@ -0,0 +1,30 @@ +package types + +import ( + "context" + + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/contracts/bindings" +) + +type AccountKeeper interface { + GetModuleAddress(moduleName string) sdk.AccAddress +} + +type EvmStakingKeeper interface { + ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingDeposit) error + ProcessWithdraw(ctx context.Context, ev *bindings.IPTokenStakingWithdraw) error + DequeueEligibleWithdrawals(ctx context.Context) (ethtypes.Withdrawals, error) + ParseDepositLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingDeposit, error) + ParseWithdrawLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingWithdraw, error) + ProcessStakingEvents(ctx context.Context, height uint64, logs []*EVMEvent) error + PeekEligibleWithdrawals(ctx context.Context) (withdrawals ethtypes.Withdrawals, err error) +} + +type UpgradeKeeper interface { + ScheduleUpgrade(ctx context.Context, plan upgradetypes.Plan) error +} diff --git a/client/x/evmengine/types/genesis.go b/client/x/evmengine/types/genesis.go new file mode 100644 index 00000000..cc0978ad --- /dev/null +++ b/client/x/evmengine/types/genesis.go @@ -0,0 +1,13 @@ +package types + +func NewGenesisState(params Params) *GenesisState { + return &GenesisState{ + Params: params, + } +} + +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + } +} diff --git a/client/x/evmengine/types/genesis.pb.go b/client/x/evmengine/types/genesis.pb.go new file mode 100644 index 00000000..79035d1c --- /dev/null +++ b/client/x/evmengine/types/genesis.pb.go @@ -0,0 +1,321 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmengine/types/genesis.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type GenesisState struct { + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_393029c4e964f2cd, []int{0} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + +func init() { + proto.RegisterType((*GenesisState)(nil), "client.x.evmengine.types.GenesisState") +} + +func init() { + proto.RegisterFile("client/x/evmengine/types/genesis.proto", fileDescriptor_393029c4e964f2cd) +} + +var fileDescriptor_393029c4e964f2cd = []byte{ + // 180 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4b, 0xce, 0xc9, 0x4c, + 0xcd, 0x2b, 0xd1, 0xaf, 0xd0, 0x4f, 0x2d, 0xcb, 0x4d, 0xcd, 0x4b, 0xcf, 0xcc, 0x4b, 0xd5, 0x2f, + 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, + 0x2f, 0xc9, 0x17, 0x92, 0x80, 0xa8, 0xd3, 0xab, 0xd0, 0x83, 0xab, 0xd3, 0x03, 0xab, 0x93, 0x12, + 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0x2b, 0xd2, 0x07, 0xb1, 0x20, 0xea, 0xa5, 0x54, 0x71, 0x9a, 0x5b, + 0x90, 0x58, 0x94, 0x98, 0x0b, 0x35, 0x56, 0xc9, 0x8f, 0x8b, 0xc7, 0x1d, 0x62, 0x4f, 0x70, 0x49, + 0x62, 0x49, 0xaa, 0x90, 0x1d, 0x17, 0x1b, 0x44, 0x5e, 0x82, 0x51, 0x81, 0x51, 0x83, 0xdb, 0x48, + 0x41, 0x0f, 0x97, 0xbd, 0x7a, 0x01, 0x60, 0x75, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, 0x41, + 0x75, 0x39, 0x19, 0x9d, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, + 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x04, 0x2e, + 0x07, 0x25, 0xb1, 0x81, 0x9d, 0x62, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x30, 0xfa, 0xa6, 0x8e, + 0x0b, 0x01, 0x00, 0x00, +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Params.Size() + n += 1 + l + sovGenesis(uint64(l)) + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGenesis(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGenesis + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGenesis + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGenesis + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmengine/types/genesis.proto b/client/x/evmengine/types/genesis.proto new file mode 100644 index 00000000..3d3c70ac --- /dev/null +++ b/client/x/evmengine/types/genesis.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package client.x.evmengine.types; + +import "gogoproto/gogo.proto"; +import "client/x/evmengine/types/params.proto"; + +option go_package = "client/x/evmengine/types"; + +message GenesisState { + Params params = 1 [(gogoproto.nullable) = false]; +} diff --git a/client/x/evmengine/types/keys.go b/client/x/evmengine/types/keys.go new file mode 100644 index 00000000..c00ae040 --- /dev/null +++ b/client/x/evmengine/types/keys.go @@ -0,0 +1,19 @@ +package types + +import "cosmossdk.io/collections" + +const ( + // ModuleName defines the module name. + ModuleName = "evmengine" + + // StoreKey defines the primary module store key. + StoreKey = ModuleName + + // MemStoreKey defines the in-memory store key. + MemStoreKey = "mem_evmengine" +) + +// KVStore key prefixes. +var ( + ParamsKey = collections.NewPrefix(0) +) diff --git a/client/x/evmengine/types/params.go b/client/x/evmengine/types/params.go new file mode 100644 index 00000000..70c8970f --- /dev/null +++ b/client/x/evmengine/types/params.go @@ -0,0 +1,29 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" +) + +// NewParams creates a new Params instance. +func NewParams(executionBlockHash []byte) Params { + return Params{ + ExecutionBlockHash: executionBlockHash, + } +} + +// DefaultParams returns a default set of parameters. +func DefaultParams() Params { + return NewParams( + nil, + ) +} + +func ValidateExecutionBlockHash(executionBlockHash []byte) error { + if len(executionBlockHash) != common.HashLength { + return errors.New("invalid execution block hash length", "length", len(executionBlockHash)) + } + + return nil +} diff --git a/client/x/evmengine/types/params.pb.go b/client/x/evmengine/types/params.pb.go new file mode 100644 index 00000000..0453959c --- /dev/null +++ b/client/x/evmengine/types/params.pb.go @@ -0,0 +1,322 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmengine/types/params.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// Params defines the parameters for the module. +type Params struct { + ExecutionBlockHash []byte `protobuf:"bytes,1,opt,name=execution_block_hash,json=executionBlockHash,proto3" json:"execution_block_hash,omitempty" yaml:"execution_block_hash"` +} + +func (m *Params) Reset() { *m = Params{} } +func (m *Params) String() string { return proto.CompactTextString(m) } +func (*Params) ProtoMessage() {} +func (*Params) Descriptor() ([]byte, []int) { + return fileDescriptor_45d874549062308c, []int{0} +} +func (m *Params) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Params) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Params.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Params) XXX_Merge(src proto.Message) { + xxx_messageInfo_Params.Merge(m, src) +} +func (m *Params) XXX_Size() int { + return m.Size() +} +func (m *Params) XXX_DiscardUnknown() { + xxx_messageInfo_Params.DiscardUnknown(m) +} + +var xxx_messageInfo_Params proto.InternalMessageInfo + +func (m *Params) GetExecutionBlockHash() []byte { + if m != nil { + return m.ExecutionBlockHash + } + return nil +} + +func init() { + proto.RegisterType((*Params)(nil), "client.x.evmengine.types.Params") +} + +func init() { + proto.RegisterFile("client/x/evmengine/types/params.proto", fileDescriptor_45d874549062308c) +} + +var fileDescriptor_45d874549062308c = []byte{ + // 188 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4d, 0xce, 0xc9, 0x4c, + 0xcd, 0x2b, 0xd1, 0xaf, 0xd0, 0x4f, 0x2d, 0xcb, 0x4d, 0xcd, 0x4b, 0xcf, 0xcc, 0x4b, 0xd5, 0x2f, + 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x2f, 0x48, 0x2c, 0x4a, 0xcc, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, + 0xc9, 0x17, 0x92, 0x80, 0x28, 0xd3, 0xab, 0xd0, 0x83, 0x2b, 0xd3, 0x03, 0x2b, 0x93, 0x12, 0x49, + 0xcf, 0x4f, 0xcf, 0x07, 0x2b, 0xd2, 0x07, 0xb1, 0x20, 0xea, 0x95, 0xa2, 0xb9, 0xd8, 0x02, 0xc0, + 0xfa, 0x85, 0x02, 0xb9, 0x44, 0x52, 0x2b, 0x52, 0x93, 0x4b, 0x4b, 0x32, 0xf3, 0xf3, 0xe2, 0x93, + 0x72, 0xf2, 0x93, 0xb3, 0xe3, 0x33, 0x12, 0x8b, 0x33, 0x24, 0x18, 0x15, 0x18, 0x35, 0x78, 0x9c, + 0xe4, 0x3f, 0xdd, 0x93, 0x97, 0xae, 0x4c, 0xcc, 0xcd, 0xb1, 0x52, 0xc2, 0xa6, 0x4a, 0x29, 0x48, + 0x08, 0x2e, 0xec, 0x04, 0x12, 0xf5, 0x48, 0x2c, 0xce, 0x70, 0x32, 0x3a, 0xf1, 0x48, 0x8e, 0xf1, + 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, + 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0x09, 0x5c, 0xbe, 0x49, 0x62, 0x03, 0xbb, 0xcb, 0x18, 0x10, + 0x00, 0x00, 0xff, 0xff, 0x94, 0x06, 0xfa, 0x5b, 0xf0, 0x00, 0x00, 0x00, +} + +func (m *Params) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Params) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.ExecutionBlockHash) > 0 { + i -= len(m.ExecutionBlockHash) + copy(dAtA[i:], m.ExecutionBlockHash) + i = encodeVarintParams(dAtA, i, uint64(len(m.ExecutionBlockHash))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintParams(dAtA []byte, offset int, v uint64) int { + offset -= sovParams(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Params) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ExecutionBlockHash) + if l > 0 { + n += 1 + l + sovParams(uint64(l)) + } + return n +} + +func sovParams(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozParams(x uint64) (n int) { + return sovParams(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Params) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Params: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Params: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ExecutionBlockHash", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ExecutionBlockHash = append(m.ExecutionBlockHash[:0], dAtA[iNdEx:postIndex]...) + if m.ExecutionBlockHash == nil { + m.ExecutionBlockHash = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipParams(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipParams(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowParams + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowParams + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowParams + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthParams + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupParams + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthParams + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthParams = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowParams = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupParams = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmengine/types/params.proto b/client/x/evmengine/types/params.proto new file mode 100644 index 00000000..6eba4e91 --- /dev/null +++ b/client/x/evmengine/types/params.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package client.x.evmengine.types; + +import "gogoproto/gogo.proto"; + +option go_package = "client/x/evmengine/types"; + +// Params defines the parameters for the module. +message Params { + bytes execution_block_hash = 1 [ + (gogoproto.moretags) = "yaml:\"execution_block_hash\"" + ]; +} diff --git a/client/x/evmengine/types/tx.go b/client/x/evmengine/types/tx.go new file mode 100644 index 00000000..03bc0bb3 --- /dev/null +++ b/client/x/evmengine/types/tx.go @@ -0,0 +1,49 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/lib/errors" +) + +// ToEthLog converts an EVMEvent to an Ethereum Log. +// Note it assumes that Verify has been called before. +func (l *EVMEvent) ToEthLog() ethtypes.Log { + topics := make([]common.Hash, 0, len(l.Topics)) + for _, t := range l.Topics { + topics = append(topics, common.BytesToHash(t)) + } + + return ethtypes.Log{ + Address: common.BytesToAddress(l.Address), + Topics: topics, + Data: l.Data, + } +} + +func (l *EVMEvent) Verify() error { + if l == nil { + return errors.New("nil log") + } + + if l.Address == nil { + return errors.New("nil address") + } + + if len(l.Topics) == 0 { + return errors.New("empty topics") + } + + if len(l.Address) != len(common.Address{}) { + return errors.New("invalid address length") + } + + for _, t := range l.Topics { + if len(t) != len(common.Hash{}) { + return errors.New("invalid topic length") + } + } + + return nil +} diff --git a/client/x/evmengine/types/tx.pb.go b/client/x/evmengine/types/tx.pb.go new file mode 100644 index 00000000..936173c4 --- /dev/null +++ b/client/x/evmengine/types/tx.pb.go @@ -0,0 +1,933 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmengine/types/tx.proto + +package types + +import ( + context "context" + fmt "fmt" + _ "github.com/cosmos/cosmos-sdk/types/msgservice" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// MsgExecutionPayload defines the next EVM execution payload and the +// logs from previous execution payload. +type MsgExecutionPayload struct { + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + ExecutionPayload []byte `protobuf:"bytes,2,opt,name=execution_payload,json=executionPayload,proto3" json:"execution_payload,omitempty"` + PrevPayloadEvents []*EVMEvent `protobuf:"bytes,3,rep,name=prev_payload_events,json=prevPayloadEvents,proto3" json:"prev_payload_events,omitempty"` +} + +func (m *MsgExecutionPayload) Reset() { *m = MsgExecutionPayload{} } +func (m *MsgExecutionPayload) String() string { return proto.CompactTextString(m) } +func (*MsgExecutionPayload) ProtoMessage() {} +func (*MsgExecutionPayload) Descriptor() ([]byte, []int) { + return fileDescriptor_fb28e9d5b0c8eb16, []int{0} +} +func (m *MsgExecutionPayload) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExecutionPayload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExecutionPayload.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExecutionPayload) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExecutionPayload.Merge(m, src) +} +func (m *MsgExecutionPayload) XXX_Size() int { + return m.Size() +} +func (m *MsgExecutionPayload) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExecutionPayload.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExecutionPayload proto.InternalMessageInfo + +func (m *MsgExecutionPayload) GetAuthority() string { + if m != nil { + return m.Authority + } + return "" +} + +func (m *MsgExecutionPayload) GetExecutionPayload() []byte { + if m != nil { + return m.ExecutionPayload + } + return nil +} + +func (m *MsgExecutionPayload) GetPrevPayloadEvents() []*EVMEvent { + if m != nil { + return m.PrevPayloadEvents + } + return nil +} + +type ExecutionPayloadResponse struct { +} + +func (m *ExecutionPayloadResponse) Reset() { *m = ExecutionPayloadResponse{} } +func (m *ExecutionPayloadResponse) String() string { return proto.CompactTextString(m) } +func (*ExecutionPayloadResponse) ProtoMessage() {} +func (*ExecutionPayloadResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_fb28e9d5b0c8eb16, []int{1} +} +func (m *ExecutionPayloadResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ExecutionPayloadResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ExecutionPayloadResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ExecutionPayloadResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ExecutionPayloadResponse.Merge(m, src) +} +func (m *ExecutionPayloadResponse) XXX_Size() int { + return m.Size() +} +func (m *ExecutionPayloadResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ExecutionPayloadResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ExecutionPayloadResponse proto.InternalMessageInfo + +// EVMEvent represents a contract log event. +// Derived fields are not included in the protobuf. +type EVMEvent struct { + Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Topics [][]byte `protobuf:"bytes,2,rep,name=topics,proto3" json:"topics,omitempty"` + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` +} + +func (m *EVMEvent) Reset() { *m = EVMEvent{} } +func (m *EVMEvent) String() string { return proto.CompactTextString(m) } +func (*EVMEvent) ProtoMessage() {} +func (*EVMEvent) Descriptor() ([]byte, []int) { + return fileDescriptor_fb28e9d5b0c8eb16, []int{2} +} +func (m *EVMEvent) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EVMEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EVMEvent.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EVMEvent) XXX_Merge(src proto.Message) { + xxx_messageInfo_EVMEvent.Merge(m, src) +} +func (m *EVMEvent) XXX_Size() int { + return m.Size() +} +func (m *EVMEvent) XXX_DiscardUnknown() { + xxx_messageInfo_EVMEvent.DiscardUnknown(m) +} + +var xxx_messageInfo_EVMEvent proto.InternalMessageInfo + +func (m *EVMEvent) GetAddress() []byte { + if m != nil { + return m.Address + } + return nil +} + +func (m *EVMEvent) GetTopics() [][]byte { + if m != nil { + return m.Topics + } + return nil +} + +func (m *EVMEvent) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +func init() { + proto.RegisterType((*MsgExecutionPayload)(nil), "client.x.evmengine.types.MsgExecutionPayload") + proto.RegisterType((*ExecutionPayloadResponse)(nil), "client.x.evmengine.types.ExecutionPayloadResponse") + proto.RegisterType((*EVMEvent)(nil), "client.x.evmengine.types.EVMEvent") +} + +func init() { proto.RegisterFile("client/x/evmengine/types/tx.proto", fileDescriptor_fb28e9d5b0c8eb16) } + +var fileDescriptor_fb28e9d5b0c8eb16 = []byte{ + // 354 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0x31, 0x4b, 0xc3, 0x40, + 0x14, 0xc7, 0x7b, 0x8d, 0x56, 0x7b, 0x16, 0x69, 0xaf, 0xa0, 0x47, 0x90, 0x10, 0x33, 0x85, 0x8a, + 0x09, 0xc6, 0xcd, 0x51, 0xe8, 0x58, 0x28, 0x27, 0x38, 0xb8, 0x94, 0x98, 0x3c, 0x62, 0xa0, 0xcd, + 0x85, 0xdc, 0x35, 0xa4, 0x9b, 0x38, 0x3a, 0xf9, 0x51, 0xfa, 0x21, 0x1c, 0x1c, 0x3b, 0x3a, 0x4a, + 0x3b, 0xf4, 0x6b, 0x48, 0xd3, 0xc6, 0x42, 0x35, 0x4e, 0x77, 0xef, 0xf1, 0xfb, 0x3f, 0xde, 0xff, + 0x7f, 0x87, 0xcf, 0xbd, 0x61, 0x08, 0x91, 0xb4, 0x33, 0x1b, 0xd2, 0x11, 0x44, 0x41, 0x18, 0x81, + 0x2d, 0x27, 0x31, 0x08, 0x5b, 0x66, 0x56, 0x9c, 0x70, 0xc9, 0x09, 0x5d, 0x23, 0x56, 0x66, 0xfd, + 0x20, 0x56, 0x8e, 0xa8, 0xa7, 0x1e, 0x17, 0x23, 0x2e, 0xec, 0x91, 0x08, 0xec, 0xf4, 0x6a, 0x75, + 0xac, 0x25, 0xc6, 0x3b, 0xc2, 0xed, 0x9e, 0x08, 0xba, 0x19, 0x78, 0x63, 0x19, 0xf2, 0xa8, 0xef, + 0x4e, 0x86, 0xdc, 0xf5, 0xc9, 0x19, 0xae, 0xbb, 0x63, 0xf9, 0xc4, 0x93, 0x50, 0x4e, 0x28, 0xd2, + 0x91, 0x59, 0x67, 0xdb, 0x06, 0xb9, 0xc0, 0x2d, 0x28, 0x14, 0x83, 0x78, 0x2d, 0xa1, 0x55, 0x1d, + 0x99, 0x0d, 0xd6, 0x84, 0xdd, 0x51, 0x0c, 0xb7, 0xe3, 0x04, 0xd2, 0x82, 0x1b, 0x40, 0x0a, 0x91, + 0x14, 0x54, 0xd1, 0x15, 0xf3, 0xc8, 0x31, 0xac, 0xb2, 0x9d, 0xad, 0xee, 0x7d, 0xaf, 0xbb, 0x42, + 0x59, 0x6b, 0x25, 0xdf, 0x4c, 0xcb, 0x3b, 0xe2, 0xe6, 0xf8, 0x65, 0x39, 0xed, 0x6c, 0x17, 0x32, + 0x54, 0x4c, 0x77, 0x2d, 0x30, 0x10, 0x31, 0x8f, 0x04, 0x18, 0x7d, 0x7c, 0x58, 0x8c, 0x22, 0x14, + 0x1f, 0xb8, 0xbe, 0x9f, 0x80, 0x10, 0xb9, 0xa9, 0x06, 0x2b, 0x4a, 0x72, 0x82, 0x6b, 0x92, 0xc7, + 0xa1, 0x27, 0x68, 0x55, 0x57, 0xcc, 0x06, 0xdb, 0x54, 0x84, 0xe0, 0x3d, 0xdf, 0x95, 0x2e, 0x55, + 0x72, 0x3c, 0xbf, 0x3b, 0xaf, 0x08, 0xe3, 0x9e, 0x08, 0xee, 0x20, 0x49, 0x43, 0x0f, 0xc8, 0x18, + 0x37, 0x7f, 0xe5, 0x77, 0x59, 0xee, 0xeb, 0x8f, 0xb8, 0x55, 0xe7, 0x9f, 0x18, 0x4a, 0x7c, 0xa9, + 0xfb, 0xcf, 0xcb, 0x69, 0x07, 0xdd, 0x3a, 0x1f, 0x73, 0x0d, 0xcd, 0xe6, 0x1a, 0xfa, 0x9a, 0x6b, + 0xe8, 0x6d, 0xa1, 0x55, 0x66, 0x0b, 0xad, 0xf2, 0xb9, 0xd0, 0x2a, 0x0f, 0xb4, 0xec, 0xc7, 0x3c, + 0xd6, 0xf2, 0xc7, 0xbf, 0xfe, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x1c, 0x1e, 0x78, 0x3c, 0x54, 0x02, + 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// MsgServiceClient is the client API for MsgService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type MsgServiceClient interface { + // ExecutionPayload submits a new execution payload from consensus to the IliadEVM. + ExecutionPayload(ctx context.Context, in *MsgExecutionPayload, opts ...grpc.CallOption) (*ExecutionPayloadResponse, error) +} + +type msgServiceClient struct { + cc grpc1.ClientConn +} + +func NewMsgServiceClient(cc grpc1.ClientConn) MsgServiceClient { + return &msgServiceClient{cc} +} + +func (c *msgServiceClient) ExecutionPayload(ctx context.Context, in *MsgExecutionPayload, opts ...grpc.CallOption) (*ExecutionPayloadResponse, error) { + out := new(ExecutionPayloadResponse) + err := c.cc.Invoke(ctx, "/client.x.evmengine.types.MsgService/ExecutionPayload", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MsgServiceServer is the server API for MsgService service. +type MsgServiceServer interface { + // ExecutionPayload submits a new execution payload from consensus to the IliadEVM. + ExecutionPayload(context.Context, *MsgExecutionPayload) (*ExecutionPayloadResponse, error) +} + +// UnimplementedMsgServiceServer can be embedded to have forward compatible implementations. +type UnimplementedMsgServiceServer struct { +} + +func (*UnimplementedMsgServiceServer) ExecutionPayload(ctx context.Context, req *MsgExecutionPayload) (*ExecutionPayloadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExecutionPayload not implemented") +} + +func RegisterMsgServiceServer(s grpc1.Server, srv MsgServiceServer) { + s.RegisterService(&_MsgService_serviceDesc, srv) +} + +func _MsgService_ExecutionPayload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgExecutionPayload) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServiceServer).ExecutionPayload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/client.x.evmengine.types.MsgService/ExecutionPayload", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServiceServer).ExecutionPayload(ctx, req.(*MsgExecutionPayload)) + } + return interceptor(ctx, in, info, handler) +} + +var _MsgService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "client.x.evmengine.types.MsgService", + HandlerType: (*MsgServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ExecutionPayload", + Handler: _MsgService_ExecutionPayload_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "client/x/evmengine/types/tx.proto", +} + +func (m *MsgExecutionPayload) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgExecutionPayload) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgExecutionPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.PrevPayloadEvents) > 0 { + for iNdEx := len(m.PrevPayloadEvents) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.PrevPayloadEvents[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.ExecutionPayload) > 0 { + i -= len(m.ExecutionPayload) + copy(dAtA[i:], m.ExecutionPayload) + i = encodeVarintTx(dAtA, i, uint64(len(m.ExecutionPayload))) + i-- + dAtA[i] = 0x12 + } + if len(m.Authority) > 0 { + i -= len(m.Authority) + copy(dAtA[i:], m.Authority) + i = encodeVarintTx(dAtA, i, uint64(len(m.Authority))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *ExecutionPayloadResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ExecutionPayloadResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ExecutionPayloadResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *EVMEvent) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EVMEvent) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EVMEvent) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Data) > 0 { + i -= len(m.Data) + copy(dAtA[i:], m.Data) + i = encodeVarintTx(dAtA, i, uint64(len(m.Data))) + i-- + dAtA[i] = 0x1a + } + if len(m.Topics) > 0 { + for iNdEx := len(m.Topics) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Topics[iNdEx]) + copy(dAtA[i:], m.Topics[iNdEx]) + i = encodeVarintTx(dAtA, i, uint64(len(m.Topics[iNdEx]))) + i-- + dAtA[i] = 0x12 + } + } + if len(m.Address) > 0 { + i -= len(m.Address) + copy(dAtA[i:], m.Address) + i = encodeVarintTx(dAtA, i, uint64(len(m.Address))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgExecutionPayload) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Authority) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.ExecutionPayload) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if len(m.PrevPayloadEvents) > 0 { + for _, e := range m.PrevPayloadEvents { + l = e.Size() + n += 1 + l + sovTx(uint64(l)) + } + } + return n +} + +func (m *ExecutionPayloadResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *EVMEvent) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Address) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if len(m.Topics) > 0 { + for _, b := range m.Topics { + l = len(b) + n += 1 + l + sovTx(uint64(l)) + } + } + l = len(m.Data) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func sovTx(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTx(x uint64) (n int) { + return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgExecutionPayload) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExecutionPayload: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExecutionPayload: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ExecutionPayload", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ExecutionPayload = append(m.ExecutionPayload[:0], dAtA[iNdEx:postIndex]...) + if m.ExecutionPayload == nil { + m.ExecutionPayload = []byte{} + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PrevPayloadEvents", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PrevPayloadEvents = append(m.PrevPayloadEvents, &EVMEvent{}) + if err := m.PrevPayloadEvents[len(m.PrevPayloadEvents)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ExecutionPayloadResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ExecutionPayloadResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ExecutionPayloadResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *EVMEvent) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EVMEvent: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EVMEvent: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Address = append(m.Address[:0], dAtA[iNdEx:postIndex]...) + if m.Address == nil { + m.Address = []byte{} + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Topics", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Topics = append(m.Topics, make([]byte, postIndex-iNdEx)) + copy(m.Topics[len(m.Topics)-1], dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) + if m.Data == nil { + m.Data = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTx(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTx + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTx + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTx + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTx = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTx = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTx = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmengine/types/tx.proto b/client/x/evmengine/types/tx.proto new file mode 100644 index 00000000..c9e4a61f --- /dev/null +++ b/client/x/evmengine/types/tx.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package client.x.evmengine.types; + +import "cosmos/msg/v1/msg.proto"; + +option go_package = "client/x/evmengine/types"; + +// MsgService defines all the gRPC methods exposed by the evmengine module. +service MsgService { + option (cosmos.msg.v1.service) = true; + + // ExecutionPayload submits a new execution payload from consensus to the IliadEVM. + rpc ExecutionPayload (MsgExecutionPayload) returns (ExecutionPayloadResponse); +} + +// MsgExecutionPayload defines the next EVM execution payload and the +// logs from previous execution payload. +message MsgExecutionPayload { + option (cosmos.msg.v1.signer) = "authority"; + string authority = 1; + bytes execution_payload = 2; + repeated EVMEvent prev_payload_events = 3; +} + +message ExecutionPayloadResponse {} + +// EVMEvent represents a contract log event. +// Derived fields are not included in the protobuf. +message EVMEvent { + bytes address = 1; // Address of the contract that emitted the log event (20 bytes). + repeated bytes topics = 2; // List of topics provided by the contract (N * 32 bytes). + bytes data = 3; // Data supplied by the contract, usually ABI-encoded. +} diff --git a/client/x/evmengine/types/upgrade_contract.go b/client/x/evmengine/types/upgrade_contract.go new file mode 100644 index 00000000..b0b2db49 --- /dev/null +++ b/client/x/evmengine/types/upgrade_contract.go @@ -0,0 +1,35 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + + "github.com/piplabs/story/contracts/bindings" +) + +var ( + upgradeEntrypointABI = mustGetABI(bindings.UpgradeEntrypointMetaData) + SoftwareUpgradeEvent = mustGetEvent(upgradeEntrypointABI, "SoftwareUpgrade") +) + +// mustGetABI returns the metadata's ABI as an abi.ABI type. +// It panics on error. +func mustGetABI(metadata *bind.MetaData) *abi.ABI { + abi, err := metadata.GetAbi() + if err != nil { + panic(err) + } + + return abi +} + +// mustGetEvent returns the event with the given name from the ABI. +// It panics if the event is not found. +func mustGetEvent(abi *abi.ABI, name string) abi.Event { + event, ok := abi.Events[name] + if !ok { + panic("event not found") + } + + return event +} diff --git a/client/x/evmstaking/keeper/abci.go b/client/x/evmstaking/keeper/abci.go new file mode 100644 index 00000000..76cdce7f --- /dev/null +++ b/client/x/evmstaking/keeper/abci.go @@ -0,0 +1,145 @@ +package keeper + +import ( + "context" + "time" + + "cosmossdk.io/math" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +type UnbondedEntry struct { + validatorAddress string + delegatorAddress string + amount math.Int +} + +// Query staking module's UnbondingDelegation (UBD Queue) to get the matured unbonding delegations. Then, +// insert the matured unbonding delegations into the withdrawal queue. +// TODO: check if unbonded delegations in staking module must be distinguished based on source of generation, CL or EL. +func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) { + log.Debug(ctx, "EndBlock.evmstaking") + defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker) + + sdkCtx := sdk.UnwrapSDKContext(ctx) + ctxTime := sdkCtx.BlockHeader().Time + blockHeight := sdkCtx.BlockHeader().Height + + matureUnbonds, err := k.GetMatureUnbondedDelegations(ctx) + log.Debug(ctx, "Processing mature unbonding delegations", "count", len(matureUnbonds)) + if err != nil { + return nil, err + } + + delegations, err := k.stakingKeeper.GetAllDelegations(ctx) + if err != nil { + return nil, err + } + log.Debug(ctx, "All delegations", "count", len(delegations)) + for _, delegation := range delegations { + log.Debug(ctx, "Delegation", "delegator", delegation.DelegatorAddress, "validator", delegation.ValidatorAddress, "shares", delegation.Shares.String()) + } + + // make an array with each entry being the validator address, delegator address, and the amount + var unbondedEntries []UnbondedEntry + + for _, dvPair := range matureUnbonds { + validatorAddr, err := k.validatorAddressCodec.StringToBytes(dvPair.ValidatorAddress) + if err != nil { + return nil, errors.Wrap(err, "validator address from bech32") + } + + delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(dvPair.DelegatorAddress) + if err != nil { + return nil, errors.Wrap(err, "delegator address from bech32") + } + + ubd, err := (k.stakingKeeper).GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr) + if err != nil { + return nil, err + } + + // TODO: parameterized bondDenom + bondDenom := sdk.DefaultBondDenom + + // loop through all the entries and process unbonding mature entries + for i := range len(ubd.Entries) { + entry := ubd.Entries[i] + if entry.IsMature(ctxTime) && !entry.OnHold() { + // track undelegation only when remaining or truncated shares are non-zero + if !entry.Balance.IsZero() { + amt := sdk.NewCoin(bondDenom, entry.Balance) + // TODO: check if it's possible to add a double entry in the unbondedEntries array + unbondedEntries = append(unbondedEntries, UnbondedEntry{ + validatorAddress: dvPair.ValidatorAddress, + delegatorAddress: dvPair.DelegatorAddress, + amount: amt.Amount, + }) + } + } + } + } + + valUpdates, err := k.stakingKeeper.EndBlocker(ctx) + if err != nil { + return nil, err + } + + for _, entry := range unbondedEntries { + log.Debug(ctx, "Adding undelegation to withdrawal queue", + "delegator", entry.delegatorAddress, + "validator", entry.validatorAddress, + "amount", entry.amount.String()) + + delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(entry.delegatorAddress) + if err != nil { + return nil, errors.Wrap(err, "delegator address from bech32") + } + // Burn tokens from the delegator + _, coins := IPTokenToBondCoin(entry.amount.BigInt()) + err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins) + if err != nil { + return nil, errors.Wrap(err, "send coins from account to module") + } + err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins) + if err != nil { + return nil, errors.Wrap(err, "burn coins") + } + + // This should not produce error, as all delegations are done via the evmstaking module via EL. + // However, we should gracefully handle in case Get fails. + delEvmAddr, err := k.DelegatorMap.Get(ctx, entry.delegatorAddress) + if err != nil { + return nil, errors.Wrap(err, "map delegator pubkey to evm address") + } + + // push the undelegation to the withdrawal queue + err = k.AddWithdrawalToQueue(ctx, types.NewWithdrawal( + uint64(blockHeight), + entry.delegatorAddress, + entry.validatorAddress, + delEvmAddr, + entry.amount.Uint64(), + )) + if err != nil { + return nil, err + } + } + + partialWithdrawals, err := k.ExpectedPartialWithdrawals(ctx) + if err != nil { + return nil, err + } + if err := k.EnqueueEligiblePartialWithdrawal(ctx, partialWithdrawals); err != nil { + return nil, err + } + + return valUpdates, nil +} diff --git a/client/x/evmstaking/keeper/deposit.go b/client/x/evmstaking/keeper/deposit.go new file mode 100644 index 00000000..7cd397ca --- /dev/null +++ b/client/x/evmstaking/keeper/deposit.go @@ -0,0 +1,95 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" +) + +func (k Keeper) ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingDeposit) error { + depositorPubkey, err := k1util.PubKeyBytesToCosmos(ev.DepositorPubkey) + if err != nil { + return errors.Wrap(err, "depositor pubkey to cosmos") + } + + validatorPubkey, err := k1util.PubKeyBytesToCosmos(ev.ValidatorPubkey) + if err != nil { + return errors.Wrap(err, "validator pubkey to cosmos") + } + + depositorAddr := sdk.AccAddress(depositorPubkey.Address().Bytes()) + validatorAddr := sdk.ValAddress(validatorPubkey.Address().Bytes()) + + valEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(validatorPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "validator pubkey to evm address") + } + delEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(depositorPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "validator pubkey to evm address") + } + + amountCoin, amountCoins := IPTokenToBondCoin(ev.Amount) + + // Create account if not exists + if !k.authKeeper.HasAccount(ctx, depositorAddr) { + acc := k.authKeeper.NewAccountWithAddress(ctx, depositorAddr) + k.authKeeper.SetAccount(ctx, acc) + log.Debug(ctx, "Created account for depositor", + "address", depositorAddr.String(), + "evm_address", delEvmAddr.String(), + ) + } + + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, amountCoins); err != nil { + return errors.Wrap(err, "create stake coin for depositor: mint coins") + } + + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositorAddr, amountCoins); err != nil { + return errors.Wrap(err, "create stake coin for depositor: send coins") + } + + log.Info(ctx, "EVM staking deposit detected, delegating to validator", + "del_iliad", depositorAddr.String(), + "val_iliad", validatorAddr.String(), + "del_evm_addr", delEvmAddr.String(), + "val_evm_addr", valEvmAddr.String(), + "amount_coin", amountCoin.String(), + ) + + // Note that, after minting, we save the mapping between delegator bech32 address and evm address, which will be used in the withdrawal queue. + // The saving is done regardless of any error below, as the money is already minted and sent to the delegator, who can withdraw the minted amount. + // TODO: Confirm that bech32 address and evm address can be used interchangeably. Must be one-to-one or many-bech32-to-one-evm. + if err := k.DelegatorMap.Set(ctx, depositorAddr.String(), delEvmAddr.String()); err != nil { + return errors.Wrap(err, "set delegator map") + } + + // TODO: Check if we can instantiate the msgServer without type assertion + evmstakingSKeeper, ok := k.stakingKeeper.(*skeeper.Keeper) + if !ok { + return errors.New("type assertion failed") + } + skeeperMsgServer := skeeper.NewMsgServerImpl(evmstakingSKeeper) + + // Delegation by the depositor on the validator (validator existence is checked in msgServer.Delegate) + msg := stypes.NewMsgDelegate(depositorAddr.String(), validatorAddr.String(), amountCoin) + _, err = skeeperMsgServer.Delegate(ctx, msg) + if err != nil { + return errors.Wrap(err, "delegate") + } + + return nil +} + +func (k Keeper) ParseDepositLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingDeposit, error) { + return k.ipTokenStakingContract.ParseDeposit(ethlog) +} diff --git a/client/x/evmstaking/keeper/genesis.go b/client/x/evmstaking/keeper/genesis.go new file mode 100644 index 00000000..d6d61409 --- /dev/null +++ b/client/x/evmstaking/keeper/genesis.go @@ -0,0 +1,88 @@ +package keeper + +import ( + "context" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" +) + +func (k Keeper) InitGenesis(ctx context.Context, gs *types.GenesisState) error { + if err := k.ValidateGenesis(gs); err != nil { + return err + } + if err := k.SetParams(ctx, gs.Params); err != nil { + return err + } + + if err := k.WithdrawalQueue.Initialize(ctx); err != nil { + log.Error(ctx, "InitGenesis.evmstaking not initialized", err) + return err + } + vals, err := k.stakingKeeper.GetAllValidators(ctx) + if err != nil { + return err + } + for _, v := range vals { + pk, ok := v.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey) + if !ok { + return err + } + evmAddr, err := k1util.CosmosPubkeyToEVMAddress(pk.Bytes()) + if err != nil { + return err + } + + delegatorPubkey, err := k1util.PubKeyBytesToCosmos(pk.Bytes()) + if err != nil { + return err + } + delAddr := sdk.AccAddress(delegatorPubkey.Address().Bytes()) + + log.Debug(ctx, "InitGenesis.evmstaking validator", + "validator", v.GetOperator(), + "val_op", v.OperatorAddress, + "pk", pk.String(), + "pk_addr", pk.Address().String(), + "evm_addr", evmAddr.String(), + "del_addr", delAddr.String(), + ) + + err = k.DelegatorMap.Set(ctx, delAddr.String(), evmAddr.String()) + if err != nil { + return errors.Wrap(err, "set delegator map") + } + } + + return nil +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { + params, err := k.GetParams(ctx) + if err != nil { + panic(err) + } + + return &types.GenesisState{ + Params: params, + } +} + +//nolint:revive // TODO: validate genesis +func (k Keeper) ValidateGenesis(gs *types.GenesisState) error { + if err := types.ValidateMaxWithdrawalPerBlock(gs.Params.MaxWithdrawalPerBlock); err != nil { + return err + } + + if err := types.ValidateMaxSweepPerBlock(gs.Params.MaxSweepPerBlock, gs.Params.MaxWithdrawalPerBlock); err != nil { + return err + } + + return types.ValidateMinPartialWithdrawalAmount(gs.Params.MinPartialWithdrawalAmount) +} diff --git a/client/x/evmstaking/keeper/grpc_query.go b/client/x/evmstaking/keeper/grpc_query.go new file mode 100644 index 00000000..13c942bd --- /dev/null +++ b/client/x/evmstaking/keeper/grpc_query.go @@ -0,0 +1,63 @@ +package keeper + +import ( + "context" + + "cosmossdk.io/store/prefix" + + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/types/query" + + "github.com/piplabs/story/client/x/evmstaking/types" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var _ types.QueryServer = Keeper{} + +func (k Keeper) Params(ctx context.Context, request *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { + if request == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + params, err := k.GetParams(ctx) + if err != nil { + return nil, err + } + + return &types.QueryParamsResponse{Params: params}, nil +} + +// GetWithdrawalQueue returns the withdrawal queue in pagination. +// TODO: GetWithdrawalQueue tests. +func (k Keeper) GetWithdrawalQueue(ctx context.Context, request *types.QueryGetWithdrawalQueueRequest) (*types.QueryGetWithdrawalQueueResponse, error) { + if request == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + store := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx)) + wqStore := prefix.NewStore(store, types.WithdrawalQueueKey) // withdrawal queue store + + withdrawals, pageResp, err := query.GenericFilteredPaginate(k.cdc, wqStore, request.Pagination, func(_ []byte, wit *types.Withdrawal) (*types.Withdrawal, error) { + return wit, nil + }, func() *types.Withdrawal { + return &types.Withdrawal{} + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + var ws []*types.Withdrawal + for _, w := range withdrawals { + ws = append(ws, &types.Withdrawal{ + CreationHeight: w.CreationHeight, + DelegatorAddress: w.DelegatorAddress, + ValidatorAddress: w.ValidatorAddress, + ExecutionAddress: w.ExecutionAddress, + Amount: w.Amount, + }) + } + + return &types.QueryGetWithdrawalQueueResponse{Withdrawals: ws, Pagination: pageResp}, nil +} diff --git a/client/x/evmstaking/keeper/keeper.go b/client/x/evmstaking/keeper/keeper.go new file mode 100644 index 00000000..cb87169d --- /dev/null +++ b/client/x/evmstaking/keeper/keeper.go @@ -0,0 +1,200 @@ +package keeper + +import ( + "context" + "fmt" + "math/big" + + "cosmossdk.io/collections" + addresscodec "cosmossdk.io/core/address" + "cosmossdk.io/core/store" + "cosmossdk.io/log" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + + addcollections "github.com/piplabs/story/client/collections" + "github.com/piplabs/story/client/genutil/evm/predeploys" + evmenginetypes "github.com/piplabs/story/client/x/evmengine/types" + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + clog "github.com/piplabs/story/lib/log" +) + +// Keeper of the x/evmstaking store. +type Keeper struct { + cdc codec.BinaryCodec + storeService store.KVStoreService + validatorAddressCodec addresscodec.Codec + authority string + + authKeeper types.AccountKeeper + bankKeeper types.BankKeeper + slashingKeeper types.SlashingKeeper + stakingKeeper types.StakingKeeper + distributionKeeper types.DistributionKeeper + + ipTokenStakingContract *bindings.IPTokenStaking + ipTokenSlashingContract *bindings.IPTokenSlashing + + WithdrawalQueue addcollections.Queue[types.Withdrawal] + DelegatorMap collections.Map[string, string] // bech32 to evm address (TODO: confirm that it's one-to-one or many-bech32-to-one-evm) +} + +// NewKeeper creates a new evmstaking Keeper instance. +func NewKeeper( + cdc codec.BinaryCodec, + storeService store.KVStoreService, + ak types.AccountKeeper, + bk types.BankKeeper, + slk types.SlashingKeeper, + stk types.StakingKeeper, + dk types.DistributionKeeper, + authority string, + ethCl ethclient.Client, + validatorAddressCodec addresscodec.Codec, +) *Keeper { + // ensure that authority is a valid AccAddress + if _, err := ak.AddressCodec().StringToBytes(authority); err != nil { + panic("authority is not a valid acc address") + } + + // ensure the module account is set + if addr := ak.GetModuleAddress(types.ModuleName); addr == nil { + panic(types.ModuleName + " module account has not been set") + } + + sb := collections.NewSchemaBuilder(storeService) + + ipTokenStakingContract, err := bindings.NewIPTokenStaking(common.HexToAddress(predeploys.IPTokenStaking), ethCl) + if err != nil { + panic(fmt.Sprintf("failed to bind to the IPTokenStaking contract: %s", err)) + } + + ipTokenSlashingContract, err := bindings.NewIPTokenSlashing(common.HexToAddress(predeploys.IPTokenSlashing), ethCl) + if err != nil { + panic(fmt.Sprintf("failed to bind to the IPTokenSlashing contract: %s", err)) + } + + return &Keeper{ + cdc: cdc, + storeService: storeService, + authKeeper: ak, + bankKeeper: bk, + slashingKeeper: slk, + stakingKeeper: stk, + distributionKeeper: dk, + authority: authority, + validatorAddressCodec: validatorAddressCodec, + ipTokenStakingContract: ipTokenStakingContract, + ipTokenSlashingContract: ipTokenSlashingContract, + WithdrawalQueue: addcollections.NewQueue(sb, types.WithdrawalQueueKey, "withdrawal_queue", codec.CollValue[types.Withdrawal](cdc)), + DelegatorMap: collections.NewMap(sb, types.DelegatorMapKey, "delegator_map", collections.StringKey, collections.StringValue), + } +} + +func Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", "x/"+types.ModuleName) +} + +// GetAuthority returns the x/evmstaking module's authority. +func (k Keeper) GetAuthority() string { + return k.authority +} + +// ValidatorAddressCodec returns the app validator address codec. +func (k Keeper) ValidatorAddressCodec() addresscodec.Codec { + return k.validatorAddressCodec +} + +// TODO: Return log event results to properly manage failures. +func (k Keeper) ProcessStakingEvents(ctx context.Context, height uint64, logs []*evmenginetypes.EVMEvent) error { + gwei, exp := big.NewInt(10), big.NewInt(9) + gwei.Exp(gwei, exp, nil) + + for _, evmLog := range logs { + if err := evmLog.Verify(); err != nil { + return errors.Wrap(err, "verify log [BUG]") // This shouldn't happen + } + ethlog := evmLog.ToEthLog() + + // TODO: handle when each event processing fails. + + // Convert the amount from wei to gwei (Eth2 spec withdrawal is specified in gwei) by dividing by 10^9. + // TODO: consider rounding and decimal precision when dividing bigint. + + switch ethlog.Topics[0] { + case types.SetWithdrawalAddress.ID: + ev, err := k.ipTokenStakingContract.ParseSetWithdrawalAddress(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse SetWithdrawalAddress log", err) + continue + } + if err = k.ProcessSetWithdrawalAddress(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process set withdrawal address", err) + continue + } + case types.CreateValidatorEvent.ID: + ev, err := k.ParseCreateValidatorLog(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse CreateValidator log", err) + continue + } + ev.StakeAmount.Div(ev.StakeAmount, gwei) + if err = k.ProcessCreateValidator(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process create validator", err) + continue + } + case types.DepositEvent.ID: + ev, err := k.ParseDepositLog(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse Deposit log", err) + continue + } + ev.Amount.Div(ev.Amount, gwei) + if err = k.ProcessDeposit(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process deposit", err) + continue + } + case types.RedelegateEvent.ID: + ev, err := k.ParseRedelegateLog(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse Redelegate log", err) + continue + } + ev.Amount.Div(ev.Amount, gwei) + if err = k.ProcessRedelegate(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process redelegate", err) + continue + } + case types.WithdrawEvent.ID: + ev, err := k.ParseWithdrawLog(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse Withdraw log", err) + continue + } + ev.Amount.Div(ev.Amount, gwei) + if err = k.ProcessWithdraw(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process withdraw", err) + continue + } + case types.UnjailEvent.ID: + ev, err := k.ParseUnjailLog(ethlog) + if err != nil { + clog.Error(ctx, "Failed to parse Unjail log", err) + continue + } + if err = k.ProcessUnjail(ctx, ev); err != nil { + clog.Error(ctx, "Failed to process unjail", err) + continue + } + } + } + + clog.Debug(ctx, "Processed staking events", "height", height, "count", len(logs)) + + return nil +} diff --git a/client/x/evmstaking/keeper/keeper_test.go b/client/x/evmstaking/keeper/keeper_test.go new file mode 100644 index 00000000..c46828b5 --- /dev/null +++ b/client/x/evmstaking/keeper/keeper_test.go @@ -0,0 +1,131 @@ +package keeper_test + +import ( + "testing" + "time" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/address" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/runtime" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/suite" + + "github.com/piplabs/story/client/x/evmstaking/keeper" + "github.com/piplabs/story/client/x/evmstaking/module" + estestutil "github.com/piplabs/story/client/x/evmstaking/testutil" + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/ethclient" + + "go.uber.org/mock/gomock" +) + +var ( + PKs = simtestutil.CreateTestPubKeys(3) +) + +type TestSuite struct { + suite.Suite + + Ctx sdk.Context + + addrs []sdk.AccAddress + BankKeeper *estestutil.MockBankKeeper + StakingKeeper *skeeper.Keeper + EVMStakingKeeper *keeper.Keeper + + encCfg moduletestutil.TestEncodingConfig +} + +func (s *TestSuite) SetupTest() { + s.addrs = simtestutil.CreateIncrementalAccounts(4) + s.encCfg = moduletestutil.MakeTestEncodingConfig(module.AppModuleBasic{}) + evmstakingKey := storetypes.NewKVStoreKey(types.StoreKey) + stakingKey := storetypes.NewKVStoreKey(stypes.StoreKey) + storeService := runtime.NewKVStoreService(evmstakingKey) + stakingStoreService := runtime.NewKVStoreService(stakingKey) + + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + cms.MountStoreWithDB(evmstakingKey, storetypes.StoreTypeIAVL, db) + cms.MountStoreWithDB(stakingKey, storetypes.StoreTypeIAVL, db) + err := cms.LoadLatestVersion() + s.Require().NoError(err) + + s.Ctx = sdk.NewContext(cms, cmtproto.Header{Time: time.Now()}, false, log.NewNopLogger()) + + interfaceRegistry := codectypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(interfaceRegistry) + legacyAmino := codec.NewLegacyAmino() + stypes.RegisterLegacyAminoCodec(legacyAmino) + stypes.RegisterInterfaces(interfaceRegistry) + marshaler := codec.NewProtoCodec(interfaceRegistry) + + cfg := sdk.GetConfig() + cfg.SetBech32PrefixForAccount("story", "storypub") + cfg.SetBech32PrefixForValidator("storyvaloper", "storyvaloperpub") + cfg.SetBech32PrefixForConsensusNode("storyvalcons", "storyvalconspub") + + // gomock initializations + ctrl := gomock.NewController(s.T()) + + // mock keepers + accountKeeper := estestutil.NewMockAccountKeeper(ctrl) + accountKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(s.addrs[0]).AnyTimes() + accountKeeper.EXPECT().GetModuleAddress(stypes.ModuleName).Return(s.addrs[1]).AnyTimes() + accountKeeper.EXPECT().GetModuleAddress(stypes.BondedPoolName).Return(s.addrs[2]).AnyTimes() + accountKeeper.EXPECT().GetModuleAddress(stypes.NotBondedPoolName).Return(s.addrs[3]).AnyTimes() + accountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("story")).AnyTimes() + bankKeeper := estestutil.NewMockBankKeeper(ctrl) + s.BankKeeper = bankKeeper + distrKeeper := estestutil.NewMockDistributionKeeper(ctrl) + slashingKeeper := estestutil.NewMockSlashingKeeper(ctrl) + + // staking keeper + stakingKeeper := skeeper.NewKeeper( + marshaler, + stakingStoreService, + accountKeeper, + bankKeeper, + authtypes.NewModuleAddress(stypes.ModuleName).String(), + address.NewBech32Codec("storyvaloper"), + address.NewBech32Codec("storyvaloper"), + ) + s.StakingKeeper = stakingKeeper + s.Require().NoError(s.StakingKeeper.SetParams(s.Ctx, stypes.DefaultParams())) + + // emvstaking keeper + ethCl, err := ethclient.NewEngineMock() + s.Require().NoError(err) + evmstakingKeeper := keeper.NewKeeper( + marshaler, + storeService, + accountKeeper, + bankKeeper, + slashingKeeper, + stakingKeeper, + distrKeeper, + authtypes.NewModuleAddress(types.ModuleName).String(), + ethCl, + address.NewBech32Codec("storyvaloper"), + ) + s.EVMStakingKeeper = evmstakingKeeper +} + +func TestTestSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(TestSuite)) +} diff --git a/client/x/evmstaking/keeper/msg_server.go b/client/x/evmstaking/keeper/msg_server.go new file mode 100644 index 00000000..00dab721 --- /dev/null +++ b/client/x/evmstaking/keeper/msg_server.go @@ -0,0 +1,93 @@ +package keeper + +import ( + "context" + "strconv" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/piplabs/story/client/x/evmstaking/types" +) + +type msgServer struct { + *Keeper + types.UnimplementedMsgServiceServer +} + +func NewMsgServerImpl(keeper *Keeper) types.MsgServiceServer { + return &msgServer{Keeper: keeper} +} + +func (s msgServer) AddWithdrawal(ctx context.Context, msg *types.MsgAddWithdrawal) (*types.MsgAddWithdrawalResponse, error) { + validatorAddr, err := s.validatorAddressCodec.StringToBytes(msg.Withdrawal.ValidatorAddress) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid validator address: %s", err) + } + + // delegatorAddr + _, err = s.authKeeper.AddressCodec().StringToBytes(msg.Withdrawal.DelegatorAddress) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid delegator address: %s", err) + } + + if msg.Withdrawal.Amount <= 0 { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "invalid withdrawal amount") + } + + validator, err := (s.stakingKeeper).GetValidator(ctx, validatorAddr) + if err != nil { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "validator not found") + } + + if validator.IsJailed() { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "validator is jailed") + } + + // if validator.IsUnbonding() { + // return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "validator is unbonding") + //} + + // TODO: when validator is unbonded and stakes are also unbonded back to delegates, figure out how to allow withdrawal to EL + if validator.IsUnbonded() { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "validator is unbonded") + } + + // TODO: check bond denom once Amount is math.Int with coin type + + // bondDenom, err := (*s.stakingKeeper).BondDenom(ctx) + // if err != nil { + // return nil, err + //} + // + // if msg.Withdrawal.Amount.Denom != bondDenom { + // return nil, errorsmod.Wrapf( + // sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Withdrawal.Amount.Denom, bondDenom, + // ) + //} + + // TODO: balance check and transfer balance into this module, so that the module can burn the balance when the withdrawal is executed + + err = s.AddWithdrawalToQueue(ctx, types.NewWithdrawalFromMsg(msg)) + if err != nil { + return nil, errorsmod.Wrap(err, "add withdrawal") + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeAddWithdrawal, + sdk.NewAttribute(types.AttributeKeyValidator, msg.Withdrawal.ValidatorAddress), + sdk.NewAttribute(types.AttributeKeyDelegator, msg.Withdrawal.DelegatorAddress), + sdk.NewAttribute(types.AttributeKeyExecutionAddress, msg.Withdrawal.ExecutionAddress), + sdk.NewAttribute(sdk.AttributeKeyAmount, strconv.FormatUint(msg.Withdrawal.Amount, 10)), + sdk.NewAttribute(types.AttributeKeyCreationHeight, strconv.FormatUint(msg.Withdrawal.CreationHeight, 10)), + ), + }) + + return &types.MsgAddWithdrawalResponse{ + //RequestIndex: s., + }, nil +} diff --git a/client/x/evmstaking/keeper/params.go b/client/x/evmstaking/keeper/params.go new file mode 100644 index 00000000..a500fed5 --- /dev/null +++ b/client/x/evmstaking/keeper/params.go @@ -0,0 +1,108 @@ +package keeper + +import ( + "context" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/errors" +) + +func (k Keeper) MaxWithdrawalPerBlock(ctx context.Context) (uint32, error) { + params, err := k.GetParams(ctx) + if err != nil { + return 0, err + } + + return params.MaxWithdrawalPerBlock, nil +} + +func (k Keeper) MaxSweepPerBlock(ctx context.Context) (uint32, error) { + params, err := k.GetParams(ctx) + if err != nil { + return 0, err + } + + return params.MaxSweepPerBlock, nil +} + +func (k Keeper) MinPartialWithdrawalAmount(ctx context.Context) (uint64, error) { + params, err := k.GetParams(ctx) + if err != nil { + return 0, err + } + + return params.MinPartialWithdrawalAmount, nil +} + +// This method performs no validation of the parameters. +func (k Keeper) SetParams(ctx context.Context, params types.Params) error { + store := k.storeService.OpenKVStore(ctx) + bz, err := k.cdc.Marshal(¶ms) + if err != nil { + return errors.Wrap(err, "marshal params") + } + + err = store.Set(types.ParamsKey, bz) + if err != nil { + return errors.Wrap(err, "set params") + } + + return nil +} + +func (k Keeper) GetParams(ctx context.Context) (params types.Params, err error) { + store := k.storeService.OpenKVStore(ctx) + bz, err := store.Get(types.ParamsKey) + if err != nil { + return params, errors.Wrap(err, "get params") + } + + if bz == nil { + return params, nil + } + + err = k.cdc.Unmarshal(bz, ¶ms) + if err != nil { + return params, errors.Wrap(err, "unmarshal params") + } + + return params, nil +} + +func (k Keeper) SetNextValidatorSweepIndex(ctx context.Context, nextValIndex sdk.IntProto) error { + store := k.storeService.OpenKVStore(ctx) + bz, err := k.cdc.Marshal(&nextValIndex) + if err != nil { + return errors.Wrap(err, "marshal next validator sweep index") + } + + err = store.Set(types.NextValidatorSweepIndexKey, bz) + if err != nil { + return errors.Wrap(err, "set next validator sweep index") + } + + return nil +} + +func (k Keeper) GetNextValidatorSweepIndex(ctx context.Context) (nextValIndex sdk.IntProto, err error) { + store := k.storeService.OpenKVStore(ctx) + bz, err := store.Get(types.NextValidatorSweepIndexKey) + if err != nil { + return nextValIndex, errors.Wrap(err, "get next validator sweep index") + } + + if bz == nil { + return sdk.IntProto{Int: math.NewInt(0)}, nil + } + + err = k.cdc.Unmarshal(bz, &nextValIndex) + if err != nil { + return nextValIndex, errors.Wrap(err, "unmarshal next validator sweep index") + } + + return nextValIndex, nil +} diff --git a/client/x/evmstaking/keeper/redelegation.go b/client/x/evmstaking/keeper/redelegation.go new file mode 100644 index 00000000..a765fd2d --- /dev/null +++ b/client/x/evmstaking/keeper/redelegation.go @@ -0,0 +1,73 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" +) + +func (k Keeper) ProcessRedelegate(ctx context.Context, ev *bindings.IPTokenStakingRedelegate) error { + depositorPubkey, err := k1util.PubKeyBytesToCosmos(ev.DepositorPubkey) + if err != nil { + return errors.Wrap(err, "depositor pubkey to cosmos") + } + + validatorSrcPubkey, err := k1util.PubKeyBytesToCosmos(ev.ValidatorSrcPubkey) + if err != nil { + return errors.Wrap(err, "src validator pubkey to cosmos") + } + + validatorDstPubkey, err := k1util.PubKeyBytesToCosmos(ev.ValidatorDstPubkey) + if err != nil { + return errors.Wrap(err, "dst validator pubkey to cosmos") + } + + depositorAddr := sdk.AccAddress(depositorPubkey.Address().Bytes()) + validatorSrcAddr := sdk.ValAddress(validatorSrcPubkey.Address().Bytes()) + validatorDstAddr := sdk.ValAddress(validatorDstPubkey.Address().Bytes()) + + delEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(depositorPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "deledator pubkey to evm address") + } + valSrcEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(validatorSrcPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "src validator pubkey to evm address") + } + valDstEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(validatorDstPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "dst validator pubkey to evm address") + } + + amountCoin, _ := IPTokenToBondCoin(ev.Amount) + + log.Info(ctx, "EVM staking relegation detected", + "del_iliad", depositorAddr.String(), + "val_src_iliad", validatorSrcAddr.String(), + "val_dst_iliad", validatorDstAddr.String(), + "del_evm_addr", delEvmAddr.String(), + "val_src_evm_addr", valSrcEvmAddr.String(), + "val_dst_evm_addr", valDstEvmAddr.String(), + "amount_coin", amountCoin.String(), + ) + + msg := stypes.NewMsgBeginRedelegate(depositorAddr.String(), validatorSrcAddr.String(), validatorDstAddr.String(), amountCoin) + _, err = skeeper.NewMsgServerImpl(k.stakingKeeper.(*skeeper.Keeper)).BeginRedelegate(ctx, msg) + if err != nil { + return errors.Wrap(err, "failed to begin redelegation") + } + + return nil +} + +func (k Keeper) ParseRedelegateLog(ethLog ethtypes.Log) (*bindings.IPTokenStakingRedelegate, error) { + return k.ipTokenStakingContract.ParseRedelegate(ethLog) +} diff --git a/client/x/evmstaking/keeper/redelegation_test.go b/client/x/evmstaking/keeper/redelegation_test.go new file mode 100644 index 00000000..79ae8ec6 --- /dev/null +++ b/client/x/evmstaking/keeper/redelegation_test.go @@ -0,0 +1,120 @@ +package keeper_test + +import ( + "math/big" + + "github.com/cometbft/cometbft/crypto" + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/testutil" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + gethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/k1util" + + "go.uber.org/mock/gomock" +) + +func createAddresses(count int) ([]crypto.PubKey, []sdk.AccAddress, []sdk.ValAddress) { + var pubKeys []crypto.PubKey + var accAddrs []sdk.AccAddress + var valAddrs []sdk.ValAddress + for range count { + pubKey := k1.GenPrivKey().PubKey() + accAddr := sdk.AccAddress(pubKey.Address().Bytes()) + valAddr := sdk.ValAddress(pubKey.Address().Bytes()) + pubKeys = append(pubKeys, pubKey) + accAddrs = append(accAddrs, accAddr) + valAddrs = append(valAddrs, valAddr) + } + + return pubKeys, accAddrs, valAddrs +} + +func (s *TestSuite) TestRedelegation() { + ctx, keeper, stakingKeeper := s.Ctx, s.EVMStakingKeeper, s.StakingKeeper + require := s.Require() + + // create addresses + pubKeys, accAddrs, valAddrs := createAddresses(3) + delAddr := accAddrs[0] + valSrcAddr := valAddrs[1] + valDstAddr := valAddrs[2] + + // create a validator (src) + validatorSrc := testutil.NewValidator(s.T(), valSrcAddr, PKs[1]) + require.NoError(stakingKeeper.SetValidatorByConsAddr(ctx, validatorSrc)) + + // delegate to the validator + valTokens := stakingKeeper.TokensFromConsensusPower(ctx, 10) + validator, issuedShares := validatorSrc.AddTokensFromDel(valTokens) + require.Equal(valTokens, issuedShares.RoundInt()) + s.BankKeeper.EXPECT().SendCoinsFromModuleToModule(gomock.Any(), stypes.NotBondedPoolName, stypes.BondedPoolName, gomock.Any()) + _ = skeeper.TestingUpdateValidator(stakingKeeper, ctx, validator, true) + delegation := stypes.NewDelegation(delAddr.String(), valSrcAddr.String(), issuedShares) + require.NoError(stakingKeeper.SetDelegation(ctx, delegation)) + delEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(pubKeys[0].Bytes()) + require.NoError(err) + require.NoError(keeper.DelegatorMap.Set(ctx, delAddr.String(), delEvmAddr.String())) + + // create a second validator(dst) and delegate the same amount of token + validatorDst := testutil.NewValidator(s.T(), valDstAddr, PKs[1]) + validatorDst, issuedShares = validatorDst.AddTokensFromDel(valTokens) + require.Equal(valTokens, issuedShares.RoundInt()) + s.BankKeeper.EXPECT().SendCoinsFromModuleToModule(gomock.Any(), stypes.NotBondedPoolName, stypes.BondedPoolName, gomock.Any()) + _ = skeeper.TestingUpdateValidator(stakingKeeper, ctx, validatorDst, true) + delegation = stypes.NewDelegation(delAddr.String(), valDstAddr.String(), issuedShares) + require.NoError(stakingKeeper.SetDelegation(ctx, delegation)) + + // check the amount of delegated tokens + delSrc, err := stakingKeeper.GetDelegatorValidator(ctx, delAddr, valSrcAddr) + require.NoError(err) + require.True(delSrc.Tokens.Equal(valTokens)) + + delDst, err := stakingKeeper.GetDelegatorValidator(ctx, delAddr, valDstAddr) + require.NoError(err) + require.True(delDst.Tokens.Equal(valTokens)) + + // test shouldn't have and redelegations + has, err := stakingKeeper.HasReceivingRedelegation(ctx, delAddr, valDstAddr) + require.NoError(err) + require.False(has) + + redelTokens := stakingKeeper.TokensFromConsensusPower(ctx, 5) + + ipTokenRedelegate := &bindings.IPTokenStakingRedelegate{ + DepositorPubkey: pubKeys[0].Bytes(), + ValidatorSrcPubkey: pubKeys[1].Bytes(), + ValidatorDstPubkey: pubKeys[2].Bytes(), + Amount: big.NewInt(redelTokens.Int64()), // multiply power reduction of 1000000 + Raw: gethtypes.Log{}, + } + + // redelegation + require.NoError(keeper.ProcessRedelegate(ctx, ipTokenRedelegate)) + + // check the amount of delegated tokens after redelegation + delSrc, err = stakingKeeper.GetDelegatorValidator(ctx, delAddr, valSrcAddr) + require.NoError(err) + require.True(delSrc.Tokens.Equal(valTokens.Sub(redelTokens))) + + delDst, err = stakingKeeper.GetDelegatorValidator(ctx, delAddr, valDstAddr) + require.NoError(err) + require.True(delDst.Tokens.Equal(valTokens.Add(redelTokens))) + + // params + params, err := s.StakingKeeper.GetParams(ctx) + require.NoError(err) + + redelegation, err := stakingKeeper.GetRedelegation(ctx, delAddr, valSrcAddr, valDstAddr) + require.NoError(err) + require.Equal(delAddr.String(), redelegation.DelegatorAddress) + require.Equal(valSrcAddr.String(), redelegation.ValidatorSrcAddress) + require.Equal(valDstAddr.String(), redelegation.ValidatorDstAddress) + require.Equal(redelTokens, redelegation.Entries[0].InitialBalance) + require.Equal(ctx.BlockTime().Add(params.UnbondingTime), redelegation.Entries[0].CompletionTime) + + // TODO: test EndBlock +} diff --git a/client/x/evmstaking/keeper/set_address.go b/client/x/evmstaking/keeper/set_address.go new file mode 100644 index 00000000..c8f1a276 --- /dev/null +++ b/client/x/evmstaking/keeper/set_address.go @@ -0,0 +1,28 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" +) + +func (k Keeper) ProcessSetWithdrawalAddress(ctx context.Context, ev *bindings.IPTokenStakingSetWithdrawalAddress) error { + depositorPubkey, err := k1util.PubKeyBytesToCosmos(ev.DepositorPubkey) + if err != nil { + return errors.Wrap(err, "depositor pubkey to cosmos") + } + + depositorAddr := sdk.AccAddress(depositorPubkey.Address().Bytes()) + executionAddr := common.BytesToAddress(ev.ExecutionAddress[:]) + + if err := k.DelegatorMap.Set(ctx, depositorAddr.String(), executionAddr.String()); err != nil { + return errors.Wrap(err, "delegator map set") + } + + return nil +} diff --git a/client/x/evmstaking/keeper/staking_queue.go b/client/x/evmstaking/keeper/staking_queue.go new file mode 100644 index 00000000..b2ffda03 --- /dev/null +++ b/client/x/evmstaking/keeper/staking_queue.go @@ -0,0 +1,35 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/piplabs/story/lib/errors" +) + +// Modified from https://github.com/cosmos/cosmos-sdk/blob/v0.50.7/x/staking/keeper/delegation.go#L521 +func (k Keeper) GetMatureUnbondedDelegations(ctx context.Context) (matureUnbonds []stypes.DVPair, err error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + currTime := sdkCtx.BlockHeader().Time + + // gets an iterator for all timeslices from time 0 until the current Blockheader time + unbondingTimesliceIterator, err := (k.stakingKeeper).UBDQueueIterator(ctx, currTime) + if err != nil { + return nil, err + } + defer unbondingTimesliceIterator.Close() + + for ; unbondingTimesliceIterator.Valid(); unbondingTimesliceIterator.Next() { + timeslice := stypes.DVPairs{} + value := unbondingTimesliceIterator.Value() + if err = k.cdc.Unmarshal(value, ×lice); err != nil { + return matureUnbonds, errors.Wrap(err, "failed to unmarshal unbonding timeslice") + } + + matureUnbonds = append(matureUnbonds, timeslice.Pairs...) + } + + return matureUnbonds, nil +} diff --git a/client/x/evmstaking/keeper/unjail.go b/client/x/evmstaking/keeper/unjail.go new file mode 100644 index 00000000..4504325a --- /dev/null +++ b/client/x/evmstaking/keeper/unjail.go @@ -0,0 +1,31 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" +) + +func (k Keeper) ProcessUnjail(ctx context.Context, ev *bindings.IPTokenSlashingUnjail) error { + validatorPubkey, err := k1util.PubKeyBytesToCosmos(ev.ValidatorCmpPubkey) + if err != nil { + return errors.Wrap(err, "validator pubkey to cosmos") + } + + valAddr := sdk.ValAddress(validatorPubkey.Address().Bytes()) + err = k.slashingKeeper.Unjail(ctx, valAddr) + if err != nil { + return errors.Wrap(err, "unjail") + } + + return nil +} + +func (k Keeper) ParseUnjailLog(ethlog ethtypes.Log) (*bindings.IPTokenSlashingUnjail, error) { + return k.ipTokenSlashingContract.ParseUnjail(ethlog) +} diff --git a/client/x/evmstaking/keeper/utils.go b/client/x/evmstaking/keeper/utils.go new file mode 100644 index 00000000..d8dd62be --- /dev/null +++ b/client/x/evmstaking/keeper/utils.go @@ -0,0 +1,17 @@ +package keeper + +import ( + "math/big" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// IPTokenToBondCoin converts the IP amount into a $STAKE coin. +// TODO: At this point, it is 1-to1, but this might change in the future. +// TODO: parameterized bondDenom. +func IPTokenToBondCoin(amount *big.Int) (sdk.Coin, sdk.Coins) { + coin := sdk.NewCoin(sdk.DefaultBondDenom, math.NewIntFromBigInt(amount)) + return coin, sdk.NewCoins(coin) +} diff --git a/client/x/evmstaking/keeper/validator.go b/client/x/evmstaking/keeper/validator.go new file mode 100644 index 00000000..9d05f98c --- /dev/null +++ b/client/x/evmstaking/keeper/validator.go @@ -0,0 +1,125 @@ +package keeper + +import ( + "context" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" +) + +func (k Keeper) ProcessCreateValidator(ctx context.Context, ev *bindings.IPTokenStakingCreateValidator) error { + // When creating a validator, it's self-delegation. Thus, validator pubkey is also delegation pubkey. + validatorPubkey, err := k1util.PubKeyBytesToCosmos(ev.ValidatorPubkey) + if err != nil { + return errors.Wrap(err, "validator pubkey to cosmos") + } + + validatorAddr := sdk.ValAddress(validatorPubkey.Address().Bytes()) + delegatorAddr := sdk.AccAddress(validatorPubkey.Address().Bytes()) + + delEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(validatorPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "validator pubkey to evm address") + } + + amountCoin, amountCoins := IPTokenToBondCoin(ev.StakeAmount) + + // Create account if not exists + if !k.authKeeper.HasAccount(ctx, delegatorAddr) { + acc := k.authKeeper.NewAccountWithAddress(ctx, delegatorAddr) + k.authKeeper.SetAccount(ctx, acc) + log.Debug(ctx, "Created account for depositor", + "address", validatorAddr.String(), + "evm_address", delEvmAddr.String(), + ) + } + + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, amountCoins); err != nil { + return errors.Wrap(err, "create stake coin for depositor: mint coins") + } + + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, delegatorAddr, amountCoins); err != nil { + return errors.Wrap(err, "create stake coin for depositor: send coins") + } + + log.Info(ctx, "EVM staking create validator detected", + "val_iliad", validatorAddr.String(), + "val_pubkey", validatorPubkey.String(), + "del_iliad", delegatorAddr.String(), + "del_evm_addr", delEvmAddr.String(), + "amount_coin", amountCoin.String(), + ) + + // Note that, after minting, we save the mapping between delegator bech32 address and evm address, which will be used in the withdrawal queue. + // The saving is done regardless of any error below, as the money is already minted and sent to the delegator, who can withdraw the minted amount. + // TODO: Confirm that bech32 address and evm address can be used interchangeably. Must be one-to-one or many-bech32-to-one-evm. + if err := k.DelegatorMap.Set(ctx, delegatorAddr.String(), delEvmAddr.String()); err != nil { + return errors.Wrap(err, "set delegator map") + } + + // TODO: Check if we can instantiate the msgServer without type assertion + evmstakingSKeeper, ok := k.stakingKeeper.(*skeeper.Keeper) + if !ok { + return errors.New("type assertion failed") + } + skeeperMsgServer := skeeper.NewMsgServerImpl(evmstakingSKeeper) + + _, err = k.stakingKeeper.GetValidator(ctx, validatorAddr) + if err != nil { //nolint:nestif // readability + // Either the validator does not exist, or unknown error. + if !errors.Is(err, stypes.ErrNoValidatorFound) { + return errors.Wrap(err, "get validator") + } + + moniker := ev.Moniker + if moniker == "validator" { + moniker = validatorAddr.String() // use validator address as moniker if not provided (ie. "validator") + } + + // Validator does not exist, create validator with self-delegation. + msg, err := stypes.NewMsgCreateValidator( + validatorAddr.String(), + validatorPubkey, + amountCoin, + stypes.Description{Moniker: moniker}, + stypes.NewCommissionRates( + // Divide these decimals by 100 to convert from basis points to decimal. Will cut off decimal as the rates are integers. + math.LegacyNewDec(int64(ev.CommissionRate)).Quo(math.LegacyNewDec(10000)), + math.LegacyNewDec(int64(ev.MaxCommissionRate)).Quo(math.LegacyNewDec(10000)), + math.LegacyNewDec(int64(ev.MaxCommissionChangeRate)).Quo(math.LegacyNewDec(10000)), + ), + math.NewInt(1)) // Stub out minimum self delegation for now, just use 1. + if err != nil { + return errors.Wrap(err, "create validator message") + } + + _, err = skeeperMsgServer.CreateValidator(ctx, msg) + if err != nil { + return errors.Wrap(err, "create validator") + } + } else { + // The validator already exists, delegate the amount to the validator. + // UX should prevent this, but users can theoretically call CreateValidator twice on the same validator pubkey. + msg := stypes.NewMsgDelegate(delegatorAddr.String(), validatorAddr.String(), amountCoin) + _, err = skeeperMsgServer.Delegate(ctx, msg) + if err != nil { + return errors.Wrap(err, "delegate") + } + } + + return nil +} + +func (k Keeper) ParseCreateValidatorLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingCreateValidator, error) { + return k.ipTokenStakingContract.ParseCreateValidator(ethlog) +} diff --git a/client/x/evmstaking/keeper/withdraw.go b/client/x/evmstaking/keeper/withdraw.go new file mode 100644 index 00000000..da942edd --- /dev/null +++ b/client/x/evmstaking/keeper/withdraw.go @@ -0,0 +1,292 @@ +package keeper + +import ( + "context" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + dtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + estypes "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/promutil" +) + +func (k Keeper) ExpectedPartialWithdrawals(ctx context.Context) ([]estypes.Withdrawal, error) { + // TODO: user more fine-grained cursor with next delegator sweep index. + nextValSweepIndex, err := k.GetNextValidatorSweepIndex(ctx) + if err != nil { + return nil, err + } + nextValIndex := nextValSweepIndex.Int.Int64() + // Get all validators first, and then do a circular sweep + validatorSet, err := (k.stakingKeeper.(*skeeper.Keeper)).GetAllValidators(ctx) + if err != nil { + return nil, errors.Wrap(err, "get all validators") + } + + if nextValIndex >= int64(len(validatorSet)) { + // TODO: TBD + log.Warn( + ctx, "NextValidatorIndex exceeds the validator set size", + errors.New("nextValidatorIndex overflow"), + "validator_set", len(validatorSet), + "next_validator_index", nextValIndex, + ) + nextValIndex = 0 + } + + // Iterate all validators from `nextValidatorIndex` to find out eligible partial withdrawals. + var ( + swept uint32 + withdrawals []estypes.Withdrawal + ) + // Get sweep limit per block. + sweepBound, err := k.MaxSweepPerBlock(ctx) + if err != nil { + return nil, err + } + // Get minimal partial withdrawal amount. + minPartialWithdrawalAmount, err := k.MinPartialWithdrawalAmount(ctx) + if err != nil { + return nil, err + } + log.Debug( + ctx, "partial withdrawal params", + "min_partial_withdraw_amount", minPartialWithdrawalAmount, + "max_sweep_per_block", sweepBound, + ) + // Sweep and get eligible partial withdrawals. + for range validatorSet { + if swept > sweepBound { + break + } + if validatorSet[nextValIndex].IsJailed() { + continue + } + // Get validator's address. + valBz, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(validatorSet[nextValIndex].GetOperator()) + if err != nil { + return nil, errors.Wrap(err, "validator address from bech32") + } + valAddr := sdk.ValAddress(valBz) + valAccAddr := sdk.AccAddress(valAddr) + // Get validator commissions. + valCommission, err := k.distributionKeeper.GetValidatorAccumulatedCommission(ctx, valAddr) + if err != nil { + return nil, err + } + log.Debug( + ctx, "Get validator commission", + "val_addr", valAddr.String(), + "commission_amount", valCommission.Commission.String(), + ) + // Get all delegators of the validator. + delegators, err := (k.stakingKeeper.(*skeeper.Keeper)).GetValidatorDelegations(ctx, valAddr) + if err != nil { + return nil, errors.Wrap(err, "get validator delegations") + } + swept += uint32(len(delegators)) + log.Debug( + ctx, "Get all delegators of validator", + "val_addr", valAddr.String(), + "delegator_amount", len(delegators), + ) + // Get delegator rewards. + for i := range delegators { + // Get end current period and calculate rewards. + endingPeriod, err := k.distributionKeeper.IncrementValidatorPeriod(ctx, validatorSet[nextValIndex]) + if err != nil { + return nil, err + } + delRewards, err := k.distributionKeeper.CalculateDelegationRewards(ctx, validatorSet[nextValIndex], delegators[i], endingPeriod) + if err != nil { + return nil, err + } + if delegators[i].DelegatorAddress == valAccAddr.String() { + delRewards = delRewards.Add(valCommission.Commission...) + } + delRewardsTruncated, _ := delRewards.TruncateDecimal() + bondDenomAmount := delRewardsTruncated.AmountOf(sdk.DefaultBondDenom).Uint64() + + log.Debug( + ctx, "Calculate delegator rewards", + "val_addr", valAddr.String(), + "del_addr", delegators[i].DelegatorAddress, + "rewards_amount", bondDenomAmount, + "ending_period", endingPeriod, + ) + + if bondDenomAmount >= minPartialWithdrawalAmount { + delEvmAddr, err := k.DelegatorMap.Get(ctx, delegators[i].DelegatorAddress) + if err != nil { + return nil, errors.Wrap(err, "map delegator pubkey to evm address") + } + withdrawals = append(withdrawals, estypes.NewWithdrawal( + uint64(sdk.UnwrapSDKContext(ctx).BlockHeight()), + delegators[i].DelegatorAddress, + valAddr.String(), + delEvmAddr, + bondDenomAmount, + )) + + log.Debug( + ctx, "Found an eligible partial withdrawal", + "val_addr", valAddr.String(), + "del_addr", delegators[i].DelegatorAddress, + "del_evm_addr", delEvmAddr, + "rewards_amount", bondDenomAmount, + ) + } + } + nextValIndex = (nextValIndex + 1) % int64(len(validatorSet)) + } + // Update the nextValidatorSweepIndex. + if err := k.SetNextValidatorSweepIndex( + ctx, + sdk.IntProto{Int: math.NewInt(nextValIndex)}, + ); err != nil { + return nil, err + } + log.Debug( + ctx, "Finish validator sweep for partial withdrawals", + "next_validator_index", nextValIndex, + "partial_withdrawals", len(withdrawals), + ) + + return withdrawals, nil +} + +func (k Keeper) EnqueueEligiblePartialWithdrawal(ctx context.Context, withdrawals []estypes.Withdrawal) error { + for i := range withdrawals { + valAddr, err := sdk.ValAddressFromBech32(withdrawals[i].ValidatorAddress) + if err != nil { + return errors.Wrap(err, "validator address from bech32") + } + + valAccAddr := sdk.AccAddress(valAddr).String() + + // Withdraw delegation rewards. + delAddr := sdk.MustAccAddressFromBech32(withdrawals[i].DelegatorAddress) + delRewards, err := k.distributionKeeper.WithdrawDelegationRewards(ctx, delAddr, valAddr) + if err != nil { + return err + } + + // Withdraw commission if it is a self delegation. + if withdrawals[i].DelegatorAddress == valAccAddr { + commissionRewards, err := k.distributionKeeper.WithdrawValidatorCommission(ctx, valAddr) + if errors.Is(err, dtypes.ErrNoValidatorCommission) { + log.Debug( + ctx, "No validator commission", + "validator_addr", withdrawals[i].ValidatorAddress, + "validator_account_addr", valAccAddr, + ) + } else if err != nil { + return err + } else { + delRewards = delRewards.Add(commissionRewards...) + } + } + curBondDenomAmount := delRewards.AmountOf(sdk.DefaultBondDenom).Uint64() + log.Debug( + ctx, "Withdraw delegator rewards", + "validator_addr", withdrawals[i].ValidatorAddress, + "validator_account_addr", valAccAddr, + "delegator_addr", withdrawals[i].DelegatorAddress, + "amount_calculate", withdrawals[i].Amount, + "amount_withdraw", curBondDenomAmount, + ) + + // Burn tokens from the delegator + err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delAddr, estypes.ModuleName, delRewards) + if err != nil { + return err + } + err = k.bankKeeper.BurnCoins(ctx, estypes.ModuleName, delRewards) + if err != nil { + return err + } + + // Enqueue to the global withdrawal queue. + if err := k.AddWithdrawalToQueue(ctx, estypes.NewWithdrawal( + uint64(sdk.UnwrapSDKContext(ctx).BlockHeight()), + withdrawals[i].DelegatorAddress, withdrawals[i].ValidatorAddress, withdrawals[i].ExecutionAddress, + curBondDenomAmount, + )); err != nil { + return err + } + } + + // set metrics + promutil.EVMStakingQueueDepth.Set(float64(k.WithdrawalQueue.Len(ctx))) + + return nil +} + +func (k Keeper) ProcessWithdraw(ctx context.Context, ev *bindings.IPTokenStakingWithdraw) error { + depositorPubkey, err := k1util.PubKeyBytesToCosmos(ev.DepositorPubkey) + if err != nil { + return errors.Wrap(err, "depositor pubkey to cosmos") + } + + validatorPubkey, err := k1util.PubKeyBytesToCosmos(ev.ValidatorPubkey) + if err != nil { + return errors.Wrap(err, "validator pubkey to cosmos") + } + + depositorAddr := sdk.AccAddress(depositorPubkey.Address().Bytes()) + validatorAddr := sdk.ValAddress(validatorPubkey.Address().Bytes()) + + valEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(validatorPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "validator pubkey to evm address") + } + delEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(depositorPubkey.Bytes()) + if err != nil { + return errors.Wrap(err, "validator pubkey to evm address") + } + + amountCoin, _ := IPTokenToBondCoin(ev.Amount) + + log.Debug(ctx, "Processing EVM staking withdraw", + "del_iliad", depositorAddr.String(), + "val_iliad", validatorAddr.String(), + "del_evm_addr", delEvmAddr.String(), + "val_evm_addr", valEvmAddr.String(), + "amount", ev.Amount.String(), + ) + + if !k.authKeeper.HasAccount(ctx, depositorAddr) { + // TODO: gracefully handle when malicious or uninformed user tries to withdraw from non-existent account + // skip errors.Wrap(err) since err will be nil (since all prev errors were nil to reach this branch) + return errors.New("depositor account not found") + } + + msg := stypes.NewMsgUndelegate(depositorAddr.String(), validatorAddr.String(), amountCoin) + + // Undelegate from the validator (validator existence is checked in ValidateUnbondAmount) + resp, err := skeeper.NewMsgServerImpl(k.stakingKeeper.(*skeeper.Keeper)).Undelegate(ctx, msg) + if err != nil { + return errors.Wrap(err, "undelegate") + } + + log.Info(ctx, "EVM staking withdraw detected, undelegating from validator", + "delegator", depositorAddr.String(), + "validator", validatorAddr.String(), + "amount", resp.Amount.String(), + "completion_time", resp.CompletionTime) + + return nil +} + +func (k Keeper) ParseWithdrawLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingWithdraw, error) { + return k.ipTokenStakingContract.ParseWithdraw(ethlog) +} diff --git a/client/x/evmstaking/keeper/withdrawal_queue.go b/client/x/evmstaking/keeper/withdrawal_queue.go new file mode 100644 index 00000000..690c3e74 --- /dev/null +++ b/client/x/evmstaking/keeper/withdrawal_queue.go @@ -0,0 +1,137 @@ +package keeper + +import ( + "context" + "errors" + + "github.com/ethereum/go-ethereum/common" + etypes "github.com/ethereum/go-ethereum/core/types" + + addcollections "github.com/piplabs/story/client/collections" + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/log" +) + +// AddWithdrawalToQueue inserts a withdrawal into the queue. +func (k Keeper) AddWithdrawalToQueue(ctx context.Context, withdrawal types.Withdrawal) error { + return k.WithdrawalQueue.Enqueue(ctx, withdrawal) +} + +func (k Keeper) DequeueEligibleWithdrawals(ctx context.Context) (withdrawals etypes.Withdrawals, err error) { + maxDequeue, err := k.MaxWithdrawalPerBlock(ctx) + if err != nil { + return nil, err + } + + // front is the unique monotonically increasing index of a withdrawal in the queue. + // It's used as the value in etypes.Withdrawal.Index for later validation purposes, + // when evmengine's msg_server receives withdrawals as part of the execution payload + // and needs to verify that the received withdrawals are in the correct order from + // the front of the queue. + front, err := k.WithdrawalQueue.Front(ctx) + if err != nil { + log.Debug(ctx, "Front", "err", err) + return nil, err + } + + for i := range uint64(maxDequeue) { + withdrawal, err := k.WithdrawalQueue.Dequeue(ctx) + if err != nil { + // Dequeue will return ErrEmptyQueue if the queue is empty + if errors.Is(err, addcollections.ErrEmptyQueue) { + break + } + + return nil, err + } + withdrawals = append(withdrawals, &etypes.Withdrawal{ + Index: front + i, // increment front by i to get the correct index in the loop + Validator: 0, // does not matter for EL + Address: common.HexToAddress(withdrawal.ExecutionAddress), + Amount: withdrawal.Amount, + }) + } + + return withdrawals, nil +} + +func (k Keeper) PeekEligibleWithdrawals(ctx context.Context) (withdrawals etypes.Withdrawals, err error) { + maxDequeue, err := k.MaxWithdrawalPerBlock(ctx) + if err != nil { + return nil, err + } + + if k.WithdrawalQueue.IsEmpty(ctx) { + return withdrawals, nil + } + + // front is the unique monotonically increasing index of a withdrawal in the queue. + // It's used as the value in etypes.Withdrawal.Index for later validation purposes, + // when evmengine's msg_server receives withdrawals as part of the execution payload + // and needs to verify that the received withdrawals are in the correct order from + // the front of the queue. + front, err := k.WithdrawalQueue.Front(ctx) + if err != nil { + return nil, err + } + + for i := range uint64(maxDequeue) { + // NOTE: Get adjusts the provided index by the front index of the queue + withdrawal, err := k.WithdrawalQueue.Get(ctx, i) + if err != nil { + // Get will return ErrOutOfBoundsQueue if the queue is empty + if errors.Is(err, addcollections.ErrOutOfBoundsQueue) { + break + } + + return nil, err + } + withdrawals = append(withdrawals, &etypes.Withdrawal{ + Index: front + i, // increment front by i to get the correct index in the loop + Validator: 0, // does not matter for EL + Address: common.HexToAddress(withdrawal.ExecutionAddress), + Amount: withdrawal.Amount, + }) + } + + return withdrawals, nil +} + +// GetAllWithdrawals gets the set of all withdrawals with no limits. +func (k Keeper) GetAllWithdrawals(ctx context.Context) (withdrawals []types.Withdrawal, err error) { + iterator, err := k.WithdrawalQueue.Iterate(ctx) + if err != nil { + return nil, err + } + + wdrKvs, err := iterator.KeyValues() + if err != nil { + return nil, err + } + + for _, withdrawal := range wdrKvs { + withdrawals = append(withdrawals, withdrawal.Value) + } + + return withdrawals, nil +} + +// GetWithdrawals returns at max the requested amount of withdrawals. +func (k Keeper) GetWithdrawals(ctx context.Context, maxRetrieve uint32) (withdrawals []types.Withdrawal, err error) { + iterator, err := k.WithdrawalQueue.Iterate(ctx) + if err != nil { + return nil, err + } + + i := 0 + for ; iterator.Valid() && i < int(maxRetrieve); iterator.Next() { + withdrawal, err := iterator.Value() + if err != nil { + return nil, err + } + withdrawals = append(withdrawals, withdrawal) + i++ + } + + return withdrawals[:i], nil // trim if the array length < maxRetrieve +} diff --git a/client/x/evmstaking/module/depinject.go b/client/x/evmstaking/module/depinject.go new file mode 100644 index 00000000..ff9248e3 --- /dev/null +++ b/client/x/evmstaking/module/depinject.go @@ -0,0 +1,73 @@ +package module + +import ( + "cosmossdk.io/core/appmodule" + "cosmossdk.io/core/store" + "cosmossdk.io/depinject" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/runtime" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/piplabs/story/client/x/evmstaking/keeper" + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/ethclient" +) + +//nolint:gochecknoinits // depinject +func init() { + appmodule.Register( + &Module{}, + appmodule.Provide( + ProvideModule, + ), + ) +} + +type ModuleInputs struct { + depinject.In + + Config *Module + ValidatorAddressCodec runtime.ValidatorAddressCodec + EthClient ethclient.Client + AccountKeeper types.AccountKeeper + BankKeeper types.BankKeeper + SlashingKeeper types.SlashingKeeper + StakingKeeper types.StakingKeeper + DistributionKeeper types.DistributionKeeper + Cdc codec.Codec + StoreService store.KVStoreService +} + +type ModuleOutputs struct { + depinject.Out + + Keeper *keeper.Keeper + Module appmodule.AppModule +} + +func ProvideModule(in ModuleInputs) ModuleOutputs { + // default to governance authority if not provided + authority := authtypes.NewModuleAddress(govtypes.ModuleName) + if in.Config.GetAuthority() != "" { + authority = authtypes.NewModuleAddressOrBech32Address(in.Config.GetAuthority()) + } + + k := keeper.NewKeeper( + in.Cdc, + in.StoreService, + in.AccountKeeper, + in.BankKeeper, + in.SlashingKeeper, + in.StakingKeeper, + in.DistributionKeeper, + authority.String(), + in.EthClient, + in.ValidatorAddressCodec, + ) + + m := NewAppModule(in.Cdc, k, in.AccountKeeper, in.BankKeeper, in.SlashingKeeper, &in.StakingKeeper) + + return ModuleOutputs{Keeper: k, Module: m} +} diff --git a/client/x/evmstaking/module/module.go b/client/x/evmstaking/module/module.go new file mode 100644 index 00000000..699f9fa3 --- /dev/null +++ b/client/x/evmstaking/module/module.go @@ -0,0 +1,155 @@ +package module + +import ( + "context" + "encoding/json" + "fmt" + + "cosmossdk.io/core/appmodule" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + + "github.com/piplabs/story/client/x/evmstaking/keeper" + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/lib/errors" +) + +var ( + _ module.AppModuleBasic = AppModule{} + _ module.HasName = AppModule{} + _ module.HasGenesis = AppModule{} + _ module.HasServices = AppModule{} + _ module.HasABCIEndBlock = AppModule{} + + _ appmodule.AppModule = AppModule{} +) + +// ConsensusVersion defines the current module consensus version. +const ConsensusVersion = 1 + +// ---------------------------------------------------------------------------- +// AppModuleBasic +// ---------------------------------------------------------------------------- + +// AppModuleBasic implements the AppModuleBasic interface that defines the +// independent methods a Cosmos SDK module needs to implement. +type AppModuleBasic struct { + cdc codec.BinaryCodec +} + +func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { + return AppModuleBasic{cdc: cdc} +} + +// Name returns the name of the module as a string. +func (AppModuleBasic) Name() string { + return types.ModuleName +} + +// RegisterLegacyAminoCodec registers the amino codec for the module, which is used +// to marshal and unmarshal structs to/from []byte in order to persist them in the module's KVStore. +func (AppModuleBasic) RegisterLegacyAminoCodec(*codec.LegacyAmino) {} + +// RegisterInterfaces registers a module's interface types and their concrete implementations as proto.Message. +func (AppModuleBasic) RegisterInterfaces(reg codectypes.InterfaceRegistry) { + types.RegisterInterfaces(reg) +} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(client.Context, *runtime.ServeMux) {} + +// ---------------------------------------------------------------------------- +// AppModule +// ---------------------------------------------------------------------------- + +// AppModule implements the AppModule interface that defines the inter-dependent methods that modules need to implement. + +type AppModule struct { + AppModuleBasic + + keeper *keeper.Keeper + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + slashingKeeper types.SlashingKeeper + stakingKeeper *types.StakingKeeper +} + +// NewAppModule creates a new AppModule object. +func NewAppModule( + cdc codec.Codec, + keeper *keeper.Keeper, + accountKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, + slashingKeeper types.SlashingKeeper, + stakingKeeper *types.StakingKeeper, +) AppModule { + return AppModule{ + AppModuleBasic: NewAppModuleBasic(cdc), + keeper: keeper, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + slashingKeeper: slashingKeeper, + stakingKeeper: stakingKeeper, + } +} + +// IsOnePerModuleType implements the depinject.OnePerModuleType interface. +func (AppModule) IsOnePerModuleType() {} + +// IsAppModule implements the appmodule.AppModule interface. +func (AppModule) IsAppModule() {} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion } + +// DefaultGenesis returns default genesis state as raw bytes for the module. +func (AppModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(types.DefaultGenesisState()) +} + +// RegisterServices registers a gRPC query service to respond to the module-specific gRPC queries. +// +// func (am AppModule) RegisterServices(registrar grpc.ServiceRegistrar) error { +// types.RegisterQueryServer(registrar, am.keeper) +// return nil +// } +func (am AppModule) RegisterServices(cfg module.Configurator) { + types.RegisterQueryServer(cfg.QueryServer(), am.keeper) + types.RegisterMsgServiceServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) +} + +// ValidateGenesis performs genesis state validation for the module. +func (am AppModule) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { + var gs types.GenesisState + if err := cdc.UnmarshalJSON(bz, &gs); err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to unmarshal %s genesis state", types.ModuleName)) + } + + return am.keeper.ValidateGenesis(&gs) +} + +// InitGenesis performs genesis initialization for the module. +// It returns no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) { + var gs types.GenesisState + cdc.MustUnmarshalJSON(data, &gs) + + if err := am.keeper.InitGenesis(ctx, &gs); err != nil { + panic(fmt.Sprintf("failed to initialize %s genesis state: %v", types.ModuleName, err)) + } +} + +// ExportGenesis returns the exported genesis state as raw bytes for the module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(am.keeper.ExportGenesis(ctx)) +} + +func (am AppModule) EndBlock(ctx context.Context) ([]abci.ValidatorUpdate, error) { + return am.keeper.EndBlock(ctx) +} diff --git a/client/x/evmstaking/module/module.proto b/client/x/evmstaking/module/module.proto new file mode 100644 index 00000000..5b484caa --- /dev/null +++ b/client/x/evmstaking/module/module.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; +package client.x.evmstaking.module; + +import "cosmos/app/v1alpha1/module.proto"; + +option go_package = "client/x/evmstaking/module"; + +// ModuleName is the config object for the evmstaking module. +message Module { + option (cosmos.app.v1alpha1.module) = { + go_import: "github.com/piplabs/story/client/x/evmstaking" + }; + + // authority defines the custom module authority. If not set, defaults to the governance module. + string authority = 1; +} diff --git a/client/x/evmstaking/module/module.pulsar.go b/client/x/evmstaking/module/module.pulsar.go new file mode 100644 index 00000000..d8a8ec63 --- /dev/null +++ b/client/x/evmstaking/module/module.pulsar.go @@ -0,0 +1,580 @@ +// Code generated by protoc-gen-go-pulsar. DO NOT EDIT. +package module + +import ( + _ "cosmossdk.io/api/cosmos/app/v1alpha1" + fmt "fmt" + runtime "github.com/cosmos/cosmos-proto/runtime" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoiface "google.golang.org/protobuf/runtime/protoiface" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + io "io" + reflect "reflect" + sync "sync" +) + +var ( + md_Module protoreflect.MessageDescriptor + fd_Module_authority protoreflect.FieldDescriptor +) + +func init() { + file_client_x_evmstaking_module_module_proto_init() + md_Module = File_client_x_evmstaking_module_module_proto.Messages().ByName("Module") + fd_Module_authority = md_Module.Fields().ByName("authority") +} + +var _ protoreflect.Message = (*fastReflection_Module)(nil) + +type fastReflection_Module Module + +func (x *Module) ProtoReflect() protoreflect.Message { + return (*fastReflection_Module)(x) +} + +func (x *Module) slowProtoReflect() protoreflect.Message { + mi := &file_client_x_evmstaking_module_module_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_Module_messageType fastReflection_Module_messageType +var _ protoreflect.MessageType = fastReflection_Module_messageType{} + +type fastReflection_Module_messageType struct{} + +func (x fastReflection_Module_messageType) Zero() protoreflect.Message { + return (*fastReflection_Module)(nil) +} +func (x fastReflection_Module_messageType) New() protoreflect.Message { + return new(fastReflection_Module) +} +func (x fastReflection_Module_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_Module +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_Module) Descriptor() protoreflect.MessageDescriptor { + return md_Module +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_Module) Type() protoreflect.MessageType { + return _fastReflection_Module_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_Module) New() protoreflect.Message { + return new(fastReflection_Module) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_Module) Interface() protoreflect.ProtoMessage { + return (*Module)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_Module) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.Authority != "" { + value := protoreflect.ValueOfString(x.Authority) + if !f(fd_Module_authority, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_Module) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "client.x.evmstaking.module.Module.authority": + return x.Authority != "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmstaking.module.Module")) + } + panic(fmt.Errorf("message client.x.evmstaking.module.Module does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "client.x.evmstaking.module.Module.authority": + x.Authority = "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmstaking.module.Module")) + } + panic(fmt.Errorf("message client.x.evmstaking.module.Module does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_Module) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "client.x.evmstaking.module.Module.authority": + value := x.Authority + return protoreflect.ValueOfString(value) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmstaking.module.Module")) + } + panic(fmt.Errorf("message client.x.evmstaking.module.Module does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "client.x.evmstaking.module.Module.authority": + x.Authority = value.Interface().(string) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmstaking.module.Module")) + } + panic(fmt.Errorf("message client.x.evmstaking.module.Module does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "client.x.evmstaking.module.Module.authority": + panic(fmt.Errorf("field authority of message client.x.evmstaking.module.Module is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmstaking.module.Module")) + } + panic(fmt.Errorf("message client.x.evmstaking.module.Module does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_Module) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "client.x.evmstaking.module.Module.authority": + return protoreflect.ValueOfString("") + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: client.x.evmstaking.module.Module")) + } + panic(fmt.Errorf("message client.x.evmstaking.module.Module does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_Module) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in client.x.evmstaking.module.Module", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_Module) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_Module) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_Module) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*Module) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + l = len(x.Authority) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*Module) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.Authority) > 0 { + i -= len(x.Authority) + copy(dAtA[i:], x.Authority) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Authority))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*Module) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: Module: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: Module: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.0 +// protoc (unknown) +// source: client/x/evmstaking/module/module.proto + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ModuleName is the config object for the evmstaking module. +type Module struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // authority defines the custom module authority. If not set, defaults to the governance module. + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` +} + +func (x *Module) Reset() { + *x = Module{} + if protoimpl.UnsafeEnabled { + mi := &file_client_x_evmstaking_module_module_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Module) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Module) ProtoMessage() {} + +// Deprecated: Use Module.ProtoReflect.Descriptor instead. +func (*Module) Descriptor() ([]byte, []int) { + return file_client_x_evmstaking_module_module_proto_rawDescGZIP(), []int{0} +} + +func (x *Module) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +var File_client_x_evmstaking_module_module_proto protoreflect.FileDescriptor + +var file_client_x_evmstaking_module_module_proto_rawDesc = []byte{ + 0x0a, 0x27, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, 0x76, 0x6d, 0x73, 0x74, + 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1a, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x2e, 0x78, 0x2e, 0x65, 0x76, 0x6d, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x6d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x1a, 0x20, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x70, + 0x70, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x62, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x3a, + 0x3a, 0xba, 0xc0, 0x96, 0xda, 0x01, 0x34, 0x0a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x69, 0x6c, 0x69, 0x61, 0x64, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, + 0x2f, 0x65, 0x76, 0x6d, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x42, 0xe6, 0x01, 0x0a, 0x1e, + 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x78, 0x2e, 0x65, 0x76, 0x6d, + 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x42, 0x0b, + 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x78, 0x2f, 0x65, 0x76, 0x6d, 0x73, 0x74, 0x61, 0x6b, + 0x69, 0x6e, 0x67, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0xa2, 0x02, 0x04, 0x43, 0x58, 0x45, + 0x4d, 0xaa, 0x02, 0x1a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x58, 0x2e, 0x45, 0x76, 0x6d, + 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0xca, 0x02, + 0x1a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5c, 0x58, 0x5c, 0x45, 0x76, 0x6d, 0x73, 0x74, 0x61, + 0x6b, 0x69, 0x6e, 0x67, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0xe2, 0x02, 0x26, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5c, 0x58, 0x5c, 0x45, 0x76, 0x6d, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, + 0x67, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x3a, 0x58, + 0x3a, 0x3a, 0x45, 0x76, 0x6d, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x4d, 0x6f, + 0x64, 0x75, 0x6c, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_client_x_evmstaking_module_module_proto_rawDescOnce sync.Once + file_client_x_evmstaking_module_module_proto_rawDescData = file_client_x_evmstaking_module_module_proto_rawDesc +) + +func file_client_x_evmstaking_module_module_proto_rawDescGZIP() []byte { + file_client_x_evmstaking_module_module_proto_rawDescOnce.Do(func() { + file_client_x_evmstaking_module_module_proto_rawDescData = protoimpl.X.CompressGZIP(file_client_x_evmstaking_module_module_proto_rawDescData) + }) + return file_client_x_evmstaking_module_module_proto_rawDescData +} + +var file_client_x_evmstaking_module_module_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_client_x_evmstaking_module_module_proto_goTypes = []interface{}{ + (*Module)(nil), // 0: client.x.evmstaking.module.Module +} +var file_client_x_evmstaking_module_module_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_client_x_evmstaking_module_module_proto_init() } +func file_client_x_evmstaking_module_module_proto_init() { + if File_client_x_evmstaking_module_module_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_client_x_evmstaking_module_module_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Module); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_client_x_evmstaking_module_module_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_client_x_evmstaking_module_module_proto_goTypes, + DependencyIndexes: file_client_x_evmstaking_module_module_proto_depIdxs, + MessageInfos: file_client_x_evmstaking_module_module_proto_msgTypes, + }.Build() + File_client_x_evmstaking_module_module_proto = out.File + file_client_x_evmstaking_module_module_proto_rawDesc = nil + file_client_x_evmstaking_module_module_proto_goTypes = nil + file_client_x_evmstaking_module_module_proto_depIdxs = nil +} diff --git a/client/x/evmstaking/testutil/expected_keepers_mocks.go b/client/x/evmstaking/testutil/expected_keepers_mocks.go new file mode 100644 index 00000000..ab85d653 --- /dev/null +++ b/client/x/evmstaking/testutil/expected_keepers_mocks.go @@ -0,0 +1,739 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client/x/evmstaking/types/expected_keepers.go +// +// Generated by this command: +// +// mockgen -source=client/x/evmstaking/types/expected_keepers.go -package testutil -destination client/x/evmstaking/testutil/expected_keepers_mocks.go +// + +// Package testutil is a generated GoMock package. +package testutil + +import ( + context "context" + reflect "reflect" + time "time" + + address "cosmossdk.io/core/address" + store "cosmossdk.io/core/store" + types "github.com/cometbft/cometbft/abci/types" + types0 "github.com/cosmos/cosmos-sdk/types" + types1 "github.com/cosmos/cosmos-sdk/x/distribution/types" + types2 "github.com/cosmos/cosmos-sdk/x/staking/types" + gomock "go.uber.org/mock/gomock" +) + +// MockAccountKeeper is a mock of AccountKeeper interface. +type MockAccountKeeper struct { + ctrl *gomock.Controller + recorder *MockAccountKeeperMockRecorder +} + +// MockAccountKeeperMockRecorder is the mock recorder for MockAccountKeeper. +type MockAccountKeeperMockRecorder struct { + mock *MockAccountKeeper +} + +// NewMockAccountKeeper creates a new mock instance. +func NewMockAccountKeeper(ctrl *gomock.Controller) *MockAccountKeeper { + mock := &MockAccountKeeper{ctrl: ctrl} + mock.recorder = &MockAccountKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder { + return m.recorder +} + +// AddressCodec mocks base method. +func (m *MockAccountKeeper) AddressCodec() address.Codec { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddressCodec") + ret0, _ := ret[0].(address.Codec) + return ret0 +} + +// AddressCodec indicates an expected call of AddressCodec. +func (mr *MockAccountKeeperMockRecorder) AddressCodec() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddressCodec", reflect.TypeOf((*MockAccountKeeper)(nil).AddressCodec)) +} + +// GetAccount mocks base method. +func (m *MockAccountKeeper) GetAccount(ctx context.Context, addr types0.AccAddress) types0.AccountI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccount", ctx, addr) + ret0, _ := ret[0].(types0.AccountI) + return ret0 +} + +// GetAccount indicates an expected call of GetAccount. +func (mr *MockAccountKeeperMockRecorder) GetAccount(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockAccountKeeper)(nil).GetAccount), ctx, addr) +} + +// GetModuleAccount mocks base method. +func (m *MockAccountKeeper) GetModuleAccount(ctx context.Context, moduleName string) types0.ModuleAccountI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModuleAccount", ctx, moduleName) + ret0, _ := ret[0].(types0.ModuleAccountI) + return ret0 +} + +// GetModuleAccount indicates an expected call of GetModuleAccount. +func (mr *MockAccountKeeperMockRecorder) GetModuleAccount(ctx, moduleName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAccount", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAccount), ctx, moduleName) +} + +// GetModuleAddress mocks base method. +func (m *MockAccountKeeper) GetModuleAddress(moduleName string) types0.AccAddress { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModuleAddress", moduleName) + ret0, _ := ret[0].(types0.AccAddress) + return ret0 +} + +// GetModuleAddress indicates an expected call of GetModuleAddress. +func (mr *MockAccountKeeperMockRecorder) GetModuleAddress(moduleName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAddress", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAddress), moduleName) +} + +// HasAccount mocks base method. +func (m *MockAccountKeeper) HasAccount(ctx context.Context, addr types0.AccAddress) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasAccount", ctx, addr) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasAccount indicates an expected call of HasAccount. +func (mr *MockAccountKeeperMockRecorder) HasAccount(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasAccount", reflect.TypeOf((*MockAccountKeeper)(nil).HasAccount), ctx, addr) +} + +// IterateAccounts mocks base method. +func (m *MockAccountKeeper) IterateAccounts(ctx context.Context, process func(types0.AccountI) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateAccounts", ctx, process) +} + +// IterateAccounts indicates an expected call of IterateAccounts. +func (mr *MockAccountKeeperMockRecorder) IterateAccounts(ctx, process any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateAccounts", reflect.TypeOf((*MockAccountKeeper)(nil).IterateAccounts), ctx, process) +} + +// NewAccountWithAddress mocks base method. +func (m *MockAccountKeeper) NewAccountWithAddress(ctx context.Context, addr types0.AccAddress) types0.AccountI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewAccountWithAddress", ctx, addr) + ret0, _ := ret[0].(types0.AccountI) + return ret0 +} + +// NewAccountWithAddress indicates an expected call of NewAccountWithAddress. +func (mr *MockAccountKeeperMockRecorder) NewAccountWithAddress(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewAccountWithAddress", reflect.TypeOf((*MockAccountKeeper)(nil).NewAccountWithAddress), ctx, addr) +} + +// SetAccount mocks base method. +func (m *MockAccountKeeper) SetAccount(ctx context.Context, acc types0.AccountI) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAccount", ctx, acc) +} + +// SetAccount indicates an expected call of SetAccount. +func (mr *MockAccountKeeperMockRecorder) SetAccount(ctx, acc any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAccount", reflect.TypeOf((*MockAccountKeeper)(nil).SetAccount), ctx, acc) +} + +// SetModuleAccount mocks base method. +func (m *MockAccountKeeper) SetModuleAccount(arg0 context.Context, arg1 types0.ModuleAccountI) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetModuleAccount", arg0, arg1) +} + +// SetModuleAccount indicates an expected call of SetModuleAccount. +func (mr *MockAccountKeeperMockRecorder) SetModuleAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetModuleAccount", reflect.TypeOf((*MockAccountKeeper)(nil).SetModuleAccount), arg0, arg1) +} + +// MockBankKeeper is a mock of BankKeeper interface. +type MockBankKeeper struct { + ctrl *gomock.Controller + recorder *MockBankKeeperMockRecorder +} + +// MockBankKeeperMockRecorder is the mock recorder for MockBankKeeper. +type MockBankKeeperMockRecorder struct { + mock *MockBankKeeper +} + +// NewMockBankKeeper creates a new mock instance. +func NewMockBankKeeper(ctrl *gomock.Controller) *MockBankKeeper { + mock := &MockBankKeeper{ctrl: ctrl} + mock.recorder = &MockBankKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBankKeeper) EXPECT() *MockBankKeeperMockRecorder { + return m.recorder +} + +// BurnCoins mocks base method. +func (m *MockBankKeeper) BurnCoins(ctx context.Context, moduleName string, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BurnCoins", ctx, moduleName, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// BurnCoins indicates an expected call of BurnCoins. +func (mr *MockBankKeeperMockRecorder) BurnCoins(ctx, moduleName, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BurnCoins", reflect.TypeOf((*MockBankKeeper)(nil).BurnCoins), ctx, moduleName, amt) +} + +// DelegateCoinsFromAccountToModule mocks base method. +func (m *MockBankKeeper) DelegateCoinsFromAccountToModule(ctx context.Context, senderAddr types0.AccAddress, recipientModule string, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DelegateCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// DelegateCoinsFromAccountToModule indicates an expected call of DelegateCoinsFromAccountToModule. +func (mr *MockBankKeeperMockRecorder) DelegateCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelegateCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).DelegateCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt) +} + +// GetAllBalances mocks base method. +func (m *MockBankKeeper) GetAllBalances(ctx context.Context, addr types0.AccAddress) types0.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllBalances", ctx, addr) + ret0, _ := ret[0].(types0.Coins) + return ret0 +} + +// GetAllBalances indicates an expected call of GetAllBalances. +func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr) +} + +// GetBalance mocks base method. +func (m *MockBankKeeper) GetBalance(ctx context.Context, addr types0.AccAddress, denom string) types0.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", ctx, addr, denom) + ret0, _ := ret[0].(types0.Coin) + return ret0 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBankKeeperMockRecorder) GetBalance(ctx, addr, denom any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBankKeeper)(nil).GetBalance), ctx, addr, denom) +} + +// GetSupply mocks base method. +func (m *MockBankKeeper) GetSupply(ctx context.Context, denom string) types0.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSupply", ctx, denom) + ret0, _ := ret[0].(types0.Coin) + return ret0 +} + +// GetSupply indicates an expected call of GetSupply. +func (mr *MockBankKeeperMockRecorder) GetSupply(ctx, denom any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSupply", reflect.TypeOf((*MockBankKeeper)(nil).GetSupply), ctx, denom) +} + +// LockedCoins mocks base method. +func (m *MockBankKeeper) LockedCoins(ctx context.Context, addr types0.AccAddress) types0.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LockedCoins", ctx, addr) + ret0, _ := ret[0].(types0.Coins) + return ret0 +} + +// LockedCoins indicates an expected call of LockedCoins. +func (mr *MockBankKeeperMockRecorder) LockedCoins(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockedCoins", reflect.TypeOf((*MockBankKeeper)(nil).LockedCoins), ctx, addr) +} + +// MintCoins mocks base method. +func (m *MockBankKeeper) MintCoins(ctx context.Context, moduleName string, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MintCoins", ctx, moduleName, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// MintCoins indicates an expected call of MintCoins. +func (mr *MockBankKeeperMockRecorder) MintCoins(ctx, moduleName, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintCoins", reflect.TypeOf((*MockBankKeeper)(nil).MintCoins), ctx, moduleName, amt) +} + +// SendCoinsFromAccountToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx context.Context, senderAddr types0.AccAddress, recipientModule string, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt) +} + +// SendCoinsFromModuleToAccount mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr types0.AccAddress, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToAccount", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToAccount indicates an expected call of SendCoinsFromModuleToAccount. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) +} + +// SendCoinsFromModuleToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, senderPool, recipientPool string, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToModule", ctx, senderPool, recipientPool, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToModule indicates an expected call of SendCoinsFromModuleToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderPool, recipientPool, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToModule), ctx, senderPool, recipientPool, amt) +} + +// SpendableCoins mocks base method. +func (m *MockBankKeeper) SpendableCoins(ctx context.Context, addr types0.AccAddress) types0.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SpendableCoins", ctx, addr) + ret0, _ := ret[0].(types0.Coins) + return ret0 +} + +// SpendableCoins indicates an expected call of SpendableCoins. +func (mr *MockBankKeeperMockRecorder) SpendableCoins(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpendableCoins", reflect.TypeOf((*MockBankKeeper)(nil).SpendableCoins), ctx, addr) +} + +// UndelegateCoinsFromModuleToAccount mocks base method. +func (m *MockBankKeeper) UndelegateCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr types0.AccAddress, amt types0.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UndelegateCoinsFromModuleToAccount", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// UndelegateCoinsFromModuleToAccount indicates an expected call of UndelegateCoinsFromModuleToAccount. +func (mr *MockBankKeeperMockRecorder) UndelegateCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndelegateCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).UndelegateCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) +} + +// MockStakingKeeper is a mock of StakingKeeper interface. +type MockStakingKeeper struct { + ctrl *gomock.Controller + recorder *MockStakingKeeperMockRecorder +} + +// MockStakingKeeperMockRecorder is the mock recorder for MockStakingKeeper. +type MockStakingKeeperMockRecorder struct { + mock *MockStakingKeeper +} + +// NewMockStakingKeeper creates a new mock instance. +func NewMockStakingKeeper(ctrl *gomock.Controller) *MockStakingKeeper { + mock := &MockStakingKeeper{ctrl: ctrl} + mock.recorder = &MockStakingKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStakingKeeper) EXPECT() *MockStakingKeeperMockRecorder { + return m.recorder +} + +// BondDenom mocks base method. +func (m *MockStakingKeeper) BondDenom(ctx context.Context) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BondDenom", ctx) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BondDenom indicates an expected call of BondDenom. +func (mr *MockStakingKeeperMockRecorder) BondDenom(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BondDenom", reflect.TypeOf((*MockStakingKeeper)(nil).BondDenom), ctx) +} + +// CompleteRedelegation mocks base method. +func (m *MockStakingKeeper) CompleteRedelegation(ctx context.Context, delAddr types0.AccAddress, valSrcAddr, valDstAddr types0.ValAddress) (types0.Coins, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CompleteRedelegation", ctx, delAddr, valSrcAddr, valDstAddr) + ret0, _ := ret[0].(types0.Coins) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CompleteRedelegation indicates an expected call of CompleteRedelegation. +func (mr *MockStakingKeeperMockRecorder) CompleteRedelegation(ctx, delAddr, valSrcAddr, valDstAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteRedelegation", reflect.TypeOf((*MockStakingKeeper)(nil).CompleteRedelegation), ctx, delAddr, valSrcAddr, valDstAddr) +} + +// DeleteUnbondingIndex mocks base method. +func (m *MockStakingKeeper) DeleteUnbondingIndex(ctx context.Context, id uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUnbondingIndex", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUnbondingIndex indicates an expected call of DeleteUnbondingIndex. +func (mr *MockStakingKeeperMockRecorder) DeleteUnbondingIndex(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUnbondingIndex", reflect.TypeOf((*MockStakingKeeper)(nil).DeleteUnbondingIndex), ctx, id) +} + +// DequeueAllMatureRedelegationQueue mocks base method. +func (m *MockStakingKeeper) DequeueAllMatureRedelegationQueue(ctx context.Context, currTime time.Time) ([]types2.DVVTriplet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DequeueAllMatureRedelegationQueue", ctx, currTime) + ret0, _ := ret[0].([]types2.DVVTriplet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DequeueAllMatureRedelegationQueue indicates an expected call of DequeueAllMatureRedelegationQueue. +func (mr *MockStakingKeeperMockRecorder) DequeueAllMatureRedelegationQueue(ctx, currTime any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DequeueAllMatureRedelegationQueue", reflect.TypeOf((*MockStakingKeeper)(nil).DequeueAllMatureRedelegationQueue), ctx, currTime) +} + +// EndBlocker mocks base method. +func (m *MockStakingKeeper) EndBlocker(ctx context.Context) ([]types.ValidatorUpdate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EndBlocker", ctx) + ret0, _ := ret[0].([]types.ValidatorUpdate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EndBlocker indicates an expected call of EndBlocker. +func (mr *MockStakingKeeperMockRecorder) EndBlocker(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndBlocker", reflect.TypeOf((*MockStakingKeeper)(nil).EndBlocker), ctx) +} + +// GetAllDelegations mocks base method. +func (m *MockStakingKeeper) GetAllDelegations(ctx context.Context) ([]types2.Delegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllDelegations", ctx) + ret0, _ := ret[0].([]types2.Delegation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllDelegations indicates an expected call of GetAllDelegations. +func (mr *MockStakingKeeperMockRecorder) GetAllDelegations(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllDelegations", reflect.TypeOf((*MockStakingKeeper)(nil).GetAllDelegations), ctx) +} + +// GetAllValidators mocks base method. +func (m *MockStakingKeeper) GetAllValidators(ctx context.Context) ([]types2.Validator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllValidators", ctx) + ret0, _ := ret[0].([]types2.Validator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllValidators indicates an expected call of GetAllValidators. +func (mr *MockStakingKeeperMockRecorder) GetAllValidators(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllValidators", reflect.TypeOf((*MockStakingKeeper)(nil).GetAllValidators), ctx) +} + +// GetUnbondingDelegation mocks base method. +func (m *MockStakingKeeper) GetUnbondingDelegation(ctx context.Context, delAddr types0.AccAddress, valAddr types0.ValAddress) (types2.UnbondingDelegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnbondingDelegation", ctx, delAddr, valAddr) + ret0, _ := ret[0].(types2.UnbondingDelegation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUnbondingDelegation indicates an expected call of GetUnbondingDelegation. +func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegation(ctx, delAddr, valAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingDelegation", reflect.TypeOf((*MockStakingKeeper)(nil).GetUnbondingDelegation), ctx, delAddr, valAddr) +} + +// GetUnbondingDelegations mocks base method. +func (m *MockStakingKeeper) GetUnbondingDelegations(ctx context.Context, delegator types0.AccAddress, maxRetrieve uint16) ([]types2.UnbondingDelegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnbondingDelegations", ctx, delegator, maxRetrieve) + ret0, _ := ret[0].([]types2.UnbondingDelegation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUnbondingDelegations indicates an expected call of GetUnbondingDelegations. +func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegations(ctx, delegator, maxRetrieve any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingDelegations", reflect.TypeOf((*MockStakingKeeper)(nil).GetUnbondingDelegations), ctx, delegator, maxRetrieve) +} + +// GetUnbondingDelegationsFromValidator mocks base method. +func (m *MockStakingKeeper) GetUnbondingDelegationsFromValidator(ctx context.Context, valAddr types0.ValAddress) ([]types2.UnbondingDelegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnbondingDelegationsFromValidator", ctx, valAddr) + ret0, _ := ret[0].([]types2.UnbondingDelegation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUnbondingDelegationsFromValidator indicates an expected call of GetUnbondingDelegationsFromValidator. +func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegationsFromValidator(ctx, valAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingDelegationsFromValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetUnbondingDelegationsFromValidator), ctx, valAddr) +} + +// GetValidator mocks base method. +func (m *MockStakingKeeper) GetValidator(ctx context.Context, addr types0.ValAddress) (types2.Validator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidator", ctx, addr) + ret0, _ := ret[0].(types2.Validator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValidator indicates an expected call of GetValidator. +func (mr *MockStakingKeeperMockRecorder) GetValidator(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidator), ctx, addr) +} + +// GetValidatorDelegations mocks base method. +func (m *MockStakingKeeper) GetValidatorDelegations(ctx context.Context, valAddr types0.ValAddress) ([]types2.Delegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorDelegations", ctx, valAddr) + ret0, _ := ret[0].([]types2.Delegation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValidatorDelegations indicates an expected call of GetValidatorDelegations. +func (mr *MockStakingKeeperMockRecorder) GetValidatorDelegations(ctx, valAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorDelegations", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidatorDelegations), ctx, valAddr) +} + +// UBDQueueIterator mocks base method. +func (m *MockStakingKeeper) UBDQueueIterator(ctx context.Context, endTime time.Time) (store.Iterator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UBDQueueIterator", ctx, endTime) + ret0, _ := ret[0].(store.Iterator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UBDQueueIterator indicates an expected call of UBDQueueIterator. +func (mr *MockStakingKeeperMockRecorder) UBDQueueIterator(ctx, endTime any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UBDQueueIterator", reflect.TypeOf((*MockStakingKeeper)(nil).UBDQueueIterator), ctx, endTime) +} + +// ValidatorAddressCodec mocks base method. +func (m *MockStakingKeeper) ValidatorAddressCodec() address.Codec { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidatorAddressCodec") + ret0, _ := ret[0].(address.Codec) + return ret0 +} + +// ValidatorAddressCodec indicates an expected call of ValidatorAddressCodec. +func (mr *MockStakingKeeperMockRecorder) ValidatorAddressCodec() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatorAddressCodec", reflect.TypeOf((*MockStakingKeeper)(nil).ValidatorAddressCodec)) +} + +// MockSlashingKeeper is a mock of SlashingKeeper interface. +type MockSlashingKeeper struct { + ctrl *gomock.Controller + recorder *MockSlashingKeeperMockRecorder +} + +// MockSlashingKeeperMockRecorder is the mock recorder for MockSlashingKeeper. +type MockSlashingKeeperMockRecorder struct { + mock *MockSlashingKeeper +} + +// NewMockSlashingKeeper creates a new mock instance. +func NewMockSlashingKeeper(ctrl *gomock.Controller) *MockSlashingKeeper { + mock := &MockSlashingKeeper{ctrl: ctrl} + mock.recorder = &MockSlashingKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSlashingKeeper) EXPECT() *MockSlashingKeeperMockRecorder { + return m.recorder +} + +// Unjail mocks base method. +func (m *MockSlashingKeeper) Unjail(ctx context.Context, validatorAddr types0.ValAddress) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unjail", ctx, validatorAddr) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unjail indicates an expected call of Unjail. +func (mr *MockSlashingKeeperMockRecorder) Unjail(ctx, validatorAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unjail", reflect.TypeOf((*MockSlashingKeeper)(nil).Unjail), ctx, validatorAddr) +} + +// MockDistributionKeeper is a mock of DistributionKeeper interface. +type MockDistributionKeeper struct { + ctrl *gomock.Controller + recorder *MockDistributionKeeperMockRecorder +} + +// MockDistributionKeeperMockRecorder is the mock recorder for MockDistributionKeeper. +type MockDistributionKeeperMockRecorder struct { + mock *MockDistributionKeeper +} + +// NewMockDistributionKeeper creates a new mock instance. +func NewMockDistributionKeeper(ctrl *gomock.Controller) *MockDistributionKeeper { + mock := &MockDistributionKeeper{ctrl: ctrl} + mock.recorder = &MockDistributionKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDistributionKeeper) EXPECT() *MockDistributionKeeperMockRecorder { + return m.recorder +} + +// CalculateDelegationRewards mocks base method. +func (m *MockDistributionKeeper) CalculateDelegationRewards(ctx context.Context, val types2.ValidatorI, del types2.DelegationI, endingPeriod uint64) (types0.DecCoins, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CalculateDelegationRewards", ctx, val, del, endingPeriod) + ret0, _ := ret[0].(types0.DecCoins) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CalculateDelegationRewards indicates an expected call of CalculateDelegationRewards. +func (mr *MockDistributionKeeperMockRecorder) CalculateDelegationRewards(ctx, val, del, endingPeriod any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CalculateDelegationRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).CalculateDelegationRewards), ctx, val, del, endingPeriod) +} + +// GetValidatorAccumulatedCommission mocks base method. +func (m *MockDistributionKeeper) GetValidatorAccumulatedCommission(ctx context.Context, val types0.ValAddress) (types1.ValidatorAccumulatedCommission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorAccumulatedCommission", ctx, val) + ret0, _ := ret[0].(types1.ValidatorAccumulatedCommission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValidatorAccumulatedCommission indicates an expected call of GetValidatorAccumulatedCommission. +func (mr *MockDistributionKeeperMockRecorder) GetValidatorAccumulatedCommission(ctx, val any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorAccumulatedCommission", reflect.TypeOf((*MockDistributionKeeper)(nil).GetValidatorAccumulatedCommission), ctx, val) +} + +// GetValidatorCurrentRewards mocks base method. +func (m *MockDistributionKeeper) GetValidatorCurrentRewards(ctx context.Context, val types0.ValAddress) (types1.ValidatorCurrentRewards, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorCurrentRewards", ctx, val) + ret0, _ := ret[0].(types1.ValidatorCurrentRewards) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValidatorCurrentRewards indicates an expected call of GetValidatorCurrentRewards. +func (mr *MockDistributionKeeperMockRecorder) GetValidatorCurrentRewards(ctx, val any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorCurrentRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).GetValidatorCurrentRewards), ctx, val) +} + +// IncrementValidatorPeriod mocks base method. +func (m *MockDistributionKeeper) IncrementValidatorPeriod(ctx context.Context, val types2.ValidatorI) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IncrementValidatorPeriod", ctx, val) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IncrementValidatorPeriod indicates an expected call of IncrementValidatorPeriod. +func (mr *MockDistributionKeeperMockRecorder) IncrementValidatorPeriod(ctx, val any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementValidatorPeriod", reflect.TypeOf((*MockDistributionKeeper)(nil).IncrementValidatorPeriod), ctx, val) +} + +// WithdrawDelegationRewards mocks base method. +func (m *MockDistributionKeeper) WithdrawDelegationRewards(ctx context.Context, delAddr types0.AccAddress, valAddr types0.ValAddress) (types0.Coins, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithdrawDelegationRewards", ctx, delAddr, valAddr) + ret0, _ := ret[0].(types0.Coins) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WithdrawDelegationRewards indicates an expected call of WithdrawDelegationRewards. +func (mr *MockDistributionKeeperMockRecorder) WithdrawDelegationRewards(ctx, delAddr, valAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithdrawDelegationRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).WithdrawDelegationRewards), ctx, delAddr, valAddr) +} + +// WithdrawValidatorCommission mocks base method. +func (m *MockDistributionKeeper) WithdrawValidatorCommission(ctx context.Context, valAddr types0.ValAddress) (types0.Coins, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithdrawValidatorCommission", ctx, valAddr) + ret0, _ := ret[0].(types0.Coins) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WithdrawValidatorCommission indicates an expected call of WithdrawValidatorCommission. +func (mr *MockDistributionKeeperMockRecorder) WithdrawValidatorCommission(ctx, valAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithdrawValidatorCommission", reflect.TypeOf((*MockDistributionKeeper)(nil).WithdrawValidatorCommission), ctx, valAddr) +} diff --git a/client/x/evmstaking/types/codec.go b/client/x/evmstaking/types/codec.go new file mode 100644 index 00000000..a27d1231 --- /dev/null +++ b/client/x/evmstaking/types/codec.go @@ -0,0 +1,23 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" +) + +func RegisterCodec(_ *codec.LegacyAmino) { + // cdc.RegisterConcrete(&MsgAddWithdrawal{}, "evmstaking/MsgAddWithdrawal", nil) + // cdc.RegisterConcrete(&MsgRemoveWithdrawal{}, "evmstaking/MsgRemoveWithdrawal", nil) +} + +// RegisterInterfaces registers the x/staking interfaces types with the interface registry. +func RegisterInterfaces(registrar cdctypes.InterfaceRegistry) { + registrar.RegisterImplementations((*sdk.Msg)(nil), + &MsgAddWithdrawal{}, + &MsgRemoveWithdrawal{}, + ) + + msgservice.RegisterMsgServiceDesc(registrar, &_MsgService_serviceDesc) +} diff --git a/client/x/evmstaking/types/events.go b/client/x/evmstaking/types/events.go new file mode 100644 index 00000000..a740e8d4 --- /dev/null +++ b/client/x/evmstaking/types/events.go @@ -0,0 +1,12 @@ +package types + +// evmstaking module event types. +const ( + EventTypeAddWithdrawal = "add_withdrawal" + EventTypeRemoveWithdrawal = "remove_withdrawal" + + AttributeKeyValidator = "validator" + AttributeKeyDelegator = "delegator" + AttributeKeyExecutionAddress = "execution_address" + AttributeKeyCreationHeight = "creation_height" +) diff --git a/client/x/evmstaking/types/evmstaking.pb.go b/client/x/evmstaking/types/evmstaking.pb.go new file mode 100644 index 00000000..e6b5e0c0 --- /dev/null +++ b/client/x/evmstaking/types/evmstaking.pb.go @@ -0,0 +1,504 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmstaking/types/evmstaking.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Withdrawal struct { + CreationHeight uint64 `protobuf:"varint,1,opt,name=creation_height,json=creationHeight,proto3" json:"creation_height,omitempty"` + DelegatorAddress string `protobuf:"bytes,2,opt,name=delegator_address,json=delegatorAddress,proto3" json:"delegator_address,omitempty" yaml:"delegator_address"` + ValidatorAddress string `protobuf:"bytes,3,opt,name=validator_address,json=validatorAddress,proto3" json:"validator_address,omitempty" yaml:"validator_address"` + // TODO: use ethcommon.Address type + ExecutionAddress string `protobuf:"bytes,4,opt,name=execution_address,json=executionAddress,proto3" json:"execution_address,omitempty" yaml:"execution_address"` + Amount uint64 `protobuf:"varint,5,opt,name=amount,proto3" json:"amount,omitempty" yaml:"amount"` +} + +func (m *Withdrawal) Reset() { *m = Withdrawal{} } +func (m *Withdrawal) String() string { return proto.CompactTextString(m) } +func (*Withdrawal) ProtoMessage() {} +func (*Withdrawal) Descriptor() ([]byte, []int) { + return fileDescriptor_185991fb447209d8, []int{0} +} +func (m *Withdrawal) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Withdrawal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Withdrawal.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Withdrawal) XXX_Merge(src proto.Message) { + xxx_messageInfo_Withdrawal.Merge(m, src) +} +func (m *Withdrawal) XXX_Size() int { + return m.Size() +} +func (m *Withdrawal) XXX_DiscardUnknown() { + xxx_messageInfo_Withdrawal.DiscardUnknown(m) +} + +var xxx_messageInfo_Withdrawal proto.InternalMessageInfo + +func init() { + proto.RegisterType((*Withdrawal)(nil), "client.x.evmstaking.types.Withdrawal") +} + +func init() { + proto.RegisterFile("client/x/evmstaking/types/evmstaking.proto", fileDescriptor_185991fb447209d8) +} + +var fileDescriptor_185991fb447209d8 = []byte{ + // 340 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x4a, 0xce, 0xc9, 0x4c, + 0xcd, 0x2b, 0xd1, 0xaf, 0xd0, 0x4f, 0x2d, 0xcb, 0x2d, 0x2e, 0x49, 0xcc, 0xce, 0xcc, 0x4b, 0xd7, + 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0x46, 0x12, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x92, 0x84, + 0xa8, 0xd5, 0xab, 0xd0, 0x43, 0x92, 0x02, 0xab, 0x95, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xab, + 0xd2, 0x07, 0xb1, 0x20, 0x1a, 0xa4, 0x24, 0x93, 0xf3, 0x8b, 0x73, 0xf3, 0x8b, 0xe3, 0x21, 0x12, + 0x10, 0x0e, 0x44, 0x4a, 0x69, 0x0e, 0x33, 0x17, 0x57, 0x78, 0x66, 0x49, 0x46, 0x4a, 0x51, 0x62, + 0x79, 0x62, 0x8e, 0x90, 0x3a, 0x17, 0x7f, 0x72, 0x51, 0x6a, 0x62, 0x49, 0x66, 0x7e, 0x5e, 0x7c, + 0x46, 0x6a, 0x66, 0x7a, 0x46, 0x89, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x4b, 0x10, 0x1f, 0x4c, 0xd8, + 0x03, 0x2c, 0x2a, 0x94, 0xc8, 0x25, 0x98, 0x92, 0x9a, 0x93, 0x9a, 0x9e, 0x58, 0x92, 0x5f, 0x14, + 0x9f, 0x98, 0x92, 0x52, 0x94, 0x5a, 0x5c, 0x2c, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, 0xe9, 0x64, 0xf2, + 0xe9, 0x9e, 0xbc, 0x44, 0x65, 0x62, 0x6e, 0x8e, 0x95, 0x12, 0x86, 0x12, 0xa5, 0x4b, 0x5b, 0x74, + 0x45, 0xa0, 0x0e, 0x70, 0x84, 0x08, 0x05, 0x97, 0x14, 0x65, 0xe6, 0xa5, 0x07, 0x09, 0xc0, 0xd5, + 0x42, 0xc5, 0x85, 0xb2, 0xb8, 0x04, 0xcb, 0x12, 0x73, 0x32, 0x53, 0x50, 0xac, 0x60, 0x06, 0x5b, + 0x61, 0x8b, 0xb0, 0x02, 0x43, 0x09, 0xc8, 0x0a, 0x59, 0xa8, 0x15, 0x61, 0x30, 0x49, 0x34, 0xbb, + 0xca, 0xd0, 0xc4, 0x41, 0xde, 0x49, 0xad, 0x48, 0x4d, 0x2e, 0x05, 0x7b, 0x1c, 0x66, 0x17, 0x0b, + 0xba, 0x77, 0x30, 0x94, 0xe0, 0xf1, 0x0e, 0x5c, 0x2d, 0xcc, 0x0a, 0x4d, 0x2e, 0xb6, 0xc4, 0xdc, + 0xfc, 0xd2, 0xbc, 0x12, 0x09, 0x56, 0x50, 0x88, 0x3a, 0x09, 0x7e, 0xba, 0x27, 0xcf, 0x0b, 0x31, + 0x17, 0x22, 0xae, 0x14, 0x04, 0x55, 0x60, 0xc5, 0xd1, 0xb1, 0x40, 0x9e, 0xe1, 0xc5, 0x02, 0x79, + 0x46, 0x27, 0xe3, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, + 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, 0x6e, 0x3c, 0x96, 0x63, 0x88, 0x92, 0xc4, 0x99, + 0x60, 0x92, 0xd8, 0xc0, 0x51, 0x6b, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x18, 0x6c, 0x63, 0x24, + 0x54, 0x02, 0x00, 0x00, +} + +func (this *Withdrawal) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Withdrawal) + if !ok { + that2, ok := that.(Withdrawal) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.CreationHeight != that1.CreationHeight { + return false + } + if this.DelegatorAddress != that1.DelegatorAddress { + return false + } + if this.ValidatorAddress != that1.ValidatorAddress { + return false + } + if this.ExecutionAddress != that1.ExecutionAddress { + return false + } + if this.Amount != that1.Amount { + return false + } + return true +} +func (m *Withdrawal) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Withdrawal) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Withdrawal) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Amount != 0 { + i = encodeVarintEvmstaking(dAtA, i, uint64(m.Amount)) + i-- + dAtA[i] = 0x28 + } + if len(m.ExecutionAddress) > 0 { + i -= len(m.ExecutionAddress) + copy(dAtA[i:], m.ExecutionAddress) + i = encodeVarintEvmstaking(dAtA, i, uint64(len(m.ExecutionAddress))) + i-- + dAtA[i] = 0x22 + } + if len(m.ValidatorAddress) > 0 { + i -= len(m.ValidatorAddress) + copy(dAtA[i:], m.ValidatorAddress) + i = encodeVarintEvmstaking(dAtA, i, uint64(len(m.ValidatorAddress))) + i-- + dAtA[i] = 0x1a + } + if len(m.DelegatorAddress) > 0 { + i -= len(m.DelegatorAddress) + copy(dAtA[i:], m.DelegatorAddress) + i = encodeVarintEvmstaking(dAtA, i, uint64(len(m.DelegatorAddress))) + i-- + dAtA[i] = 0x12 + } + if m.CreationHeight != 0 { + i = encodeVarintEvmstaking(dAtA, i, uint64(m.CreationHeight)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintEvmstaking(dAtA []byte, offset int, v uint64) int { + offset -= sovEvmstaking(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Withdrawal) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.CreationHeight != 0 { + n += 1 + sovEvmstaking(uint64(m.CreationHeight)) + } + l = len(m.DelegatorAddress) + if l > 0 { + n += 1 + l + sovEvmstaking(uint64(l)) + } + l = len(m.ValidatorAddress) + if l > 0 { + n += 1 + l + sovEvmstaking(uint64(l)) + } + l = len(m.ExecutionAddress) + if l > 0 { + n += 1 + l + sovEvmstaking(uint64(l)) + } + if m.Amount != 0 { + n += 1 + sovEvmstaking(uint64(m.Amount)) + } + return n +} + +func sovEvmstaking(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozEvmstaking(x uint64) (n int) { + return sovEvmstaking(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Withdrawal) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Withdrawal: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Withdrawal: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreationHeight", wireType) + } + m.CreationHeight = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreationHeight |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvmstaking + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvmstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvmstaking + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvmstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ExecutionAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvmstaking + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvmstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ExecutionAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + } + m.Amount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Amount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipEvmstaking(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthEvmstaking + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipEvmstaking(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowEvmstaking + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthEvmstaking + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupEvmstaking + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthEvmstaking + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthEvmstaking = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowEvmstaking = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupEvmstaking = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmstaking/types/evmstaking.proto b/client/x/evmstaking/types/evmstaking.proto new file mode 100644 index 00000000..128bb12a --- /dev/null +++ b/client/x/evmstaking/types/evmstaking.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; +package client.x.evmstaking.types; + +import "gogoproto/gogo.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "client/x/evmstaking/types"; + +message Withdrawal { + option (gogoproto.equal) = true; + option (gogoproto.goproto_getters) = false; + + uint64 creation_height = 1; + string delegator_address = 2 [ + (cosmos_proto.scalar) = "cosmos.AddressString", + (gogoproto.moretags) = "yaml:\"delegator_address\"" + ]; + string validator_address = 3 [ + (cosmos_proto.scalar) = "cosmos.ValidatorAddressString", + (gogoproto.moretags) = "yaml:\"validator_address\"" + ]; + // TODO: use ethcommon.Address type + string execution_address = 4 [ + (cosmos_proto.scalar) = "cosmos.AddressString", + (gogoproto.moretags) = "yaml:\"execution_address\"" + ]; + uint64 amount = 5 [ + // TODO: use custom Int type, need to resolve issue in auto-generated pb.go + // (cosmos_proto.scalar) = "cosmos.Int", + // (gogoproto.customtype) = "cosmossdk.io/math.Int", + // (gogoproto.nullable) = false, + (gogoproto.moretags) = "yaml:\"amount\"" + ]; +} diff --git a/client/x/evmstaking/types/expected_keepers.go b/client/x/evmstaking/types/expected_keepers.go new file mode 100644 index 00000000..e4f5e20d --- /dev/null +++ b/client/x/evmstaking/types/expected_keepers.go @@ -0,0 +1,83 @@ +package types + +import ( + "context" + "time" + + "cosmossdk.io/core/address" + corestore "cosmossdk.io/core/store" + + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// AccountKeeper defines the expected account keeper (noalias). +type AccountKeeper interface { + AddressCodec() address.Codec + HasAccount(ctx context.Context, addr sdk.AccAddress) bool + NewAccountWithAddress(ctx context.Context, addr sdk.AccAddress) sdk.AccountI + SetAccount(ctx context.Context, acc sdk.AccountI) + GetModuleAddress(moduleName string) sdk.AccAddress + // only used for simulation + GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI + IterateAccounts(ctx context.Context, process func(sdk.AccountI) (stop bool)) + GetModuleAccount(ctx context.Context, moduleName string) sdk.ModuleAccountI + SetModuleAccount(ctx context.Context, modAcc sdk.ModuleAccountI) +} + +// BankKeeper defines the expected interface needed to retrieve account balances. +type BankKeeper interface { + SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + UndelegateCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + DelegateCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin + // only used for simulation + GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins + LockedCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins + SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins + GetSupply(ctx context.Context, denom string) sdk.Coin + SendCoinsFromModuleToModule(ctx context.Context, senderPool, recipientPool string, amt sdk.Coins) error +} + +// StakingKeeper defines the expected interface for the staking module. +type StakingKeeper interface { + ValidatorAddressCodec() address.Codec + + GetValidator(ctx context.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, err error) + GetAllValidators(ctx context.Context) (validators []stakingtypes.Validator, err error) + BondDenom(ctx context.Context) (string, error) + + UBDQueueIterator(ctx context.Context, endTime time.Time) (corestore.Iterator, error) + // GetUnbondingDelegation returns a unbonding delegation. + GetUnbondingDelegation(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (ubd stakingtypes.UnbondingDelegation, err error) + DeleteUnbondingIndex(ctx context.Context, id uint64) error + + GetAllDelegations(ctx context.Context) (delegations []stakingtypes.Delegation, err error) + GetValidatorDelegations(ctx context.Context, valAddr sdk.ValAddress) (delegations []stakingtypes.Delegation, err error) + GetUnbondingDelegations(ctx context.Context, delegator sdk.AccAddress, maxRetrieve uint16) (unbondingDelegations []stakingtypes.UnbondingDelegation, err error) + GetUnbondingDelegationsFromValidator(ctx context.Context, valAddr sdk.ValAddress) (ubds []stakingtypes.UnbondingDelegation, err error) + + EndBlocker(ctx context.Context) ([]abci.ValidatorUpdate, error) +} + +// SlashingKeeper defines the expected interface for the slashing module. +type SlashingKeeper interface { + Unjail(ctx context.Context, validatorAddr sdk.ValAddress) error +} + +// DistributionKeeper defines the expected interface needed to calculate validator commission and delegator rewards. +type DistributionKeeper interface { + GetValidatorCurrentRewards(ctx context.Context, val sdk.ValAddress) (rewards distributiontypes.ValidatorCurrentRewards, err error) + GetValidatorAccumulatedCommission(ctx context.Context, val sdk.ValAddress) (commission distributiontypes.ValidatorAccumulatedCommission, err error) + CalculateDelegationRewards(ctx context.Context, val stakingtypes.ValidatorI, del stakingtypes.DelegationI, endingPeriod uint64) (rewards sdk.DecCoins, err error) + + WithdrawDelegationRewards(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error) + WithdrawValidatorCommission(ctx context.Context, valAddr sdk.ValAddress) (sdk.Coins, error) + + IncrementValidatorPeriod(ctx context.Context, val stakingtypes.ValidatorI) (uint64, error) +} diff --git a/client/x/evmstaking/types/genesis.go b/client/x/evmstaking/types/genesis.go new file mode 100644 index 00000000..6f230796 --- /dev/null +++ b/client/x/evmstaking/types/genesis.go @@ -0,0 +1,14 @@ +package types + +func NewGenesisState(params Params) *GenesisState { + return &GenesisState{ + Params: params, + } +} + +// DefaultGenesisState returns the default genesis state. +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + } +} diff --git a/client/x/evmstaking/types/genesis.pb.go b/client/x/evmstaking/types/genesis.pb.go new file mode 100644 index 00000000..6105a7a6 --- /dev/null +++ b/client/x/evmstaking/types/genesis.pb.go @@ -0,0 +1,321 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmstaking/types/genesis.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type GenesisState struct { + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_bf57cf100cbaf4bd, []int{0} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + +func init() { + proto.RegisterType((*GenesisState)(nil), "client.x.evmstaking.types.GenesisState") +} + +func init() { + proto.RegisterFile("client/x/evmstaking/types/genesis.proto", fileDescriptor_bf57cf100cbaf4bd) +} + +var fileDescriptor_bf57cf100cbaf4bd = []byte{ + // 181 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4f, 0xce, 0xc9, 0x4c, + 0xcd, 0x2b, 0xd1, 0xaf, 0xd0, 0x4f, 0x2d, 0xcb, 0x2d, 0x2e, 0x49, 0xcc, 0xce, 0xcc, 0x4b, 0xd7, + 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, + 0xca, 0x2f, 0xc9, 0x17, 0x92, 0x84, 0x28, 0xd4, 0xab, 0xd0, 0x43, 0x28, 0xd4, 0x03, 0x2b, 0x94, + 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xab, 0xd2, 0x07, 0xb1, 0x20, 0x1a, 0xa4, 0xd4, 0x70, 0x9b, + 0x5c, 0x90, 0x58, 0x94, 0x98, 0x0b, 0x35, 0x58, 0xc9, 0x9f, 0x8b, 0xc7, 0x1d, 0x62, 0x53, 0x70, + 0x49, 0x62, 0x49, 0xaa, 0x90, 0x3d, 0x17, 0x1b, 0x44, 0x5e, 0x82, 0x51, 0x81, 0x51, 0x83, 0xdb, + 0x48, 0x51, 0x0f, 0xa7, 0xcd, 0x7a, 0x01, 0x60, 0x85, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, + 0x41, 0xb5, 0x39, 0x19, 0x9f, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, + 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x24, + 0x4e, 0x27, 0x25, 0xb1, 0x81, 0x1d, 0x63, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x1d, 0x1e, 0x57, + 0x0f, 0x10, 0x01, 0x00, 0x00, +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Params.Size() + n += 1 + l + sovGenesis(uint64(l)) + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGenesis(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGenesis + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGenesis + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGenesis + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmstaking/types/genesis.proto b/client/x/evmstaking/types/genesis.proto new file mode 100644 index 00000000..6a7433de --- /dev/null +++ b/client/x/evmstaking/types/genesis.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +package client.x.evmstaking.types; + +import "gogoproto/gogo.proto"; +import "client/x/evmstaking/types/params.proto"; + +option go_package = "client/x/evmstaking/types"; + +message GenesisState { + Params params = 1 [(gogoproto.nullable) = false]; + // TODO: Add withdrawals collections field as ORM if needed +} \ No newline at end of file diff --git a/client/x/evmstaking/types/keys.go b/client/x/evmstaking/types/keys.go new file mode 100644 index 00000000..f453a6d2 --- /dev/null +++ b/client/x/evmstaking/types/keys.go @@ -0,0 +1,22 @@ +package types + +import "cosmossdk.io/collections" + +const ( + // ModuleName is the name of the evmstaking module. + ModuleName = "evmstaking" + + // StoreKey is the string store representation. + StoreKey = ModuleName + + // RouterKey is the msg router key for the evmstaking module. + RouterKey = ModuleName +) + +// KVStore keys. +var ( + ParamsKey = collections.NewPrefix(0) + WithdrawalQueueKey = collections.NewPrefix(1) + DelegatorMapKey = collections.NewPrefix(2) + NextValidatorSweepIndexKey = collections.NewPrefix(3) +) diff --git a/client/x/evmstaking/types/params.go b/client/x/evmstaking/types/params.go new file mode 100644 index 00000000..30191e5c --- /dev/null +++ b/client/x/evmstaking/types/params.go @@ -0,0 +1,99 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + + "github.com/piplabs/story/lib/errors" +) + +// Staking params default values. +const ( + DefaultMaxWithdrawalPerBlock uint32 = 4 + + DefaultMaxSweepPerBlock uint32 = 64 + + DefaultMinPartialWithdrawalAmount uint64 = 600_000 +) + +// NewParams creates a new Params instance. +func NewParams(maxWithdrawalPerBlock uint32, maxSweepPerBlock uint32, minPartialWithdrawalAmount uint64) Params { + return Params{ + MaxWithdrawalPerBlock: maxWithdrawalPerBlock, + MaxSweepPerBlock: maxSweepPerBlock, + MinPartialWithdrawalAmount: minPartialWithdrawalAmount, + } +} + +// DefaultParams returns a default set of parameters. +func DefaultParams() Params { + return NewParams( + DefaultMaxWithdrawalPerBlock, + DefaultMaxSweepPerBlock, + DefaultMinPartialWithdrawalAmount, + ) +} + +// unmarshal the current params value from store key or panic. +func MustUnmarshalParams(cdc *codec.LegacyAmino, value []byte) Params { + params, err := UnmarshalParams(cdc, value) + if err != nil { + panic(err) + } + + return params +} + +// unmarshal the current params value from store key. +func UnmarshalParams(cdc *codec.LegacyAmino, value []byte) (params Params, err error) { + err = cdc.Unmarshal(value, ¶ms) + if err != nil { + return params, errors.Wrap(err, "unmarshal params") + } + + return params, nil +} + +func ValidateMaxWithdrawalPerBlock(i any) error { + v, ok := i.(uint32) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v == 0 { + return fmt.Errorf("max withdrawal per block must be positive: %d", v) + } + + return nil +} + +func ValidateMaxSweepPerBlock(i any, maxWithdrawalPerBlock uint32) error { + v, ok := i.(uint32) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v == 0 { + return fmt.Errorf("max sweep per block must be positive: %d", v) + } + + if v < maxWithdrawalPerBlock { + return fmt.Errorf("max sweep per block must be greater than or equal to max withdrawal per block: %d < %d", v, maxWithdrawalPerBlock) + } + + return nil +} + +func ValidateMinPartialWithdrawalAmount(i any) error { + v, ok := i.(uint64) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v == 0 { + return fmt.Errorf("min partial withdrawal amount must be positive: %d", v) + } + + return nil +} diff --git a/client/x/evmstaking/types/params.pb.go b/client/x/evmstaking/types/params.pb.go new file mode 100644 index 00000000..0a5473bb --- /dev/null +++ b/client/x/evmstaking/types/params.pb.go @@ -0,0 +1,380 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmstaking/types/params.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// Params defines the parameters for the module. +type Params struct { + MaxWithdrawalPerBlock uint32 `protobuf:"varint,1,opt,name=max_withdrawal_per_block,json=maxWithdrawalPerBlock,proto3" json:"max_withdrawal_per_block,omitempty" yaml:"max_withdrawal_per_block"` + MaxSweepPerBlock uint32 `protobuf:"varint,2,opt,name=max_sweep_per_block,json=maxSweepPerBlock,proto3" json:"max_sweep_per_block,omitempty" yaml:"max_sweep_per_block"` + MinPartialWithdrawalAmount uint64 `protobuf:"varint,3,opt,name=min_partial_withdrawal_amount,json=minPartialWithdrawalAmount,proto3" json:"min_partial_withdrawal_amount,omitempty" yaml:"min_partial_withdrawal_amount"` +} + +func (m *Params) Reset() { *m = Params{} } +func (m *Params) String() string { return proto.CompactTextString(m) } +func (*Params) ProtoMessage() {} +func (*Params) Descriptor() ([]byte, []int) { + return fileDescriptor_dddf03d6f1b350f8, []int{0} +} +func (m *Params) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Params) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Params.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Params) XXX_Merge(src proto.Message) { + xxx_messageInfo_Params.Merge(m, src) +} +func (m *Params) XXX_Size() int { + return m.Size() +} +func (m *Params) XXX_DiscardUnknown() { + xxx_messageInfo_Params.DiscardUnknown(m) +} + +var xxx_messageInfo_Params proto.InternalMessageInfo + +func (m *Params) GetMaxWithdrawalPerBlock() uint32 { + if m != nil { + return m.MaxWithdrawalPerBlock + } + return 0 +} + +func (m *Params) GetMaxSweepPerBlock() uint32 { + if m != nil { + return m.MaxSweepPerBlock + } + return 0 +} + +func (m *Params) GetMinPartialWithdrawalAmount() uint64 { + if m != nil { + return m.MinPartialWithdrawalAmount + } + return 0 +} + +func init() { + proto.RegisterType((*Params)(nil), "client.x.evmstaking.types.Params") +} + +func init() { + proto.RegisterFile("client/x/evmstaking/types/params.proto", fileDescriptor_dddf03d6f1b350f8) +} + +var fileDescriptor_dddf03d6f1b350f8 = []byte{ + // 288 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4b, 0xce, 0xc9, 0x4c, + 0xcd, 0x2b, 0xd1, 0xaf, 0xd0, 0x4f, 0x2d, 0xcb, 0x2d, 0x2e, 0x49, 0xcc, 0xce, 0xcc, 0x4b, 0xd7, + 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x2f, 0x48, 0x2c, 0x4a, 0xcc, 0x2d, 0xd6, 0x2b, 0x28, 0xca, + 0x2f, 0xc9, 0x17, 0x92, 0x84, 0xa8, 0xd3, 0xab, 0xd0, 0x43, 0xa8, 0xd3, 0x03, 0xab, 0x93, 0x12, + 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xab, 0xd2, 0x07, 0xb1, 0x20, 0x1a, 0x94, 0x16, 0x31, 0x71, 0xb1, + 0x05, 0x80, 0x4d, 0x10, 0x8a, 0xe1, 0x92, 0xc8, 0x4d, 0xac, 0x88, 0x2f, 0xcf, 0x2c, 0xc9, 0x48, + 0x29, 0x4a, 0x2c, 0x4f, 0xcc, 0x89, 0x2f, 0x48, 0x2d, 0x8a, 0x4f, 0xca, 0xc9, 0x4f, 0xce, 0x96, + 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x75, 0x52, 0xfe, 0x74, 0x4f, 0x5e, 0xbe, 0x32, 0x31, 0x37, 0xc7, + 0x4a, 0x09, 0x97, 0x4a, 0xa5, 0x20, 0xd1, 0xdc, 0xc4, 0x8a, 0x70, 0xb8, 0x4c, 0x40, 0x6a, 0x91, + 0x13, 0x48, 0x5c, 0xc8, 0x97, 0x4b, 0x18, 0xa4, 0xa7, 0xb8, 0x3c, 0x35, 0xb5, 0x00, 0xc9, 0x60, + 0x26, 0xb0, 0xc1, 0x72, 0x9f, 0xee, 0xc9, 0x4b, 0x21, 0x0c, 0x46, 0x53, 0xa4, 0x14, 0x24, 0x90, + 0x9b, 0x58, 0x11, 0x0c, 0x12, 0x84, 0x1b, 0x97, 0xcd, 0x25, 0x9b, 0x9b, 0x99, 0x17, 0x5f, 0x90, + 0x58, 0x54, 0x92, 0x99, 0x98, 0x83, 0xec, 0x94, 0xc4, 0xdc, 0xfc, 0xd2, 0xbc, 0x12, 0x09, 0x66, + 0x05, 0x46, 0x0d, 0x16, 0x27, 0x8d, 0x4f, 0xf7, 0xe4, 0x55, 0xa0, 0x06, 0xe3, 0x53, 0xae, 0x14, + 0x24, 0x95, 0x9b, 0x99, 0x17, 0x00, 0x91, 0x46, 0xb8, 0xde, 0x11, 0x2c, 0xe9, 0x64, 0x7c, 0xe2, + 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, + 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c, 0x51, 0x92, 0x38, 0xe3, 0x25, 0x89, 0x0d, 0x1c, + 0xc0, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x45, 0xda, 0xba, 0xbb, 0x01, 0x00, 0x00, +} + +func (m *Params) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Params) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.MinPartialWithdrawalAmount != 0 { + i = encodeVarintParams(dAtA, i, uint64(m.MinPartialWithdrawalAmount)) + i-- + dAtA[i] = 0x18 + } + if m.MaxSweepPerBlock != 0 { + i = encodeVarintParams(dAtA, i, uint64(m.MaxSweepPerBlock)) + i-- + dAtA[i] = 0x10 + } + if m.MaxWithdrawalPerBlock != 0 { + i = encodeVarintParams(dAtA, i, uint64(m.MaxWithdrawalPerBlock)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintParams(dAtA []byte, offset int, v uint64) int { + offset -= sovParams(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Params) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.MaxWithdrawalPerBlock != 0 { + n += 1 + sovParams(uint64(m.MaxWithdrawalPerBlock)) + } + if m.MaxSweepPerBlock != 0 { + n += 1 + sovParams(uint64(m.MaxSweepPerBlock)) + } + if m.MinPartialWithdrawalAmount != 0 { + n += 1 + sovParams(uint64(m.MinPartialWithdrawalAmount)) + } + return n +} + +func sovParams(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozParams(x uint64) (n int) { + return sovParams(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Params) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Params: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Params: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxWithdrawalPerBlock", wireType) + } + m.MaxWithdrawalPerBlock = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxWithdrawalPerBlock |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxSweepPerBlock", wireType) + } + m.MaxSweepPerBlock = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxSweepPerBlock |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MinPartialWithdrawalAmount", wireType) + } + m.MinPartialWithdrawalAmount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MinPartialWithdrawalAmount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipParams(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipParams(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowParams + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowParams + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowParams + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthParams + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupParams + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthParams + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthParams = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowParams = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupParams = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmstaking/types/params.proto b/client/x/evmstaking/types/params.proto new file mode 100644 index 00000000..38cf6443 --- /dev/null +++ b/client/x/evmstaking/types/params.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; +package client.x.evmstaking.types; + +import "gogoproto/gogo.proto"; + +option go_package = "client/x/evmstaking/types"; + +// Params defines the parameters for the module. +message Params { + uint32 max_withdrawal_per_block = 1 [ + (gogoproto.moretags) = "yaml:\"max_withdrawal_per_block\"" + ]; + uint32 max_sweep_per_block = 2 [ + (gogoproto.moretags) = "yaml:\"max_sweep_per_block\"" + ]; + uint64 min_partial_withdrawal_amount = 3 [ + (gogoproto.moretags) = "yaml:\"min_partial_withdrawal_amount\"" + ]; +} \ No newline at end of file diff --git a/client/x/evmstaking/types/query.pb.go b/client/x/evmstaking/types/query.pb.go new file mode 100644 index 00000000..00813311 --- /dev/null +++ b/client/x/evmstaking/types/query.pb.go @@ -0,0 +1,1008 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmstaking/types/query.proto + +package types + +import ( + context "context" + fmt "fmt" + query "github.com/cosmos/cosmos-sdk/types/query" + _ "github.com/cosmos/gogoproto/gogoproto" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// QueryParamsRequest is the request type for the Query/Params RPC method. +type QueryParamsRequest struct { +} + +func (m *QueryParamsRequest) Reset() { *m = QueryParamsRequest{} } +func (m *QueryParamsRequest) String() string { return proto.CompactTextString(m) } +func (*QueryParamsRequest) ProtoMessage() {} +func (*QueryParamsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_e9d6f66d5e677280, []int{0} +} +func (m *QueryParamsRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryParamsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryParamsRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryParamsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryParamsRequest.Merge(m, src) +} +func (m *QueryParamsRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryParamsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryParamsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryParamsRequest proto.InternalMessageInfo + +// QueryParamsResponse is the response type for the Query/Params RPC method. +type QueryParamsResponse struct { + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` +} + +func (m *QueryParamsResponse) Reset() { *m = QueryParamsResponse{} } +func (m *QueryParamsResponse) String() string { return proto.CompactTextString(m) } +func (*QueryParamsResponse) ProtoMessage() {} +func (*QueryParamsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_e9d6f66d5e677280, []int{1} +} +func (m *QueryParamsResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryParamsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryParamsResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryParamsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryParamsResponse.Merge(m, src) +} +func (m *QueryParamsResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryParamsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryParamsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryParamsResponse proto.InternalMessageInfo + +func (m *QueryParamsResponse) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + +// QueryGetWithdrawalQueueRequest is the request type for the Query/WithdrawalQueue RPC method. +type QueryGetWithdrawalQueueRequest struct { + Pagination *query.PageRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"` +} + +func (m *QueryGetWithdrawalQueueRequest) Reset() { *m = QueryGetWithdrawalQueueRequest{} } +func (m *QueryGetWithdrawalQueueRequest) String() string { return proto.CompactTextString(m) } +func (*QueryGetWithdrawalQueueRequest) ProtoMessage() {} +func (*QueryGetWithdrawalQueueRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_e9d6f66d5e677280, []int{2} +} +func (m *QueryGetWithdrawalQueueRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryGetWithdrawalQueueRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryGetWithdrawalQueueRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryGetWithdrawalQueueRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryGetWithdrawalQueueRequest.Merge(m, src) +} +func (m *QueryGetWithdrawalQueueRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryGetWithdrawalQueueRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryGetWithdrawalQueueRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryGetWithdrawalQueueRequest proto.InternalMessageInfo + +func (m *QueryGetWithdrawalQueueRequest) GetPagination() *query.PageRequest { + if m != nil { + return m.Pagination + } + return nil +} + +// QueryGetWithdrawalQueueResponse is the response type for the Query/WithdrawalQueue RPC method. +type QueryGetWithdrawalQueueResponse struct { + Withdrawals []*Withdrawal `protobuf:"bytes,1,rep,name=withdrawals,proto3" json:"withdrawals,omitempty"` + // pagination defines an optional pagination for the request. + Pagination *query.PageResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` +} + +func (m *QueryGetWithdrawalQueueResponse) Reset() { *m = QueryGetWithdrawalQueueResponse{} } +func (m *QueryGetWithdrawalQueueResponse) String() string { return proto.CompactTextString(m) } +func (*QueryGetWithdrawalQueueResponse) ProtoMessage() {} +func (*QueryGetWithdrawalQueueResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_e9d6f66d5e677280, []int{3} +} +func (m *QueryGetWithdrawalQueueResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryGetWithdrawalQueueResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryGetWithdrawalQueueResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryGetWithdrawalQueueResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryGetWithdrawalQueueResponse.Merge(m, src) +} +func (m *QueryGetWithdrawalQueueResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryGetWithdrawalQueueResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryGetWithdrawalQueueResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryGetWithdrawalQueueResponse proto.InternalMessageInfo + +func (m *QueryGetWithdrawalQueueResponse) GetWithdrawals() []*Withdrawal { + if m != nil { + return m.Withdrawals + } + return nil +} + +func (m *QueryGetWithdrawalQueueResponse) GetPagination() *query.PageResponse { + if m != nil { + return m.Pagination + } + return nil +} + +func init() { + proto.RegisterType((*QueryParamsRequest)(nil), "client.x.evmstaking.types.QueryParamsRequest") + proto.RegisterType((*QueryParamsResponse)(nil), "client.x.evmstaking.types.QueryParamsResponse") + proto.RegisterType((*QueryGetWithdrawalQueueRequest)(nil), "client.x.evmstaking.types.QueryGetWithdrawalQueueRequest") + proto.RegisterType((*QueryGetWithdrawalQueueResponse)(nil), "client.x.evmstaking.types.QueryGetWithdrawalQueueResponse") +} + +func init() { + proto.RegisterFile("client/x/evmstaking/types/query.proto", fileDescriptor_e9d6f66d5e677280) +} + +var fileDescriptor_e9d6f66d5e677280 = []byte{ + // 439 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0xbf, 0xae, 0xd3, 0x30, + 0x14, 0xc6, 0xe3, 0x02, 0x1d, 0xdc, 0xcd, 0xdc, 0x81, 0x1b, 0x55, 0x2e, 0x44, 0xb4, 0x54, 0x48, + 0xd8, 0x6a, 0x3b, 0xc1, 0x82, 0xd4, 0x81, 0xae, 0x6d, 0x06, 0x90, 0x58, 0x90, 0x5b, 0xac, 0x34, + 0xa2, 0x8d, 0xd3, 0xd8, 0xe9, 0x9f, 0x95, 0x1d, 0x09, 0x89, 0x47, 0x61, 0xe4, 0x05, 0x3a, 0x56, + 0x42, 0x48, 0x4c, 0x08, 0xb5, 0x3c, 0x08, 0x8a, 0x6d, 0x48, 0xaa, 0x92, 0x16, 0xdd, 0x2d, 0x8a, + 0xbf, 0xef, 0x7c, 0xbf, 0x73, 0x8e, 0x0d, 0x9b, 0x93, 0x59, 0xc8, 0x23, 0x45, 0xd7, 0x94, 0x2f, + 0xe7, 0x52, 0xb1, 0x77, 0x61, 0x14, 0x50, 0xb5, 0x89, 0xb9, 0xa4, 0x8b, 0x94, 0x27, 0x1b, 0x12, + 0x27, 0x42, 0x09, 0x74, 0x6d, 0x64, 0x64, 0x4d, 0x72, 0x19, 0xd1, 0x32, 0xf7, 0x2a, 0x10, 0x81, + 0xd0, 0x2a, 0x9a, 0x7d, 0x19, 0x83, 0x5b, 0x0f, 0x84, 0x08, 0x66, 0x9c, 0xb2, 0x38, 0xa4, 0x2c, + 0x8a, 0x84, 0x62, 0x2a, 0x14, 0x91, 0xb4, 0xa7, 0x8f, 0x27, 0x42, 0xce, 0x85, 0xa4, 0x63, 0x26, + 0xb9, 0xc9, 0xa1, 0xcb, 0xce, 0x98, 0x2b, 0xd6, 0xa1, 0x31, 0x0b, 0xc2, 0x48, 0x8b, 0xad, 0xb6, + 0x55, 0x4e, 0x18, 0xb3, 0x84, 0xcd, 0xf3, 0x9a, 0xa5, 0xba, 0x02, 0xb3, 0xd6, 0x7a, 0x57, 0x10, + 0x8d, 0xb2, 0xd4, 0xa1, 0x2e, 0xe0, 0xf3, 0x45, 0xca, 0xa5, 0xf2, 0x5e, 0xc2, 0xbb, 0x47, 0x7f, + 0x65, 0x2c, 0x22, 0xc9, 0xd1, 0x73, 0x58, 0x35, 0x41, 0xf7, 0xc0, 0x7d, 0xd0, 0xae, 0x75, 0x1f, + 0x90, 0xd2, 0x61, 0x10, 0x63, 0xed, 0xdf, 0xde, 0xfe, 0x68, 0x38, 0xbe, 0xb5, 0x79, 0x53, 0x88, + 0x75, 0xdd, 0x01, 0x57, 0xaf, 0x42, 0x35, 0x7d, 0x9b, 0xb0, 0x15, 0x9b, 0x8d, 0x52, 0x9e, 0x72, + 0x9b, 0x8c, 0x5e, 0x40, 0x98, 0xf7, 0x6d, 0x63, 0x5a, 0xc4, 0x0c, 0x89, 0x64, 0x43, 0x22, 0x66, + 0x19, 0x76, 0x48, 0x64, 0xc8, 0x82, 0x3f, 0x5e, 0xbf, 0xe0, 0xf4, 0x3e, 0x03, 0xd8, 0x28, 0x8d, + 0xb2, 0xed, 0x0c, 0x60, 0x6d, 0xf5, 0xf7, 0x28, 0xeb, 0xe9, 0x56, 0xbb, 0xd6, 0x6d, 0x9e, 0xe9, + 0x29, 0x2f, 0xe4, 0x17, 0x9d, 0x68, 0x70, 0x04, 0x5d, 0xd1, 0xd0, 0x8f, 0x2e, 0x42, 0x1b, 0x8a, + 0x22, 0x75, 0xf7, 0x5b, 0x05, 0xde, 0xd1, 0xd4, 0xe8, 0x03, 0x80, 0x55, 0x33, 0x42, 0xf4, 0xe4, + 0x0c, 0xd1, 0xe9, 0xee, 0x5c, 0xf2, 0xbf, 0x72, 0x93, 0xef, 0x3d, 0x7c, 0xff, 0xf5, 0xd7, 0xa7, + 0x0a, 0x46, 0x75, 0x6a, 0xaf, 0x4d, 0xe1, 0xd2, 0x2c, 0x3b, 0xf6, 0x66, 0xa1, 0x2f, 0x00, 0xa2, + 0xd3, 0x51, 0xa2, 0xa7, 0x97, 0xc2, 0x4a, 0x37, 0xed, 0x3e, 0xbb, 0x89, 0xd5, 0x32, 0x13, 0xcd, + 0xdc, 0x46, 0xad, 0x7f, 0x33, 0xe7, 0xbb, 0x79, 0xb3, 0xc8, 0x7c, 0xfd, 0xde, 0x76, 0x8f, 0xc1, + 0x6e, 0x8f, 0xc1, 0xcf, 0x3d, 0x06, 0x1f, 0x0f, 0xd8, 0xd9, 0x1d, 0xb0, 0xf3, 0xfd, 0x80, 0x9d, + 0xd7, 0xd7, 0xa5, 0x6f, 0x65, 0x5c, 0xd5, 0x2f, 0xa4, 0xf7, 0x3b, 0x00, 0x00, 0xff, 0xff, 0x3d, + 0xe2, 0xae, 0x8a, 0x19, 0x04, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// QueryClient is the client API for Query service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type QueryClient interface { + // Params queries the parameters of the module. + Params(ctx context.Context, in *QueryParamsRequest, opts ...grpc.CallOption) (*QueryParamsResponse, error) + // GetWithdrawalQueue queries the withdrawal queue of the module. + GetWithdrawalQueue(ctx context.Context, in *QueryGetWithdrawalQueueRequest, opts ...grpc.CallOption) (*QueryGetWithdrawalQueueResponse, error) +} + +type queryClient struct { + cc grpc1.ClientConn +} + +func NewQueryClient(cc grpc1.ClientConn) QueryClient { + return &queryClient{cc} +} + +func (c *queryClient) Params(ctx context.Context, in *QueryParamsRequest, opts ...grpc.CallOption) (*QueryParamsResponse, error) { + out := new(QueryParamsResponse) + err := c.cc.Invoke(ctx, "/client.x.evmstaking.types.Query/Params", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *queryClient) GetWithdrawalQueue(ctx context.Context, in *QueryGetWithdrawalQueueRequest, opts ...grpc.CallOption) (*QueryGetWithdrawalQueueResponse, error) { + out := new(QueryGetWithdrawalQueueResponse) + err := c.cc.Invoke(ctx, "/client.x.evmstaking.types.Query/GetWithdrawalQueue", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// QueryServer is the server API for Query service. +type QueryServer interface { + // Params queries the parameters of the module. + Params(context.Context, *QueryParamsRequest) (*QueryParamsResponse, error) + // GetWithdrawalQueue queries the withdrawal queue of the module. + GetWithdrawalQueue(context.Context, *QueryGetWithdrawalQueueRequest) (*QueryGetWithdrawalQueueResponse, error) +} + +// UnimplementedQueryServer can be embedded to have forward compatible implementations. +type UnimplementedQueryServer struct { +} + +func (*UnimplementedQueryServer) Params(ctx context.Context, req *QueryParamsRequest) (*QueryParamsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Params not implemented") +} +func (*UnimplementedQueryServer) GetWithdrawalQueue(ctx context.Context, req *QueryGetWithdrawalQueueRequest) (*QueryGetWithdrawalQueueResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetWithdrawalQueue not implemented") +} + +func RegisterQueryServer(s grpc1.Server, srv QueryServer) { + s.RegisterService(&_Query_serviceDesc, srv) +} + +func _Query_Params_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryParamsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).Params(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/client.x.evmstaking.types.Query/Params", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).Params(ctx, req.(*QueryParamsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Query_GetWithdrawalQueue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryGetWithdrawalQueueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).GetWithdrawalQueue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/client.x.evmstaking.types.Query/GetWithdrawalQueue", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).GetWithdrawalQueue(ctx, req.(*QueryGetWithdrawalQueueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Query_serviceDesc = grpc.ServiceDesc{ + ServiceName: "client.x.evmstaking.types.Query", + HandlerType: (*QueryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Params", + Handler: _Query_Params_Handler, + }, + { + MethodName: "GetWithdrawalQueue", + Handler: _Query_GetWithdrawalQueue_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "client/x/evmstaking/types/query.proto", +} + +func (m *QueryParamsRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryParamsRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryParamsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *QueryParamsResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryParamsResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryParamsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *QueryGetWithdrawalQueueRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryGetWithdrawalQueueRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryGetWithdrawalQueueRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Pagination != nil { + { + size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *QueryGetWithdrawalQueueResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryGetWithdrawalQueueResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryGetWithdrawalQueueResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Pagination != nil { + { + size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.Withdrawals) > 0 { + for iNdEx := len(m.Withdrawals) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Withdrawals[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { + offset -= sovQuery(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *QueryParamsRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *QueryParamsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Params.Size() + n += 1 + l + sovQuery(uint64(l)) + return n +} + +func (m *QueryGetWithdrawalQueueRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Pagination != nil { + l = m.Pagination.Size() + n += 1 + l + sovQuery(uint64(l)) + } + return n +} + +func (m *QueryGetWithdrawalQueueResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Withdrawals) > 0 { + for _, e := range m.Withdrawals { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } + if m.Pagination != nil { + l = m.Pagination.Size() + n += 1 + l + sovQuery(uint64(l)) + } + return n +} + +func sovQuery(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozQuery(x uint64) (n int) { + return sovQuery(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *QueryParamsRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryParamsRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryParamsRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryParamsResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryParamsResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryParamsResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryGetWithdrawalQueueRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryGetWithdrawalQueueRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryGetWithdrawalQueueRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pagination", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Pagination == nil { + m.Pagination = &query.PageRequest{} + } + if err := m.Pagination.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryGetWithdrawalQueueResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryGetWithdrawalQueueResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryGetWithdrawalQueueResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Withdrawals", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Withdrawals = append(m.Withdrawals, &Withdrawal{}) + if err := m.Withdrawals[len(m.Withdrawals)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pagination", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Pagination == nil { + m.Pagination = &query.PageResponse{} + } + if err := m.Pagination.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipQuery(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowQuery + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowQuery + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowQuery + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthQuery + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupQuery + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthQuery + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthQuery = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowQuery = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupQuery = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmstaking/types/query.proto b/client/x/evmstaking/types/query.proto new file mode 100644 index 00000000..78393a90 --- /dev/null +++ b/client/x/evmstaking/types/query.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; +package client.x.evmstaking.types; + +import "gogoproto/gogo.proto"; +import "google/api/annotations.proto"; +import "cosmos/base/query/v1beta1/pagination.proto"; +import "client/x/evmstaking/types/params.proto"; +import "client/x/evmstaking/types/evmstaking.proto"; + +option go_package = "client/x/evmstaking/types"; + +// Query defines the gRPC querier service. +service Query { + // Params queries the parameters of the module. + rpc Params(QueryParamsRequest) returns (QueryParamsResponse) { + option (google.api.http).get = "/client/evmstaking/v1/params"; + } + + // GetWithdrawalQueue queries the withdrawal queue of the module. + rpc GetWithdrawalQueue(QueryGetWithdrawalQueueRequest) returns (QueryGetWithdrawalQueueResponse) { + option (google.api.http).get = "/client/evmstaking/v1/withdrawal_queue"; + } +} + +// QueryParamsRequest is the request type for the Query/Params RPC method. +message QueryParamsRequest {} + +// QueryParamsResponse is the response type for the Query/Params RPC method. +message QueryParamsResponse { + Params params = 1 [(gogoproto.nullable) = false]; +} + +// QueryGetWithdrawalQueueRequest is the request type for the Query/WithdrawalQueue RPC method. +message QueryGetWithdrawalQueueRequest { + cosmos.base.query.v1beta1.PageRequest pagination = 1; +} + +// QueryGetWithdrawalQueueResponse is the response type for the Query/WithdrawalQueue RPC method. +message QueryGetWithdrawalQueueResponse { + repeated Withdrawal withdrawals = 1; + + // pagination defines an optional pagination for the request. + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} \ No newline at end of file diff --git a/client/x/evmstaking/types/slashing_contract.go b/client/x/evmstaking/types/slashing_contract.go new file mode 100644 index 00000000..28c3db38 --- /dev/null +++ b/client/x/evmstaking/types/slashing_contract.go @@ -0,0 +1,10 @@ +package types + +import ( + "github.com/piplabs/story/contracts/bindings" +) + +var ( + ipTokenSlashingABI = mustGetABI(bindings.IPTokenSlashingMetaData) + UnjailEvent = mustGetEvent(ipTokenSlashingABI, "Unjail") +) diff --git a/client/x/evmstaking/types/staking_contract.go b/client/x/evmstaking/types/staking_contract.go new file mode 100644 index 00000000..2ed33c59 --- /dev/null +++ b/client/x/evmstaking/types/staking_contract.go @@ -0,0 +1,39 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + + "github.com/piplabs/story/contracts/bindings" +) + +var ( + ipTokenStakingABI = mustGetABI(bindings.IPTokenStakingMetaData) + SetWithdrawalAddress = mustGetEvent(ipTokenStakingABI, "SetWithdrawalAddress") + CreateValidatorEvent = mustGetEvent(ipTokenStakingABI, "CreateValidator") + DepositEvent = mustGetEvent(ipTokenStakingABI, "Deposit") + RedelegateEvent = mustGetEvent(ipTokenStakingABI, "Redelegate") + WithdrawEvent = mustGetEvent(ipTokenStakingABI, "Withdraw") +) + +// mustGetABI returns the metadata's ABI as an abi.ABI type. +// It panics on error. +func mustGetABI(metadata *bind.MetaData) *abi.ABI { + abi, err := metadata.GetAbi() + if err != nil { + panic(err) + } + + return abi +} + +// mustGetEvent returns the event with the given name from the ABI. +// It panics if the event is not found. +func mustGetEvent(abi *abi.ABI, name string) abi.Event { + event, ok := abi.Events[name] + if !ok { + panic("event not found") + } + + return event +} diff --git a/client/x/evmstaking/types/tx.pb.go b/client/x/evmstaking/types/tx.pb.go new file mode 100644 index 00000000..2e3ad392 --- /dev/null +++ b/client/x/evmstaking/types/tx.pb.go @@ -0,0 +1,1142 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: client/x/evmstaking/types/tx.proto + +package types + +import ( + context "context" + fmt "fmt" + _ "github.com/cosmos/cosmos-sdk/types/msgservice" + _ "github.com/cosmos/gogoproto/gogoproto" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// MsgAddWithdrawal represents a message to add a withdrawal request to the withdrawal queue. +type MsgAddWithdrawal struct { + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + Withdrawal *Withdrawal `protobuf:"bytes,2,opt,name=withdrawal,proto3" json:"withdrawal,omitempty" yaml:"withdrawal"` +} + +func (m *MsgAddWithdrawal) Reset() { *m = MsgAddWithdrawal{} } +func (m *MsgAddWithdrawal) String() string { return proto.CompactTextString(m) } +func (*MsgAddWithdrawal) ProtoMessage() {} +func (*MsgAddWithdrawal) Descriptor() ([]byte, []int) { + return fileDescriptor_9acb5c9b0992bb37, []int{0} +} +func (m *MsgAddWithdrawal) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgAddWithdrawal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgAddWithdrawal.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgAddWithdrawal) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgAddWithdrawal.Merge(m, src) +} +func (m *MsgAddWithdrawal) XXX_Size() int { + return m.Size() +} +func (m *MsgAddWithdrawal) XXX_DiscardUnknown() { + xxx_messageInfo_MsgAddWithdrawal.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgAddWithdrawal proto.InternalMessageInfo + +func (m *MsgAddWithdrawal) GetAuthority() string { + if m != nil { + return m.Authority + } + return "" +} + +func (m *MsgAddWithdrawal) GetWithdrawal() *Withdrawal { + if m != nil { + return m.Withdrawal + } + return nil +} + +// MsgAddWithdrawalResponse defines the Msg/AddWithdrawal response type. +type MsgAddWithdrawalResponse struct { + RequestIndex uint64 `protobuf:"varint,1,opt,name=request_index,json=requestIndex,proto3" json:"request_index,omitempty" yaml:"request_index"` + RequestIdDelegatorValidator uint64 `protobuf:"varint,2,opt,name=request_id_delegator_validator,json=requestIdDelegatorValidator,proto3" json:"request_id_delegator_validator,omitempty" yaml:"request_id_delegator_validator"` +} + +func (m *MsgAddWithdrawalResponse) Reset() { *m = MsgAddWithdrawalResponse{} } +func (m *MsgAddWithdrawalResponse) String() string { return proto.CompactTextString(m) } +func (*MsgAddWithdrawalResponse) ProtoMessage() {} +func (*MsgAddWithdrawalResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_9acb5c9b0992bb37, []int{1} +} +func (m *MsgAddWithdrawalResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgAddWithdrawalResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgAddWithdrawalResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgAddWithdrawalResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgAddWithdrawalResponse.Merge(m, src) +} +func (m *MsgAddWithdrawalResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgAddWithdrawalResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgAddWithdrawalResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgAddWithdrawalResponse proto.InternalMessageInfo + +func (m *MsgAddWithdrawalResponse) GetRequestIndex() uint64 { + if m != nil { + return m.RequestIndex + } + return 0 +} + +func (m *MsgAddWithdrawalResponse) GetRequestIdDelegatorValidator() uint64 { + if m != nil { + return m.RequestIdDelegatorValidator + } + return 0 +} + +// MsgRemoveWithdrawal represents a message to remove a withdrawal request from the withdrawal queue. +type MsgRemoveWithdrawal struct { + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + Delegator string `protobuf:"bytes,2,opt,name=delegator,proto3" json:"delegator,omitempty" yaml:"delegator"` + Validator string `protobuf:"bytes,3,opt,name=validator,proto3" json:"validator,omitempty" yaml:"validator"` + RequestIdDelegatorValidator uint64 `protobuf:"varint,4,opt,name=request_id_delegator_validator,json=requestIdDelegatorValidator,proto3" json:"request_id_delegator_validator,omitempty" yaml:"request_id_delegator_validator"` +} + +func (m *MsgRemoveWithdrawal) Reset() { *m = MsgRemoveWithdrawal{} } +func (m *MsgRemoveWithdrawal) String() string { return proto.CompactTextString(m) } +func (*MsgRemoveWithdrawal) ProtoMessage() {} +func (*MsgRemoveWithdrawal) Descriptor() ([]byte, []int) { + return fileDescriptor_9acb5c9b0992bb37, []int{2} +} +func (m *MsgRemoveWithdrawal) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgRemoveWithdrawal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgRemoveWithdrawal.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgRemoveWithdrawal) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgRemoveWithdrawal.Merge(m, src) +} +func (m *MsgRemoveWithdrawal) XXX_Size() int { + return m.Size() +} +func (m *MsgRemoveWithdrawal) XXX_DiscardUnknown() { + xxx_messageInfo_MsgRemoveWithdrawal.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgRemoveWithdrawal proto.InternalMessageInfo + +func (m *MsgRemoveWithdrawal) GetAuthority() string { + if m != nil { + return m.Authority + } + return "" +} + +func (m *MsgRemoveWithdrawal) GetDelegator() string { + if m != nil { + return m.Delegator + } + return "" +} + +func (m *MsgRemoveWithdrawal) GetValidator() string { + if m != nil { + return m.Validator + } + return "" +} + +func (m *MsgRemoveWithdrawal) GetRequestIdDelegatorValidator() uint64 { + if m != nil { + return m.RequestIdDelegatorValidator + } + return 0 +} + +// MsgRemoveWithdrawalResponse defines the Msg/RemoveWithdrawal response type. +type MsgRemoveWithdrawalResponse struct { +} + +func (m *MsgRemoveWithdrawalResponse) Reset() { *m = MsgRemoveWithdrawalResponse{} } +func (m *MsgRemoveWithdrawalResponse) String() string { return proto.CompactTextString(m) } +func (*MsgRemoveWithdrawalResponse) ProtoMessage() {} +func (*MsgRemoveWithdrawalResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_9acb5c9b0992bb37, []int{3} +} +func (m *MsgRemoveWithdrawalResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgRemoveWithdrawalResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgRemoveWithdrawalResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgRemoveWithdrawalResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgRemoveWithdrawalResponse.Merge(m, src) +} +func (m *MsgRemoveWithdrawalResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgRemoveWithdrawalResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgRemoveWithdrawalResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgRemoveWithdrawalResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*MsgAddWithdrawal)(nil), "client.x.evmstaking.types.MsgAddWithdrawal") + proto.RegisterType((*MsgAddWithdrawalResponse)(nil), "client.x.evmstaking.types.MsgAddWithdrawalResponse") + proto.RegisterType((*MsgRemoveWithdrawal)(nil), "client.x.evmstaking.types.MsgRemoveWithdrawal") + proto.RegisterType((*MsgRemoveWithdrawalResponse)(nil), "client.x.evmstaking.types.MsgRemoveWithdrawalResponse") +} + +func init() { + proto.RegisterFile("client/x/evmstaking/types/tx.proto", fileDescriptor_9acb5c9b0992bb37) +} + +var fileDescriptor_9acb5c9b0992bb37 = []byte{ + // 472 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4a, 0xce, 0xc9, 0x4c, + 0xcd, 0x2b, 0xd1, 0xaf, 0xd0, 0x4f, 0x2d, 0xcb, 0x2d, 0x2e, 0x49, 0xcc, 0xce, 0xcc, 0x4b, 0xd7, + 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x2f, 0xa9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x92, + 0x84, 0xa8, 0xd1, 0xab, 0xd0, 0x43, 0xa8, 0xd1, 0x03, 0xab, 0x91, 0x12, 0x49, 0xcf, 0x4f, 0xcf, + 0x07, 0xab, 0xd2, 0x07, 0xb1, 0x20, 0x1a, 0xa4, 0xc4, 0x93, 0xf3, 0x8b, 0x73, 0xf3, 0x8b, 0xf5, + 0x73, 0x8b, 0xd3, 0xf5, 0xcb, 0x0c, 0x41, 0x14, 0x54, 0x42, 0x0b, 0xb7, 0x6d, 0x48, 0x46, 0x83, + 0xd5, 0x2a, 0xcd, 0x63, 0xe4, 0x12, 0xf0, 0x2d, 0x4e, 0x77, 0x4c, 0x49, 0x09, 0xcf, 0x2c, 0xc9, + 0x48, 0x29, 0x4a, 0x2c, 0x4f, 0xcc, 0x11, 0x92, 0xe1, 0xe2, 0x4c, 0x2c, 0x2d, 0xc9, 0xc8, 0x2f, + 0xca, 0x2c, 0xa9, 0x94, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x42, 0x08, 0x08, 0xc5, 0x70, 0x71, + 0x95, 0xc3, 0xd5, 0x4a, 0x30, 0x29, 0x30, 0x6a, 0x70, 0x1b, 0xa9, 0xea, 0xe1, 0x74, 0xbd, 0x1e, + 0xc2, 0x60, 0x27, 0xd1, 0x4f, 0xf7, 0xe4, 0x05, 0x2b, 0x13, 0x73, 0x73, 0xac, 0x94, 0x10, 0x46, + 0x28, 0x05, 0x21, 0x99, 0x67, 0xc5, 0xd7, 0xf4, 0x7c, 0x83, 0x16, 0xc2, 0x36, 0xa5, 0x93, 0x8c, + 0x5c, 0x12, 0xe8, 0x0e, 0x0c, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x15, 0xb2, 0xe5, 0xe2, + 0x2d, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d, 0x2e, 0x89, 0xcf, 0xcc, 0x4b, 0x49, 0xad, 0x00, 0x3b, 0x96, + 0xc5, 0x49, 0xe2, 0xd3, 0x3d, 0x79, 0x11, 0x88, 0x35, 0x28, 0xd2, 0x4a, 0x41, 0x3c, 0x50, 0xbe, + 0x27, 0x88, 0x2b, 0x94, 0xc7, 0x25, 0x07, 0x97, 0x4f, 0x89, 0x4f, 0x49, 0xcd, 0x49, 0x4d, 0x4f, + 0x2c, 0xc9, 0x2f, 0x8a, 0x2f, 0x4b, 0xcc, 0xc9, 0x4c, 0x01, 0xb1, 0xc0, 0xbe, 0x63, 0x71, 0xd2, + 0xfc, 0x74, 0x4f, 0x5e, 0x15, 0xcd, 0x3c, 0xac, 0xea, 0x95, 0x82, 0xa4, 0x61, 0x16, 0xa4, 0xb8, + 0xc0, 0xa4, 0xc3, 0xe0, 0xb2, 0xb3, 0x99, 0xb8, 0x84, 0x7d, 0x8b, 0xd3, 0x83, 0x52, 0x73, 0xf3, + 0xcb, 0x52, 0x89, 0x0e, 0x6f, 0x23, 0x2e, 0x4e, 0xb8, 0x55, 0x60, 0x07, 0x71, 0x3a, 0x89, 0x7c, + 0xba, 0x27, 0x2f, 0x00, 0x71, 0x10, 0x5c, 0x4a, 0x29, 0x08, 0xa1, 0x0c, 0xa4, 0x07, 0xe1, 0x09, + 0x66, 0x74, 0x3d, 0x48, 0xee, 0x45, 0x28, 0x23, 0x22, 0x34, 0x58, 0xa8, 0x19, 0x1a, 0x18, 0x31, + 0x2d, 0xcb, 0x25, 0x8d, 0x25, 0x70, 0x60, 0x71, 0x6d, 0xd4, 0xc4, 0xc4, 0xc5, 0xe5, 0x5b, 0x9c, + 0x1e, 0x9c, 0x5a, 0x54, 0x96, 0x99, 0x9c, 0x2a, 0x54, 0xc8, 0xc5, 0x8b, 0x9a, 0x68, 0xb5, 0xf1, + 0x24, 0x41, 0xf4, 0x04, 0x24, 0x65, 0x4c, 0x82, 0x62, 0x78, 0x6a, 0xab, 0xe2, 0x12, 0xc0, 0x88, + 0x3a, 0x3d, 0xfc, 0x06, 0xa1, 0xab, 0x97, 0x32, 0x23, 0x4d, 0x3d, 0xcc, 0x6e, 0x29, 0xd6, 0x86, + 0xe7, 0x1b, 0xb4, 0x18, 0x9d, 0x8c, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, + 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, + 0x4a, 0x12, 0x67, 0xa6, 0x4f, 0x62, 0x03, 0x67, 0x75, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xef, 0xcf, 0x54, 0x76, 0x86, 0x04, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// MsgServiceClient is the client API for MsgService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type MsgServiceClient interface { + // AddWithdrawal defines a method to add a withdrawal to the withdrawal queue. + AddWithdrawal(ctx context.Context, in *MsgAddWithdrawal, opts ...grpc.CallOption) (*MsgAddWithdrawalResponse, error) + // RemoveWithdrawal defines a method to remove a withdrawal request from the withdrawal queue. + RemoveWithdrawal(ctx context.Context, in *MsgRemoveWithdrawal, opts ...grpc.CallOption) (*MsgRemoveWithdrawalResponse, error) +} + +type msgServiceClient struct { + cc grpc1.ClientConn +} + +func NewMsgServiceClient(cc grpc1.ClientConn) MsgServiceClient { + return &msgServiceClient{cc} +} + +func (c *msgServiceClient) AddWithdrawal(ctx context.Context, in *MsgAddWithdrawal, opts ...grpc.CallOption) (*MsgAddWithdrawalResponse, error) { + out := new(MsgAddWithdrawalResponse) + err := c.cc.Invoke(ctx, "/client.x.evmstaking.types.MsgService/AddWithdrawal", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *msgServiceClient) RemoveWithdrawal(ctx context.Context, in *MsgRemoveWithdrawal, opts ...grpc.CallOption) (*MsgRemoveWithdrawalResponse, error) { + out := new(MsgRemoveWithdrawalResponse) + err := c.cc.Invoke(ctx, "/client.x.evmstaking.types.MsgService/RemoveWithdrawal", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MsgServiceServer is the server API for MsgService service. +type MsgServiceServer interface { + // AddWithdrawal defines a method to add a withdrawal to the withdrawal queue. + AddWithdrawal(context.Context, *MsgAddWithdrawal) (*MsgAddWithdrawalResponse, error) + // RemoveWithdrawal defines a method to remove a withdrawal request from the withdrawal queue. + RemoveWithdrawal(context.Context, *MsgRemoveWithdrawal) (*MsgRemoveWithdrawalResponse, error) +} + +// UnimplementedMsgServiceServer can be embedded to have forward compatible implementations. +type UnimplementedMsgServiceServer struct { +} + +func (*UnimplementedMsgServiceServer) AddWithdrawal(ctx context.Context, req *MsgAddWithdrawal) (*MsgAddWithdrawalResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddWithdrawal not implemented") +} +func (*UnimplementedMsgServiceServer) RemoveWithdrawal(ctx context.Context, req *MsgRemoveWithdrawal) (*MsgRemoveWithdrawalResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveWithdrawal not implemented") +} + +func RegisterMsgServiceServer(s grpc1.Server, srv MsgServiceServer) { + s.RegisterService(&_MsgService_serviceDesc, srv) +} + +func _MsgService_AddWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgAddWithdrawal) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServiceServer).AddWithdrawal(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/client.x.evmstaking.types.MsgService/AddWithdrawal", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServiceServer).AddWithdrawal(ctx, req.(*MsgAddWithdrawal)) + } + return interceptor(ctx, in, info, handler) +} + +func _MsgService_RemoveWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgRemoveWithdrawal) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServiceServer).RemoveWithdrawal(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/client.x.evmstaking.types.MsgService/RemoveWithdrawal", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServiceServer).RemoveWithdrawal(ctx, req.(*MsgRemoveWithdrawal)) + } + return interceptor(ctx, in, info, handler) +} + +var _MsgService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "client.x.evmstaking.types.MsgService", + HandlerType: (*MsgServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AddWithdrawal", + Handler: _MsgService_AddWithdrawal_Handler, + }, + { + MethodName: "RemoveWithdrawal", + Handler: _MsgService_RemoveWithdrawal_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "client/x/evmstaking/types/tx.proto", +} + +func (m *MsgAddWithdrawal) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgAddWithdrawal) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgAddWithdrawal) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Withdrawal != nil { + { + size, err := m.Withdrawal.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.Authority) > 0 { + i -= len(m.Authority) + copy(dAtA[i:], m.Authority) + i = encodeVarintTx(dAtA, i, uint64(len(m.Authority))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgAddWithdrawalResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgAddWithdrawalResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgAddWithdrawalResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.RequestIdDelegatorValidator != 0 { + i = encodeVarintTx(dAtA, i, uint64(m.RequestIdDelegatorValidator)) + i-- + dAtA[i] = 0x10 + } + if m.RequestIndex != 0 { + i = encodeVarintTx(dAtA, i, uint64(m.RequestIndex)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *MsgRemoveWithdrawal) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgRemoveWithdrawal) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgRemoveWithdrawal) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.RequestIdDelegatorValidator != 0 { + i = encodeVarintTx(dAtA, i, uint64(m.RequestIdDelegatorValidator)) + i-- + dAtA[i] = 0x20 + } + if len(m.Validator) > 0 { + i -= len(m.Validator) + copy(dAtA[i:], m.Validator) + i = encodeVarintTx(dAtA, i, uint64(len(m.Validator))) + i-- + dAtA[i] = 0x1a + } + if len(m.Delegator) > 0 { + i -= len(m.Delegator) + copy(dAtA[i:], m.Delegator) + i = encodeVarintTx(dAtA, i, uint64(len(m.Delegator))) + i-- + dAtA[i] = 0x12 + } + if len(m.Authority) > 0 { + i -= len(m.Authority) + copy(dAtA[i:], m.Authority) + i = encodeVarintTx(dAtA, i, uint64(len(m.Authority))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgRemoveWithdrawalResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgRemoveWithdrawalResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgRemoveWithdrawalResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgAddWithdrawal) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Authority) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.Withdrawal != nil { + l = m.Withdrawal.Size() + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgAddWithdrawalResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.RequestIndex != 0 { + n += 1 + sovTx(uint64(m.RequestIndex)) + } + if m.RequestIdDelegatorValidator != 0 { + n += 1 + sovTx(uint64(m.RequestIdDelegatorValidator)) + } + return n +} + +func (m *MsgRemoveWithdrawal) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Authority) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.Delegator) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.Validator) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.RequestIdDelegatorValidator != 0 { + n += 1 + sovTx(uint64(m.RequestIdDelegatorValidator)) + } + return n +} + +func (m *MsgRemoveWithdrawalResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func sovTx(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTx(x uint64) (n int) { + return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgAddWithdrawal) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgAddWithdrawal: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgAddWithdrawal: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Withdrawal", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Withdrawal == nil { + m.Withdrawal = &Withdrawal{} + } + if err := m.Withdrawal.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgAddWithdrawalResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgAddWithdrawalResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgAddWithdrawalResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RequestIndex", wireType) + } + m.RequestIndex = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RequestIndex |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RequestIdDelegatorValidator", wireType) + } + m.RequestIdDelegatorValidator = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RequestIdDelegatorValidator |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgRemoveWithdrawal) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgRemoveWithdrawal: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgRemoveWithdrawal: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Delegator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Delegator = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Validator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Validator = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RequestIdDelegatorValidator", wireType) + } + m.RequestIdDelegatorValidator = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RequestIdDelegatorValidator |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgRemoveWithdrawalResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgRemoveWithdrawalResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgRemoveWithdrawalResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTx(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTx + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTx + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTx + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTx = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTx = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTx = fmt.Errorf("proto: unexpected end of group") +) diff --git a/client/x/evmstaking/types/tx.proto b/client/x/evmstaking/types/tx.proto new file mode 100644 index 00000000..5466c3ba --- /dev/null +++ b/client/x/evmstaking/types/tx.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; +package client.x.evmstaking.types; + +import "gogoproto/gogo.proto"; +import "cosmos/msg/v1/msg.proto"; +import "client/x/evmstaking/types/evmstaking.proto"; + +option go_package = "client/x/evmstaking/types"; + +//// MsgService defines all the gRPC methods exposed by the evmstaking module. +service MsgService { + option (cosmos.msg.v1.service) = true; + + // AddWithdrawal defines a method to add a withdrawal to the withdrawal queue. + rpc AddWithdrawal(MsgAddWithdrawal) returns (MsgAddWithdrawalResponse); + + // RemoveWithdrawal defines a method to remove a withdrawal request from the withdrawal queue. + rpc RemoveWithdrawal(MsgRemoveWithdrawal) returns (MsgRemoveWithdrawalResponse); +} + +// MsgAddWithdrawal represents a message to add a withdrawal request to the withdrawal queue. +message MsgAddWithdrawal { + option (cosmos.msg.v1.signer) = "authority"; + string authority = 1; // authtypes.NewModuleAddress(types.ModuleName).String() + Withdrawal withdrawal = 2 [(gogoproto.moretags) = "yaml:\"withdrawal\""]; +} + +// MsgAddWithdrawalResponse defines the Msg/AddWithdrawal response type. +message MsgAddWithdrawalResponse { + uint64 request_index = 1 [(gogoproto.moretags) = "yaml:\"request_index\""]; + uint64 request_id_delegator_validator = 2 [(gogoproto.moretags) = "yaml:\"request_id_delegator_validator\""]; +} + +// MsgRemoveWithdrawal represents a message to remove a withdrawal request from the withdrawal queue. +message MsgRemoveWithdrawal { + option (cosmos.msg.v1.signer) = "authority"; + string authority = 1; // authtypes.NewModuleAddress(types.ModuleName).String() + string delegator = 2 [(gogoproto.moretags) = "yaml:\"delegator\""]; + string validator = 3 [(gogoproto.moretags) = "yaml:\"validator\""]; + uint64 request_id_delegator_validator = 4 [(gogoproto.moretags) = "yaml:\"request_id_delegator_validator\""]; +} + +// MsgRemoveWithdrawalResponse defines the Msg/RemoveWithdrawal response type. +message MsgRemoveWithdrawalResponse {} \ No newline at end of file diff --git a/client/x/evmstaking/types/withdraw.go b/client/x/evmstaking/types/withdraw.go new file mode 100644 index 00000000..00bc1994 --- /dev/null +++ b/client/x/evmstaking/types/withdraw.go @@ -0,0 +1,75 @@ +package types + +import ( + "strings" + + "cosmossdk.io/core/address" + + "github.com/cosmos/cosmos-sdk/codec" + + "github.com/piplabs/story/lib/errors" +) + +// Withdrawals is a collection of Withdrawal. +type Withdrawals struct { + Withdrawals []Withdrawal + WithdrawalCodec address.Codec +} + +func (ws Withdrawals) String() (out string) { + for _, w := range ws.Withdrawals { + out += w.String() + "\n" + } + + return strings.TrimSpace(out) +} + +func (ws Withdrawals) Len() int { + return len(ws.Withdrawals) +} + +// TODO amount as math.Int. +func NewWithdrawal(creationHeight uint64, delegatorAddr string, validatorAddr string, executionAddr string, amount uint64) Withdrawal { + return Withdrawal{ + CreationHeight: creationHeight, + DelegatorAddress: delegatorAddr, + ValidatorAddress: validatorAddr, + ExecutionAddress: executionAddr, + Amount: amount, + } +} + +func NewWithdrawalFromMsg(msg *MsgAddWithdrawal) Withdrawal { + return Withdrawal{ + CreationHeight: msg.Withdrawal.CreationHeight, + DelegatorAddress: msg.Withdrawal.DelegatorAddress, + ValidatorAddress: msg.Withdrawal.ValidatorAddress, + ExecutionAddress: msg.Withdrawal.ExecutionAddress, + Amount: msg.Withdrawal.Amount, + } +} + +func MustMarshalWithdrawal(cdc codec.BinaryCodec, withdrawal *Withdrawal) []byte { + return cdc.MustMarshal(withdrawal) +} + +// MustUnmarshalWithdrawal return the unmarshaled withdrawal from bytes. +// Panics if fails. +func MustUnmarshalWithdrawal(cdc codec.BinaryCodec, value []byte) Withdrawal { + withdrawal, err := UnmarshalWithdrawal(cdc, value) + if err != nil { + panic(err) + } + + return withdrawal +} + +// UnmarshalWithdrawal returns the withdrawal. +func UnmarshalWithdrawal(cdc codec.BinaryCodec, value []byte) (withdrawal Withdrawal, err error) { + err = cdc.Unmarshal(value, &withdrawal) + if err != nil { + return withdrawal, errors.Wrap(err, "unmarshal withdrawal") + } + + return withdrawal, nil +} diff --git a/contracts/.env.example b/contracts/.env.example new file mode 100644 index 00000000..b52a564c --- /dev/null +++ b/contracts/.env.example @@ -0,0 +1,5 @@ +INFURA_KEY= +ETHERSCAN_GOERLI_KEY= + +# for local & testnet deployments via forge scripts +CREATE3_DEPLOYER_KEY= diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 00000000..e04a43f5 --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,8 @@ +node_modules + +broadcast + +out +cache + +.env diff --git a/contracts/.prettierrc.json b/contracts/.prettierrc.json new file mode 100644 index 00000000..b084f722 --- /dev/null +++ b/contracts/.prettierrc.json @@ -0,0 +1,24 @@ +{ + "plugins": ["prettier-plugin-solidity"], + "useTabs": false, + "printWidth": 120, + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": false, + "bracketSpacing": true, + "overrides": [ + { + "files": ["*.ts", "*.js"], + "options": { + "tabWidth": 2 + } + }, + { + "files": "*.sol", + "options": { + "singleQuote": false + } + } + ] +} diff --git a/contracts/.solhint.json b/contracts/.solhint.json new file mode 100644 index 00000000..80790b05 --- /dev/null +++ b/contracts/.solhint.json @@ -0,0 +1,26 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.23"], + "const-name-snakecase": "off", + "no-empty-blocks": "off", + "constructor-syntax": "error", + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "not-rely-on-time": "off", + "reason-string": ["warn", { "maxLength": 64 }], + "no-unused-import": "error", + "no-unused-vars": "off", + "no-inline-assembly": "off", + "avoid-low-level-calls": "off", + "no-global-import": "error", + "prettier/prettier": "error", + "private-vars-leading-underscore": "off", + "func-name-mixedcase": "off", + "var-name-mixedcase": "off", + "modifier-name-mixedcase": "off", + "custom-errors": "off" + } +} diff --git a/contracts/Makefile b/contracts/Makefile new file mode 100644 index 00000000..649cb3ef --- /dev/null +++ b/contracts/Makefile @@ -0,0 +1,46 @@ +ifneq ("$(wildcard .env)","") + include .env + export $(shell sed 's/=.*//' .env) +endif + + +.PHONY: help +help: ## Display this help message. + @egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: version +version: ## Print tool versions. + @forge --version + @abigen --version + +.PHONY: build +build: version ## Build contracts. + forge build + +.PHONY: test +test: version ## Run tests. + forge test + +CONTRACTS := IPTokenStaking IPTokenSlashing UpgradeEntrypoint Create3 + +.PHONY: bindings +bindings: check-abigen-version build ## Generate golang contract bindings. + ./bindings/scripts/gen.sh $(CONTRACTS) + ./bindings/scripts/genmore.sh IPTokenStaking IPTokenSlashing UpgradeEntrypoint Create3 + +.PHONY: fork-holesky +fork-holesky: ## Run an anvil holesky fork. + anvil --fork-url https://holesky.infura.io/v3/$(INFURA_KEY) + +.PHONY: fork-mainnet +fork-mainnet: ## Run an anvil mainnet fork. + anvil --fork-url https://mainnet.infura.io/v3/$(INFURA_KEY) + +.PHONY: check-abigen-version +check-abigen-version: ## Check abigen version, exit(1) if not 1.13.14-stable. + @version=$$(abigen --version); \ + if [ "$$version" != "abigen version 1.13.14-stable" ]; then \ + echo "abigen version is not 1.13.14-stable"; \ + echo "Install with go install github.com/ethereum/go-ethereum/cmd/abigen@v1.13.14"; \ + exit 1; \ + fi diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 00000000..af21bd5c --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,41 @@ +# Iliad Contracts + +## Install Dependencies +1. Install `npm` if you haven't. + +2. Pull `node_modules`. + +``` +npm install -g pnpm +pnpm install +``` + +## Build + +1. Install `abigen`. + +``` +go install github.com/ethereum/go-ethereum/cmd/abigen@latest +``` + +2. Build the contracts. + +``` +make build +``` + +## Test + +1. Install `foundry`. + +``` +curl -L https://foundry.paradigm.xyz | bash +source ~/.bash_profile +foundryup +``` + +2. Run tests. + +``` +make test +``` diff --git a/contracts/bindings/create3.go b/contracts/bindings/create3.go new file mode 100644 index 00000000..4fb134dc --- /dev/null +++ b/contracts/bindings/create3.go @@ -0,0 +1,255 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// Create3MetaData contains all meta data concerning the Create3 contract. +var Create3MetaData = &bind.MetaData{ + ABI: "[{\"type\":\"function\",\"name\":\"deploy\",\"inputs\":[{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"creationCode\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"deployed\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"getDeployed\",\"inputs\":[{\"name\":\"deployer\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"deployed\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"}]", + Bin: "0x608060405234801561001057600080fd5b506104b2806100206000396000f3fe6080604052600436106100295760003560e01c806350f1c4641461002e578063cdcb760a1461006a575b600080fd5b34801561003a57600080fd5b5061004e610049366004610344565b61007d565b6040516001600160a01b03909116815260200160405180910390f35b61004e610078366004610392565b6100c9565b6040516001600160601b0319606084901b166020820152603481018290526000906054016040516020818303038152906040528051906020012091506100c28261010f565b9392505050565b6040516001600160601b03193360601b166020820152603481018390526000906054016040516020818303038152906040528051906020012092506100c28383346101e9565b604080518082018252601081526f67363d3d37363d34f03d5260086018f360801b60209182015290516001600160f81b0319918101919091526001600160601b03193060601b166021820152603581018290527f21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f605582015260009081906101ae906075015b6040516020818303038152906040528051906020012090565b6040516135a560f21b60208201526001600160601b0319606083901b166022820152600160f81b60368201529091506100c290603701610195565b6000806040518060400160405280601081526020016f67363d3d37363d34f03d5260086018f360801b81525090506000858251602084016000f590506001600160a01b0381166102745760405162461bcd60e51b81526020600482015260116024820152701111541313d65351539517d19052531151607a1b60448201526064015b60405180910390fd5b61027d8661010f565b92506000816001600160a01b0316858760405161029a919061044d565b60006040518083038185875af1925050503d80600081146102d7576040519150601f19603f3d011682016040523d82523d6000602084013e6102dc565b606091505b505090508080156102f657506001600160a01b0384163b15155b61033a5760405162461bcd60e51b815260206004820152601560248201527412539255125053125690551253d397d19052531151605a1b604482015260640161026b565b5050509392505050565b6000806040838503121561035757600080fd5b82356001600160a01b038116811461036e57600080fd5b946020939093013593505050565b634e487b7160e01b600052604160045260246000fd5b600080604083850312156103a557600080fd5b82359150602083013567ffffffffffffffff808211156103c457600080fd5b818501915085601f8301126103d857600080fd5b8135818111156103ea576103ea61037c565b604051601f8201601f19908116603f011681019083821181831017156104125761041261037c565b8160405282815288602084870101111561042b57600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b6000825160005b8181101561046e5760208186018101518583015201610454565b50600092019182525091905056fea2646970667358221220a752acb74ec7b8289cc6bef7edec2b86be9d66ea00b3eeefa5cbd593353cf79364736f6c63430008180033", +} + +// Create3ABI is the input ABI used to generate the binding from. +// Deprecated: Use Create3MetaData.ABI instead. +var Create3ABI = Create3MetaData.ABI + +// Create3Bin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use Create3MetaData.Bin instead. +var Create3Bin = Create3MetaData.Bin + +// DeployCreate3 deploys a new Ethereum contract, binding an instance of Create3 to it. +func DeployCreate3(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Create3, error) { + parsed, err := Create3MetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(Create3Bin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Create3{Create3Caller: Create3Caller{contract: contract}, Create3Transactor: Create3Transactor{contract: contract}, Create3Filterer: Create3Filterer{contract: contract}}, nil +} + +// Create3 is an auto generated Go binding around an Ethereum contract. +type Create3 struct { + Create3Caller // Read-only binding to the contract + Create3Transactor // Write-only binding to the contract + Create3Filterer // Log filterer for contract events +} + +// Create3Caller is an auto generated read-only Go binding around an Ethereum contract. +type Create3Caller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Create3Transactor is an auto generated write-only Go binding around an Ethereum contract. +type Create3Transactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Create3Filterer is an auto generated log filtering Go binding around an Ethereum contract events. +type Create3Filterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Create3Session is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type Create3Session struct { + Contract *Create3 // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Create3CallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type Create3CallerSession struct { + Contract *Create3Caller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// Create3TransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type Create3TransactorSession struct { + Contract *Create3Transactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Create3Raw is an auto generated low-level Go binding around an Ethereum contract. +type Create3Raw struct { + Contract *Create3 // Generic contract binding to access the raw methods on +} + +// Create3CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type Create3CallerRaw struct { + Contract *Create3Caller // Generic read-only contract binding to access the raw methods on +} + +// Create3TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type Create3TransactorRaw struct { + Contract *Create3Transactor // Generic write-only contract binding to access the raw methods on +} + +// NewCreate3 creates a new instance of Create3, bound to a specific deployed contract. +func NewCreate3(address common.Address, backend bind.ContractBackend) (*Create3, error) { + contract, err := bindCreate3(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Create3{Create3Caller: Create3Caller{contract: contract}, Create3Transactor: Create3Transactor{contract: contract}, Create3Filterer: Create3Filterer{contract: contract}}, nil +} + +// NewCreate3Caller creates a new read-only instance of Create3, bound to a specific deployed contract. +func NewCreate3Caller(address common.Address, caller bind.ContractCaller) (*Create3Caller, error) { + contract, err := bindCreate3(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &Create3Caller{contract: contract}, nil +} + +// NewCreate3Transactor creates a new write-only instance of Create3, bound to a specific deployed contract. +func NewCreate3Transactor(address common.Address, transactor bind.ContractTransactor) (*Create3Transactor, error) { + contract, err := bindCreate3(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &Create3Transactor{contract: contract}, nil +} + +// NewCreate3Filterer creates a new log filterer instance of Create3, bound to a specific deployed contract. +func NewCreate3Filterer(address common.Address, filterer bind.ContractFilterer) (*Create3Filterer, error) { + contract, err := bindCreate3(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &Create3Filterer{contract: contract}, nil +} + +// bindCreate3 binds a generic wrapper to an already deployed contract. +func bindCreate3(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := Create3MetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Create3 *Create3Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Create3.Contract.Create3Caller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Create3 *Create3Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Create3.Contract.Create3Transactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Create3 *Create3Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Create3.Contract.Create3Transactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Create3 *Create3CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Create3.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Create3 *Create3TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Create3.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Create3 *Create3TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Create3.Contract.contract.Transact(opts, method, params...) +} + +// GetDeployed is a free data retrieval call binding the contract method 0x50f1c464. +// +// Solidity: function getDeployed(address deployer, bytes32 salt) view returns(address deployed) +func (_Create3 *Create3Caller) GetDeployed(opts *bind.CallOpts, deployer common.Address, salt [32]byte) (common.Address, error) { + var out []interface{} + err := _Create3.contract.Call(opts, &out, "getDeployed", deployer, salt) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetDeployed is a free data retrieval call binding the contract method 0x50f1c464. +// +// Solidity: function getDeployed(address deployer, bytes32 salt) view returns(address deployed) +func (_Create3 *Create3Session) GetDeployed(deployer common.Address, salt [32]byte) (common.Address, error) { + return _Create3.Contract.GetDeployed(&_Create3.CallOpts, deployer, salt) +} + +// GetDeployed is a free data retrieval call binding the contract method 0x50f1c464. +// +// Solidity: function getDeployed(address deployer, bytes32 salt) view returns(address deployed) +func (_Create3 *Create3CallerSession) GetDeployed(deployer common.Address, salt [32]byte) (common.Address, error) { + return _Create3.Contract.GetDeployed(&_Create3.CallOpts, deployer, salt) +} + +// Deploy is a paid mutator transaction binding the contract method 0xcdcb760a. +// +// Solidity: function deploy(bytes32 salt, bytes creationCode) payable returns(address deployed) +func (_Create3 *Create3Transactor) Deploy(opts *bind.TransactOpts, salt [32]byte, creationCode []byte) (*types.Transaction, error) { + return _Create3.contract.Transact(opts, "deploy", salt, creationCode) +} + +// Deploy is a paid mutator transaction binding the contract method 0xcdcb760a. +// +// Solidity: function deploy(bytes32 salt, bytes creationCode) payable returns(address deployed) +func (_Create3 *Create3Session) Deploy(salt [32]byte, creationCode []byte) (*types.Transaction, error) { + return _Create3.Contract.Deploy(&_Create3.TransactOpts, salt, creationCode) +} + +// Deploy is a paid mutator transaction binding the contract method 0xcdcb760a. +// +// Solidity: function deploy(bytes32 salt, bytes creationCode) payable returns(address deployed) +func (_Create3 *Create3TransactorSession) Deploy(salt [32]byte, creationCode []byte) (*types.Transaction, error) { + return _Create3.Contract.Deploy(&_Create3.TransactOpts, salt, creationCode) +} diff --git a/contracts/bindings/create3_more.go b/contracts/bindings/create3_more.go new file mode 100644 index 00000000..7d954548 --- /dev/null +++ b/contracts/bindings/create3_more.go @@ -0,0 +1,14 @@ +package bindings + +import ( + _ "embed" +) + +const ( + Create3DeployedBytecode = "0x6080604052600436106100295760003560e01c806350f1c4641461002e578063cdcb760a1461006a575b600080fd5b34801561003a57600080fd5b5061004e610049366004610344565b61007d565b6040516001600160a01b03909116815260200160405180910390f35b61004e610078366004610392565b6100c9565b6040516001600160601b0319606084901b166020820152603481018290526000906054016040516020818303038152906040528051906020012091506100c28261010f565b9392505050565b6040516001600160601b03193360601b166020820152603481018390526000906054016040516020818303038152906040528051906020012092506100c28383346101e9565b604080518082018252601081526f67363d3d37363d34f03d5260086018f360801b60209182015290516001600160f81b0319918101919091526001600160601b03193060601b166021820152603581018290527f21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f605582015260009081906101ae906075015b6040516020818303038152906040528051906020012090565b6040516135a560f21b60208201526001600160601b0319606083901b166022820152600160f81b60368201529091506100c290603701610195565b6000806040518060400160405280601081526020016f67363d3d37363d34f03d5260086018f360801b81525090506000858251602084016000f590506001600160a01b0381166102745760405162461bcd60e51b81526020600482015260116024820152701111541313d65351539517d19052531151607a1b60448201526064015b60405180910390fd5b61027d8661010f565b92506000816001600160a01b0316858760405161029a919061044d565b60006040518083038185875af1925050503d80600081146102d7576040519150601f19603f3d011682016040523d82523d6000602084013e6102dc565b606091505b505090508080156102f657506001600160a01b0384163b15155b61033a5760405162461bcd60e51b815260206004820152601560248201527412539255125053125690551253d397d19052531151605a1b604482015260640161026b565b5050509392505050565b6000806040838503121561035757600080fd5b82356001600160a01b038116811461036e57600080fd5b946020939093013593505050565b634e487b7160e01b600052604160045260246000fd5b600080604083850312156103a557600080fd5b82359150602083013567ffffffffffffffff808211156103c457600080fd5b818501915085601f8301126103d857600080fd5b8135818111156103ea576103ea61037c565b604051601f8201601f19908116603f011681019083821181831017156104125761041261037c565b8160405282815288602084870101111561042b57600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b6000825160005b8181101561046e5760208186018101518583015201610454565b50600092019182525091905056fea26469706673582212205071ecbd4979c75d0bcb429bc9c9f81699911fb5df3e8dd679491493e1ead03b64736f6c63430008180033" +) + +//go:embed create3_storage_layout.json +var create3StorageLayoutJSON []byte + +var Create3StorageLayout = mustGetStorageLayout(create3StorageLayoutJSON) diff --git a/contracts/bindings/create3_storage_layout.json b/contracts/bindings/create3_storage_layout.json new file mode 100644 index 00000000..127dec9f --- /dev/null +++ b/contracts/bindings/create3_storage_layout.json @@ -0,0 +1,4 @@ +{ + "storage": [], + "types": {} +} diff --git a/contracts/bindings/iptokenslashing.go b/contracts/bindings/iptokenslashing.go new file mode 100644 index 00000000..6ae730d4 --- /dev/null +++ b/contracts/bindings/iptokenslashing.go @@ -0,0 +1,904 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IPTokenSlashingMetaData contains all meta data concerning the IPTokenSlashing contract. +var IPTokenSlashingMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"ipTokenStaking\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"newUnjailFee\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"IP_TOKEN_STAKING\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractIPTokenStaking\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"acceptOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"pendingOwner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setUnjailFee\",\"inputs\":[{\"name\":\"newUnjailFee\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"unjail\",\"inputs\":[{\"name\":\"validatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"unjailFee\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"unjailOnBehalf\",\"inputs\":[{\"name\":\"validatorCmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"event\",\"name\":\"OwnershipTransferStarted\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Unjail\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"validatorCmpPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + Bin: "0x60a060405234801561001057600080fd5b50604051610d2f380380610d2f83398101604081905261002f91610107565b826001600160a01b03811661005e57604051631e4fbdf760e01b81526000600482015260240160405180910390fd5b6100678161007f565b506001600160a01b0390911660805260025550610143565b600180546001600160a01b03191690556100988161009b565b50565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b80516001600160a01b038116811461010257600080fd5b919050565b60008060006060848603121561011c57600080fd5b610125846100eb565b9250610133602085016100eb565b9150604084015190509250925092565b608051610bcb6101646000396000818160a8015261059e0152610bcb6000f3fe6080604052600436106100915760003560e01c806379ba50971161005957806379ba5097146101555780638da5cb5b1461016a578063e30c397814610188578063e4dfccd8146101a6578063f2fde38b146101b957600080fd5b806304ff53ed146100965780630c863f77146100e75780632801f1ec1461010957806340eda14a1461012d578063715018a614610140575b600080fd5b3480156100a257600080fd5b506100ca7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b3480156100f357600080fd5b506101076101023660046107e0565b6101d9565b005b34801561011557600080fd5b5061011f60025481565b6040519081526020016100de565b61010761013b3660046107f9565b6101e6565b34801561014c57600080fd5b50610107610268565b34801561016157600080fd5b5061010761027c565b34801561017657600080fd5b506000546001600160a01b03166100ca565b34801561019457600080fd5b506001546001600160a01b03166100ca565b6101076101b43660046107f9565b6102c5565b3480156101c557600080fd5b506101076101d436600461086b565b610450565b6101e16104c1565b600255565b61022582828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506104ee92505050565b61026482828080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061067e92505050565b5050565b6102706104c1565b61027a600061074a565b565b60015433906001600160a01b031681146102b95760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b6102c28161074a565b50565b818133604182146102e85760405162461bcd60e51b81526004016102b09061089b565b828260008181106102fb576102fb6108e1565b9050013560f81c60f81b6001600160f81b031916600460f81b146103315760405162461bcd60e51b81526004016102b0906108f7565b806001600160a01b03166103458484610763565b6001600160a01b0316146103b35760405162461bcd60e51b815260206004820152602f60248201527f4950546f6b656e536c617368696e673a20496e76616c6964207075626b65792060448201526e64657269766564206164647265737360881b60648201526084016102b0565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e906103ef908990899060040161093d565b600060405180830381865af415801561040c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526104349190810190610a1a565b905061043f816104ee565b6104488161067e565b505050505050565b6104586104c1565b600180546001600160a01b0383166001600160a01b031990911681179091556104896000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b6000546001600160a01b0316331461027a5760405163118cdaa760e01b81523360048201526024016102b0565b805160211461050f5760405162461bcd60e51b81526004016102b09061089b565b80600081518110610522576105226108e1565b6020910101516001600160f81b031916600160f91b1480610568575080600081518110610551576105516108e1565b6020910101516001600160f81b031916600360f81b145b6105845760405162461bcd60e51b81526004016102b0906108f7565b604051638d3e1e4160e01b81526000906001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690638d3e1e41906105d3908590600401610a6b565b600060405180830381865afa1580156105f0573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526106189190810190610ab7565b50505050509050806102645760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e536c617368696e673a2056616c696461746f7220646f6573206044820152681b9bdd08195e1a5cdd60ba1b60648201526084016102b0565b60025434146106d95760405162461bcd60e51b815260206004820152602160248201527f4950546f6b656e536c617368696e673a20496e73756666696369656e742066656044820152606560f81b60648201526084016102b0565b6040516000903480156108fc029183818181858288f19350505050158015610705573d6000803e3d6000fd5b50336001600160a01b03167f4a90ea32527ecacc0f4b32b31f99e4c633a2b4fe81ea7444989e2e68bc9ece3b8260405161073f9190610a6b565b60405180910390a250565b600180546001600160a01b03191690556102c281610790565b60006107728260018186610b5b565b604051610780929190610b85565b6040519081900390209392505050565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000602082840312156107f257600080fd5b5035919050565b6000806020838503121561080c57600080fd5b823567ffffffffffffffff8082111561082457600080fd5b818501915085601f83011261083857600080fd5b81358181111561084757600080fd5b86602082850101111561085957600080fd5b60209290920196919550909350505050565b60006020828403121561087d57600080fd5b81356001600160a01b038116811461089457600080fd5b9392505050565b60208082526026908201527f4950546f6b656e536c617368696e673a20496e76616c6964207075626b6579206040820152650d8cadccee8d60d31b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60208082526026908201527f4950546f6b656e536c617368696e673a20496e76616c6964207075626b6579206040820152650e0e4caccd2f60d31b606082015260800190565b60208152816020820152818360408301376000818301604090810191909152601f909201601f19160101919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561099d578181015183820152602001610985565b50506000910152565b600067ffffffffffffffff808411156109c1576109c161096c565b604051601f8501601f19908116603f011681019082821181831017156109e9576109e961096c565b81604052809350858152868686011115610a0257600080fd5b610a10866020830187610982565b5050509392505050565b600060208284031215610a2c57600080fd5b815167ffffffffffffffff811115610a4357600080fd5b8201601f81018413610a5457600080fd5b610a63848251602084016109a6565b949350505050565b6020815260008251806020840152610a8a816040850160208701610982565b601f01601f19169190910160400192915050565b805163ffffffff81168114610ab257600080fd5b919050565b60008060008060008060c08789031215610ad057600080fd5b86518015158114610ae057600080fd5b602088015190965067ffffffffffffffff811115610afd57600080fd5b8701601f81018913610b0e57600080fd5b610b1d898251602084016109a6565b95505060408701519350610b3360608801610a9e565b9250610b4160808801610a9e565b9150610b4f60a08801610a9e565b90509295509295509295565b60008085851115610b6b57600080fd5b83861115610b7857600080fd5b5050820193919092039150565b818382376000910190815291905056fea2646970667358221220ef1caa5e2adb7ee57adc7890bbe897a407f3d4c2912ee3a3a663fc33d6345b8c64736f6c63430008180033", +} + +// IPTokenSlashingABI is the input ABI used to generate the binding from. +// Deprecated: Use IPTokenSlashingMetaData.ABI instead. +var IPTokenSlashingABI = IPTokenSlashingMetaData.ABI + +// IPTokenSlashingBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use IPTokenSlashingMetaData.Bin instead. +var IPTokenSlashingBin = IPTokenSlashingMetaData.Bin + +// DeployIPTokenSlashing deploys a new Ethereum contract, binding an instance of IPTokenSlashing to it. +func DeployIPTokenSlashing(auth *bind.TransactOpts, backend bind.ContractBackend, newOwner common.Address, ipTokenStaking common.Address, newUnjailFee *big.Int) (common.Address, *types.Transaction, *IPTokenSlashing, error) { + parsed, err := IPTokenSlashingMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(IPTokenSlashingBin), backend, newOwner, ipTokenStaking, newUnjailFee) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &IPTokenSlashing{IPTokenSlashingCaller: IPTokenSlashingCaller{contract: contract}, IPTokenSlashingTransactor: IPTokenSlashingTransactor{contract: contract}, IPTokenSlashingFilterer: IPTokenSlashingFilterer{contract: contract}}, nil +} + +// IPTokenSlashing is an auto generated Go binding around an Ethereum contract. +type IPTokenSlashing struct { + IPTokenSlashingCaller // Read-only binding to the contract + IPTokenSlashingTransactor // Write-only binding to the contract + IPTokenSlashingFilterer // Log filterer for contract events +} + +// IPTokenSlashingCaller is an auto generated read-only Go binding around an Ethereum contract. +type IPTokenSlashingCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPTokenSlashingTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IPTokenSlashingTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPTokenSlashingFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IPTokenSlashingFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPTokenSlashingSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IPTokenSlashingSession struct { + Contract *IPTokenSlashing // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IPTokenSlashingCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IPTokenSlashingCallerSession struct { + Contract *IPTokenSlashingCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IPTokenSlashingTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IPTokenSlashingTransactorSession struct { + Contract *IPTokenSlashingTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IPTokenSlashingRaw is an auto generated low-level Go binding around an Ethereum contract. +type IPTokenSlashingRaw struct { + Contract *IPTokenSlashing // Generic contract binding to access the raw methods on +} + +// IPTokenSlashingCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IPTokenSlashingCallerRaw struct { + Contract *IPTokenSlashingCaller // Generic read-only contract binding to access the raw methods on +} + +// IPTokenSlashingTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IPTokenSlashingTransactorRaw struct { + Contract *IPTokenSlashingTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIPTokenSlashing creates a new instance of IPTokenSlashing, bound to a specific deployed contract. +func NewIPTokenSlashing(address common.Address, backend bind.ContractBackend) (*IPTokenSlashing, error) { + contract, err := bindIPTokenSlashing(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IPTokenSlashing{IPTokenSlashingCaller: IPTokenSlashingCaller{contract: contract}, IPTokenSlashingTransactor: IPTokenSlashingTransactor{contract: contract}, IPTokenSlashingFilterer: IPTokenSlashingFilterer{contract: contract}}, nil +} + +// NewIPTokenSlashingCaller creates a new read-only instance of IPTokenSlashing, bound to a specific deployed contract. +func NewIPTokenSlashingCaller(address common.Address, caller bind.ContractCaller) (*IPTokenSlashingCaller, error) { + contract, err := bindIPTokenSlashing(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IPTokenSlashingCaller{contract: contract}, nil +} + +// NewIPTokenSlashingTransactor creates a new write-only instance of IPTokenSlashing, bound to a specific deployed contract. +func NewIPTokenSlashingTransactor(address common.Address, transactor bind.ContractTransactor) (*IPTokenSlashingTransactor, error) { + contract, err := bindIPTokenSlashing(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IPTokenSlashingTransactor{contract: contract}, nil +} + +// NewIPTokenSlashingFilterer creates a new log filterer instance of IPTokenSlashing, bound to a specific deployed contract. +func NewIPTokenSlashingFilterer(address common.Address, filterer bind.ContractFilterer) (*IPTokenSlashingFilterer, error) { + contract, err := bindIPTokenSlashing(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IPTokenSlashingFilterer{contract: contract}, nil +} + +// bindIPTokenSlashing binds a generic wrapper to an already deployed contract. +func bindIPTokenSlashing(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IPTokenSlashingMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IPTokenSlashing *IPTokenSlashingRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IPTokenSlashing.Contract.IPTokenSlashingCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IPTokenSlashing *IPTokenSlashingRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.IPTokenSlashingTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IPTokenSlashing *IPTokenSlashingRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.IPTokenSlashingTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IPTokenSlashing *IPTokenSlashingCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IPTokenSlashing.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IPTokenSlashing *IPTokenSlashingTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IPTokenSlashing *IPTokenSlashingTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.contract.Transact(opts, method, params...) +} + +// IPTOKENSTAKING is a free data retrieval call binding the contract method 0x04ff53ed. +// +// Solidity: function IP_TOKEN_STAKING() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingCaller) IPTOKENSTAKING(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IPTokenSlashing.contract.Call(opts, &out, "IP_TOKEN_STAKING") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// IPTOKENSTAKING is a free data retrieval call binding the contract method 0x04ff53ed. +// +// Solidity: function IP_TOKEN_STAKING() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingSession) IPTOKENSTAKING() (common.Address, error) { + return _IPTokenSlashing.Contract.IPTOKENSTAKING(&_IPTokenSlashing.CallOpts) +} + +// IPTOKENSTAKING is a free data retrieval call binding the contract method 0x04ff53ed. +// +// Solidity: function IP_TOKEN_STAKING() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingCallerSession) IPTOKENSTAKING() (common.Address, error) { + return _IPTokenSlashing.Contract.IPTOKENSTAKING(&_IPTokenSlashing.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IPTokenSlashing.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingSession) Owner() (common.Address, error) { + return _IPTokenSlashing.Contract.Owner(&_IPTokenSlashing.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingCallerSession) Owner() (common.Address, error) { + return _IPTokenSlashing.Contract.Owner(&_IPTokenSlashing.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingCaller) PendingOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IPTokenSlashing.contract.Call(opts, &out, "pendingOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingSession) PendingOwner() (common.Address, error) { + return _IPTokenSlashing.Contract.PendingOwner(&_IPTokenSlashing.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_IPTokenSlashing *IPTokenSlashingCallerSession) PendingOwner() (common.Address, error) { + return _IPTokenSlashing.Contract.PendingOwner(&_IPTokenSlashing.CallOpts) +} + +// UnjailFee is a free data retrieval call binding the contract method 0x2801f1ec. +// +// Solidity: function unjailFee() view returns(uint256) +func (_IPTokenSlashing *IPTokenSlashingCaller) UnjailFee(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IPTokenSlashing.contract.Call(opts, &out, "unjailFee") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// UnjailFee is a free data retrieval call binding the contract method 0x2801f1ec. +// +// Solidity: function unjailFee() view returns(uint256) +func (_IPTokenSlashing *IPTokenSlashingSession) UnjailFee() (*big.Int, error) { + return _IPTokenSlashing.Contract.UnjailFee(&_IPTokenSlashing.CallOpts) +} + +// UnjailFee is a free data retrieval call binding the contract method 0x2801f1ec. +// +// Solidity: function unjailFee() view returns(uint256) +func (_IPTokenSlashing *IPTokenSlashingCallerSession) UnjailFee() (*big.Int, error) { + return _IPTokenSlashing.Contract.UnjailFee(&_IPTokenSlashing.CallOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_IPTokenSlashing *IPTokenSlashingTransactor) AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenSlashing.contract.Transact(opts, "acceptOwnership") +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_IPTokenSlashing *IPTokenSlashingSession) AcceptOwnership() (*types.Transaction, error) { + return _IPTokenSlashing.Contract.AcceptOwnership(&_IPTokenSlashing.TransactOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_IPTokenSlashing *IPTokenSlashingTransactorSession) AcceptOwnership() (*types.Transaction, error) { + return _IPTokenSlashing.Contract.AcceptOwnership(&_IPTokenSlashing.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IPTokenSlashing *IPTokenSlashingTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenSlashing.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IPTokenSlashing *IPTokenSlashingSession) RenounceOwnership() (*types.Transaction, error) { + return _IPTokenSlashing.Contract.RenounceOwnership(&_IPTokenSlashing.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IPTokenSlashing *IPTokenSlashingTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _IPTokenSlashing.Contract.RenounceOwnership(&_IPTokenSlashing.TransactOpts) +} + +// SetUnjailFee is a paid mutator transaction binding the contract method 0x0c863f77. +// +// Solidity: function setUnjailFee(uint256 newUnjailFee) returns() +func (_IPTokenSlashing *IPTokenSlashingTransactor) SetUnjailFee(opts *bind.TransactOpts, newUnjailFee *big.Int) (*types.Transaction, error) { + return _IPTokenSlashing.contract.Transact(opts, "setUnjailFee", newUnjailFee) +} + +// SetUnjailFee is a paid mutator transaction binding the contract method 0x0c863f77. +// +// Solidity: function setUnjailFee(uint256 newUnjailFee) returns() +func (_IPTokenSlashing *IPTokenSlashingSession) SetUnjailFee(newUnjailFee *big.Int) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.SetUnjailFee(&_IPTokenSlashing.TransactOpts, newUnjailFee) +} + +// SetUnjailFee is a paid mutator transaction binding the contract method 0x0c863f77. +// +// Solidity: function setUnjailFee(uint256 newUnjailFee) returns() +func (_IPTokenSlashing *IPTokenSlashingTransactorSession) SetUnjailFee(newUnjailFee *big.Int) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.SetUnjailFee(&_IPTokenSlashing.TransactOpts, newUnjailFee) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IPTokenSlashing *IPTokenSlashingTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _IPTokenSlashing.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IPTokenSlashing *IPTokenSlashingSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.TransferOwnership(&_IPTokenSlashing.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IPTokenSlashing *IPTokenSlashingTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.TransferOwnership(&_IPTokenSlashing.TransactOpts, newOwner) +} + +// Unjail is a paid mutator transaction binding the contract method 0xe4dfccd8. +// +// Solidity: function unjail(bytes validatorUncmpPubkey) payable returns() +func (_IPTokenSlashing *IPTokenSlashingTransactor) Unjail(opts *bind.TransactOpts, validatorUncmpPubkey []byte) (*types.Transaction, error) { + return _IPTokenSlashing.contract.Transact(opts, "unjail", validatorUncmpPubkey) +} + +// Unjail is a paid mutator transaction binding the contract method 0xe4dfccd8. +// +// Solidity: function unjail(bytes validatorUncmpPubkey) payable returns() +func (_IPTokenSlashing *IPTokenSlashingSession) Unjail(validatorUncmpPubkey []byte) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.Unjail(&_IPTokenSlashing.TransactOpts, validatorUncmpPubkey) +} + +// Unjail is a paid mutator transaction binding the contract method 0xe4dfccd8. +// +// Solidity: function unjail(bytes validatorUncmpPubkey) payable returns() +func (_IPTokenSlashing *IPTokenSlashingTransactorSession) Unjail(validatorUncmpPubkey []byte) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.Unjail(&_IPTokenSlashing.TransactOpts, validatorUncmpPubkey) +} + +// UnjailOnBehalf is a paid mutator transaction binding the contract method 0x40eda14a. +// +// Solidity: function unjailOnBehalf(bytes validatorCmpPubkey) payable returns() +func (_IPTokenSlashing *IPTokenSlashingTransactor) UnjailOnBehalf(opts *bind.TransactOpts, validatorCmpPubkey []byte) (*types.Transaction, error) { + return _IPTokenSlashing.contract.Transact(opts, "unjailOnBehalf", validatorCmpPubkey) +} + +// UnjailOnBehalf is a paid mutator transaction binding the contract method 0x40eda14a. +// +// Solidity: function unjailOnBehalf(bytes validatorCmpPubkey) payable returns() +func (_IPTokenSlashing *IPTokenSlashingSession) UnjailOnBehalf(validatorCmpPubkey []byte) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.UnjailOnBehalf(&_IPTokenSlashing.TransactOpts, validatorCmpPubkey) +} + +// UnjailOnBehalf is a paid mutator transaction binding the contract method 0x40eda14a. +// +// Solidity: function unjailOnBehalf(bytes validatorCmpPubkey) payable returns() +func (_IPTokenSlashing *IPTokenSlashingTransactorSession) UnjailOnBehalf(validatorCmpPubkey []byte) (*types.Transaction, error) { + return _IPTokenSlashing.Contract.UnjailOnBehalf(&_IPTokenSlashing.TransactOpts, validatorCmpPubkey) +} + +// IPTokenSlashingOwnershipTransferStartedIterator is returned from FilterOwnershipTransferStarted and is used to iterate over the raw logs and unpacked data for OwnershipTransferStarted events raised by the IPTokenSlashing contract. +type IPTokenSlashingOwnershipTransferStartedIterator struct { + Event *IPTokenSlashingOwnershipTransferStarted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenSlashingOwnershipTransferStartedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenSlashingOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenSlashingOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenSlashingOwnershipTransferStartedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenSlashingOwnershipTransferStartedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenSlashingOwnershipTransferStarted represents a OwnershipTransferStarted event raised by the IPTokenSlashing contract. +type IPTokenSlashingOwnershipTransferStarted struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferStarted is a free log retrieval operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_IPTokenSlashing *IPTokenSlashingFilterer) FilterOwnershipTransferStarted(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*IPTokenSlashingOwnershipTransferStartedIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenSlashing.contract.FilterLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &IPTokenSlashingOwnershipTransferStartedIterator{contract: _IPTokenSlashing.contract, event: "OwnershipTransferStarted", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferStarted is a free log subscription operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_IPTokenSlashing *IPTokenSlashingFilterer) WatchOwnershipTransferStarted(opts *bind.WatchOpts, sink chan<- *IPTokenSlashingOwnershipTransferStarted, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenSlashing.contract.WatchLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenSlashingOwnershipTransferStarted) + if err := _IPTokenSlashing.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferStarted is a log parse operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_IPTokenSlashing *IPTokenSlashingFilterer) ParseOwnershipTransferStarted(log types.Log) (*IPTokenSlashingOwnershipTransferStarted, error) { + event := new(IPTokenSlashingOwnershipTransferStarted) + if err := _IPTokenSlashing.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenSlashingOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the IPTokenSlashing contract. +type IPTokenSlashingOwnershipTransferredIterator struct { + Event *IPTokenSlashingOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenSlashingOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenSlashingOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenSlashingOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenSlashingOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenSlashingOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenSlashingOwnershipTransferred represents a OwnershipTransferred event raised by the IPTokenSlashing contract. +type IPTokenSlashingOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_IPTokenSlashing *IPTokenSlashingFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*IPTokenSlashingOwnershipTransferredIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenSlashing.contract.FilterLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &IPTokenSlashingOwnershipTransferredIterator{contract: _IPTokenSlashing.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_IPTokenSlashing *IPTokenSlashingFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *IPTokenSlashingOwnershipTransferred, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenSlashing.contract.WatchLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenSlashingOwnershipTransferred) + if err := _IPTokenSlashing.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_IPTokenSlashing *IPTokenSlashingFilterer) ParseOwnershipTransferred(log types.Log) (*IPTokenSlashingOwnershipTransferred, error) { + event := new(IPTokenSlashingOwnershipTransferred) + if err := _IPTokenSlashing.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenSlashingUnjailIterator is returned from FilterUnjail and is used to iterate over the raw logs and unpacked data for Unjail events raised by the IPTokenSlashing contract. +type IPTokenSlashingUnjailIterator struct { + Event *IPTokenSlashingUnjail // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenSlashingUnjailIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenSlashingUnjail) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenSlashingUnjail) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenSlashingUnjailIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenSlashingUnjailIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenSlashingUnjail represents a Unjail event raised by the IPTokenSlashing contract. +type IPTokenSlashingUnjail struct { + Sender common.Address + ValidatorCmpPubkey []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterUnjail is a free log retrieval operation binding the contract event 0x4a90ea32527ecacc0f4b32b31f99e4c633a2b4fe81ea7444989e2e68bc9ece3b. +// +// Solidity: event Unjail(address indexed sender, bytes validatorCmpPubkey) +func (_IPTokenSlashing *IPTokenSlashingFilterer) FilterUnjail(opts *bind.FilterOpts, sender []common.Address) (*IPTokenSlashingUnjailIterator, error) { + + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _IPTokenSlashing.contract.FilterLogs(opts, "Unjail", senderRule) + if err != nil { + return nil, err + } + return &IPTokenSlashingUnjailIterator{contract: _IPTokenSlashing.contract, event: "Unjail", logs: logs, sub: sub}, nil +} + +// WatchUnjail is a free log subscription operation binding the contract event 0x4a90ea32527ecacc0f4b32b31f99e4c633a2b4fe81ea7444989e2e68bc9ece3b. +// +// Solidity: event Unjail(address indexed sender, bytes validatorCmpPubkey) +func (_IPTokenSlashing *IPTokenSlashingFilterer) WatchUnjail(opts *bind.WatchOpts, sink chan<- *IPTokenSlashingUnjail, sender []common.Address) (event.Subscription, error) { + + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _IPTokenSlashing.contract.WatchLogs(opts, "Unjail", senderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenSlashingUnjail) + if err := _IPTokenSlashing.contract.UnpackLog(event, "Unjail", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseUnjail is a log parse operation binding the contract event 0x4a90ea32527ecacc0f4b32b31f99e4c633a2b4fe81ea7444989e2e68bc9ece3b. +// +// Solidity: event Unjail(address indexed sender, bytes validatorCmpPubkey) +func (_IPTokenSlashing *IPTokenSlashingFilterer) ParseUnjail(log types.Log) (*IPTokenSlashingUnjail, error) { + event := new(IPTokenSlashingUnjail) + if err := _IPTokenSlashing.contract.UnpackLog(event, "Unjail", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/contracts/bindings/iptokenslashing_more.go b/contracts/bindings/iptokenslashing_more.go new file mode 100644 index 00000000..9e144e62 --- /dev/null +++ b/contracts/bindings/iptokenslashing_more.go @@ -0,0 +1,14 @@ +package bindings + +import ( + _ "embed" +) + +const ( + IPTokenSlashingDeployedBytecode = "0x6080604052600436106100915760003560e01c806379ba50971161005957806379ba5097146101555780638da5cb5b1461016a578063e30c397814610188578063e4dfccd8146101a6578063f2fde38b146101b957600080fd5b806304ff53ed146100965780630c863f77146100e75780632801f1ec1461010957806340eda14a1461012d578063715018a614610140575b600080fd5b3480156100a257600080fd5b506100ca7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b3480156100f357600080fd5b506101076101023660046107e0565b6101d9565b005b34801561011557600080fd5b5061011f60025481565b6040519081526020016100de565b61010761013b3660046107f9565b6101e6565b34801561014c57600080fd5b50610107610268565b34801561016157600080fd5b5061010761027c565b34801561017657600080fd5b506000546001600160a01b03166100ca565b34801561019457600080fd5b506001546001600160a01b03166100ca565b6101076101b43660046107f9565b6102c5565b3480156101c557600080fd5b506101076101d436600461086b565b610450565b6101e16104c1565b600255565b61022582828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506104ee92505050565b61026482828080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061067e92505050565b5050565b6102706104c1565b61027a600061074a565b565b60015433906001600160a01b031681146102b95760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b6102c28161074a565b50565b818133604182146102e85760405162461bcd60e51b81526004016102b09061089b565b828260008181106102fb576102fb6108e1565b9050013560f81c60f81b6001600160f81b031916600460f81b146103315760405162461bcd60e51b81526004016102b0906108f7565b806001600160a01b03166103458484610763565b6001600160a01b0316146103b35760405162461bcd60e51b815260206004820152602f60248201527f4950546f6b656e536c617368696e673a20496e76616c6964207075626b65792060448201526e64657269766564206164647265737360881b60648201526084016102b0565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906103ef908990899060040161093d565b600060405180830381865af415801561040c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526104349190810190610a1a565b905061043f816104ee565b6104488161067e565b505050505050565b6104586104c1565b600180546001600160a01b0383166001600160a01b031990911681179091556104896000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b6000546001600160a01b0316331461027a5760405163118cdaa760e01b81523360048201526024016102b0565b805160211461050f5760405162461bcd60e51b81526004016102b09061089b565b80600081518110610522576105226108e1565b6020910101516001600160f81b031916600160f91b1480610568575080600081518110610551576105516108e1565b6020910101516001600160f81b031916600360f81b145b6105845760405162461bcd60e51b81526004016102b0906108f7565b604051638d3e1e4160e01b81526000906001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690638d3e1e41906105d3908590600401610a6b565b600060405180830381865afa1580156105f0573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526106189190810190610ab7565b50505050509050806102645760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e536c617368696e673a2056616c696461746f7220646f6573206044820152681b9bdd08195e1a5cdd60ba1b60648201526084016102b0565b60025434146106d95760405162461bcd60e51b815260206004820152602160248201527f4950546f6b656e536c617368696e673a20496e73756666696369656e742066656044820152606560f81b60648201526084016102b0565b6040516000903480156108fc029183818181858288f19350505050158015610705573d6000803e3d6000fd5b50336001600160a01b03167f4a90ea32527ecacc0f4b32b31f99e4c633a2b4fe81ea7444989e2e68bc9ece3b8260405161073f9190610a6b565b60405180910390a250565b600180546001600160a01b03191690556102c281610790565b60006107728260018186610b5b565b604051610780929190610b85565b6040519081900390209392505050565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000602082840312156107f257600080fd5b5035919050565b6000806020838503121561080c57600080fd5b823567ffffffffffffffff8082111561082457600080fd5b818501915085601f83011261083857600080fd5b81358181111561084757600080fd5b86602082850101111561085957600080fd5b60209290920196919550909350505050565b60006020828403121561087d57600080fd5b81356001600160a01b038116811461089457600080fd5b9392505050565b60208082526026908201527f4950546f6b656e536c617368696e673a20496e76616c6964207075626b6579206040820152650d8cadccee8d60d31b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60208082526026908201527f4950546f6b656e536c617368696e673a20496e76616c6964207075626b6579206040820152650e0e4caccd2f60d31b606082015260800190565b60208152816020820152818360408301376000818301604090810191909152601f909201601f19160101919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561099d578181015183820152602001610985565b50506000910152565b600067ffffffffffffffff808411156109c1576109c161096c565b604051601f8501601f19908116603f011681019082821181831017156109e9576109e961096c565b81604052809350858152868686011115610a0257600080fd5b610a10866020830187610982565b5050509392505050565b600060208284031215610a2c57600080fd5b815167ffffffffffffffff811115610a4357600080fd5b8201601f81018413610a5457600080fd5b610a63848251602084016109a6565b949350505050565b6020815260008251806020840152610a8a816040850160208701610982565b601f01601f19169190910160400192915050565b805163ffffffff81168114610ab257600080fd5b919050565b60008060008060008060c08789031215610ad057600080fd5b86518015158114610ae057600080fd5b602088015190965067ffffffffffffffff811115610afd57600080fd5b8701601f81018913610b0e57600080fd5b610b1d898251602084016109a6565b95505060408701519350610b3360608801610a9e565b9250610b4160808801610a9e565b9150610b4f60a08801610a9e565b90509295509295509295565b60008085851115610b6b57600080fd5b83861115610b7857600080fd5b5050820193919092039150565b818382376000910190815291905056fea2646970667358221220518734ced1f5808f38b15b569cae72cf48a894f86b1e5a88cac8505c46c3c8d764736f6c63430008180033" +) + +//go:embed iptokenslashing_storage_layout.json +var iptokenslashingStorageLayoutJSON []byte + +var IPTokenSlashingStorageLayout = mustGetStorageLayout(iptokenslashingStorageLayoutJSON) diff --git a/contracts/bindings/iptokenslashing_storage_layout.json b/contracts/bindings/iptokenslashing_storage_layout.json new file mode 100644 index 00000000..3062dadf --- /dev/null +++ b/contracts/bindings/iptokenslashing_storage_layout.json @@ -0,0 +1,40 @@ +{ + "storage": [ + { + "astId": 8, + "contract": "src/protocol/IPTokenSlashing.sol:IPTokenSlashing", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 156, + "contract": "src/protocol/IPTokenSlashing.sol:IPTokenSlashing", + "label": "_pendingOwner", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 40320, + "contract": "src/protocol/IPTokenSlashing.sol:IPTokenSlashing", + "label": "unjailFee", + "offset": 0, + "slot": "2", + "type": "t_uint256" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } +} diff --git a/contracts/bindings/iptokenstaking.go b/contracts/bindings/iptokenstaking.go new file mode 100644 index 00000000..f530c97b --- /dev/null +++ b/contracts/bindings/iptokenstaking.go @@ -0,0 +1,2122 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IIPTokenStakingRedelegateParams is an auto generated low-level Go binding around an user-defined struct. +type IIPTokenStakingRedelegateParams struct { + DelegatorUncmpPubkey []byte + ValidatorSrcPubkey []byte + ValidatorDstPubkey []byte + Amount *big.Int +} + +// IPTokenStakingMetaData contains all meta data concerning the IPTokenStaking contract. +var IPTokenStakingMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_minStakeAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"_minUnstakeAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"_minRedelegateAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"stakingRounding\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"_withdrawalAddressChangeInterval\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"defaultCommissionRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"defaultMaxCommissionRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"defaultMaxCommissionChangeRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"DEFAULT_COMMISSION_RATE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"DEFAULT_MAX_COMMISSION_CHANGE_RATE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"DEFAULT_MAX_COMMISSION_RATE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"STAKE_ROUNDING\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"acceptOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"addOperator\",\"inputs\":[{\"name\":\"uncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"createValidator\",\"inputs\":[{\"name\":\"validatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"moniker\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"commissionRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"maxCommissionRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"maxCommissionChangeRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"createValidatorOnBehalf\",\"inputs\":[{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"delegatorTotalStakes\",\"inputs\":[{\"name\":\"delegatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"stakedAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"delegatorValidatorStakes\",\"inputs\":[{\"name\":\"delegatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"stakedAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getOperators\",\"inputs\":[{\"name\":\"pubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"minRedelegateAmount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"minStakeAmount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"minUnstakeAmount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"pendingOwner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"redelegate\",\"inputs\":[{\"name\":\"p\",\"type\":\"tuple\",\"internalType\":\"structIIPTokenStaking.RedelegateParams\",\"components\":[{\"name\":\"delegatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorSrcPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorDstPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"redelegateOnBehalf\",\"inputs\":[{\"name\":\"p\",\"type\":\"tuple\",\"internalType\":\"structIIPTokenStaking.RedelegateParams\",\"components\":[{\"name\":\"delegatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorSrcPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorDstPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"removeOperator\",\"inputs\":[{\"name\":\"uncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"roundedStakeAmount\",\"inputs\":[{\"name\":\"rawAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"remainder\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"setMinRedelegateAmount\",\"inputs\":[{\"name\":\"newMinRedelegateAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setMinStakeAmount\",\"inputs\":[{\"name\":\"newMinStakeAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setMinUnstakeAmount\",\"inputs\":[{\"name\":\"newMinUnstakeAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setWithdrawalAddress\",\"inputs\":[{\"name\":\"delegatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"newWithdrawalAddress\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setWithdrawalAddressChangeInterval\",\"inputs\":[{\"name\":\"newWithdrawalAddressChangeInterval\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"stake\",\"inputs\":[{\"name\":\"delegatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"stakeOnBehalf\",\"inputs\":[{\"name\":\"delegatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"unstake\",\"inputs\":[{\"name\":\"delegatorUncmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"unstakeOnBehalf\",\"inputs\":[{\"name\":\"delegatorCmpPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"validatorMetadata\",\"inputs\":[{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"exists\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"moniker\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"totalStake\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"commissionRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"maxCommissionRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"maxCommissionChangeRate\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"withdrawalAddressChange\",\"inputs\":[{\"name\":\"delegatorPubkey\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"lastChange\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"withdrawalAddressChangeInterval\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"CreateValidator\",\"inputs\":[{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"moniker\",\"type\":\"string\",\"indexed\":false,\"internalType\":\"string\"},{\"name\":\"stakeAmount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"commissionRate\",\"type\":\"uint32\",\"indexed\":false,\"internalType\":\"uint32\"},{\"name\":\"maxCommissionRate\",\"type\":\"uint32\",\"indexed\":false,\"internalType\":\"uint32\"},{\"name\":\"maxCommissionChangeRate\",\"type\":\"uint32\",\"indexed\":false,\"internalType\":\"uint32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Deposit\",\"inputs\":[{\"name\":\"depositorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferStarted\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Redelegate\",\"inputs\":[{\"name\":\"depositorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"validatorSrcPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"validatorDstPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SetWithdrawalAddress\",\"inputs\":[{\"name\":\"depositorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"executionAddress\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Withdraw\",\"inputs\":[{\"name\":\"depositorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"validatorPubkey\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + Bin: "0x6101006040523480156200001257600080fd5b5060405162003a3338038062003a3383398101604081905262000035916200012d565b886001600160a01b0381166200006557604051631e4fbdf760e01b81526000600482015260240160405180910390fd5b6200007081620000a5565b5060029790975560039590955560049390935560055560e05263ffffffff90811660805290811660a0521660c05250620001ca565b600180546001600160a01b0319169055620000c081620000c3565b50565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b805163ffffffff811681146200012857600080fd5b919050565b60008060008060008060008060006101208a8c0312156200014d57600080fd5b89516001600160a01b03811681146200016557600080fd5b8099505060208a0151975060408a0151965060608a0151955060808a0151945060a08a015193506200019a60c08b0162000113565b9250620001aa60e08b0162000113565b9150620001bb6101008b0162000113565b90509295985092959850929598565b60805160a05160c05160e0516137fe6200023560003960008181610585015281816110c001528181611cad01528181611eee0152611f960152600081816106cd0152610ae40152600081816102d00152610ac30152600081816105510152610aa001526137fe6000f3fe6080604052600436106101f95760003560e01c806386eec4a11161010d578063c24ae586116100a0578063eee5cead1161006f578063eee5cead14610652578063f188768414610672578063f2fde38b14610688578063fc2e5932146106a8578063fc56c2a2146106bb57600080fd5b8063c24ae586146105a7578063d2e1f5b8146105df578063e30c397814610614578063eb4af0451461063257600080fd5b80639855c8b5116100dc5780639855c8b5146104ff578063a1cb18461461051f578063b8db983e1461053f578063bda16b151461057357600080fd5b806386eec4a1146104755780638d3e1e41146104885780638da5cb5b146104ba5780638f37ec19146104ec57600080fd5b80635706750311610190578063715018a61161015f578063715018a6146103de578063787f82c8146103f357806379ba5097146104135780637b6e842c1461042857806383dffd6f1461044857600080fd5b806357067503146103505780635a69825d146103885780635d5ab9681461039e5780636ea3a228146103be57600080fd5b80632ebc6034116101cc5780632ebc6034146102be57806339ec4df91461030757806348903e381461031d57806353972c2a1461033057600080fd5b8063057b9296146101fe578063060ceab01461022057806317e42e12146102495780632d1e973e14610269575b600080fd5b34801561020a57600080fd5b5061021e610219366004612d1f565b6106ef565b005b34801561022c57600080fd5b5061023660055481565b6040519081526020015b60405180910390f35b34801561025557600080fd5b5061021e610264366004612d1f565b610854565b34801561027557600080fd5b50610236610284366004612e35565b8151602081840181018051600882529282019482019490942091909352815180830184018051928152908401929093019190912091525481565b3480156102ca57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b60405163ffffffff9091168152602001610240565b34801561031357600080fd5b5061023660035481565b61021e61032b366004612e98565b6109a6565b34801561033c57600080fd5b5061021e61034b366004612ed9565b610b0e565b34801561035c57600080fd5b5061023661036b366004612f13565b805160208183018101805160078252928201919093012091525481565b34801561039457600080fd5b5061023660045481565b3480156103aa57600080fd5b5061021e6103b9366004612f4f565b610e2c565b3480156103ca57600080fd5b5061021e6103d9366004612fc2565b61104e565b3480156103ea57600080fd5b5061021e6110f5565b3480156103ff57600080fd5b5061021e61040e366004612d1f565b611109565b34801561041f57600080fd5b5061021e6113bf565b34801561043457600080fd5b5061021e610443366004612ed9565b611403565b34801561045457600080fd5b50610468610463366004612e98565b611753565b6040516102409190612fdb565b61021e610483366004613028565b611786565b34801561049457600080fd5b506104a86104a3366004612f13565b61199a565b604051610240969594939291906130e3565b3480156104c657600080fd5b506000546001600160a01b03165b6040516001600160a01b039091168152602001610240565b61021e6104fa366004613028565b611a76565b34801561050b57600080fd5b5061021e61051a366004612fc2565b611c38565b34801561052b57600080fd5b5061021e61053a366004612f4f565b611ce2565b34801561054b57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b34801561057f57600080fd5b506102367f000000000000000000000000000000000000000000000000000000000000000081565b3480156105b357600080fd5b506102366105c2366004612f13565b8051602081830181018051600a8252928201919093012091525481565b3480156105eb57600080fd5b506105ff6105fa366004612fc2565b611ee6565b60408051928352602083019190915201610240565b34801561062057600080fd5b506001546001600160a01b03166104d4565b34801561063e57600080fd5b5061021e61064d366004612fc2565b611f26565b34801561065e57600080fd5b5061021e61066d366004612fc2565b611fcb565b34801561067e57600080fd5b5061023660025481565b34801561069457600080fd5b5061021e6106a336600461312b565b61204e565b61021e6106b636600461315a565b6120bf565b3480156106c757600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b8282336041821461071b5760405162461bcd60e51b8152600401610712906131fb565b60405180910390fd5b8282600081811061072e5761072e613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146107645760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610778848461222d565b6001600160a01b03161461079e5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e906107da908a908a90600401613312565b600060405180830381865af41580156107f7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261081f9190810190613326565b905061084a85600983604051610835919061339c565b9081526040519081900360200190209061225a565b5050505050505050565b828233604182146108775760405162461bcd60e51b8152600401610712906131fb565b8282600081811061088a5761088a613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146108c05760405162461bcd60e51b815260040161071290613256565b806001600160a01b03166108d4848461222d565b6001600160a01b0316146108fa5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e90610936908a908a90600401613312565b600060405180830381865af4158015610953573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261097b9190810190613326565b905061084a85600983604051610991919061339c565b9081526040519081900360200190209061226f565b8181602181146109c85760405162461bcd60e51b8152600401610712906131fb565b818160008181106109db576109db613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610a29575081816000818110610a0e57610a0e613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610a455760405162461bcd60e51b815260040161071290613256565b610b0884848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250506040805180820190915260098152683b30b634b230ba37b960b91b602082015291507f000000000000000000000000000000000000000000000000000000000000000090507f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000612284565b50505050565b610b1881806133b8565b60418114610b385760405162461bcd60e51b8152600401610712906131fb565b81816000818110610b4b57610b4b613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610b815760405162461bcd60e51b815260040161071290613256565b610b8e60208401846133b8565b60218114610bae5760405162461bcd60e51b8152600401610712906131fb565b81816000818110610bc157610bc1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610c0f575081816000818110610bf457610bf4613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610c2b5760405162461bcd60e51b815260040161071290613256565b60068282604051610c3d9291906133fe565b9081526040519081900360200190205460ff16610c6c5760405162461bcd60e51b81526004016107129061340e565b610c7960408601866133b8565b60218114610c995760405162461bcd60e51b8152600401610712906131fb565b81816000818110610cac57610cac613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610cfa575081816000818110610cdf57610cdf613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610d165760405162461bcd60e51b815260040161071290613256565b60068282604051610d289291906133fe565b9081526040519081900360200190205460ff16610d575760405162461bcd60e51b81526004016107129061340e565b600073__$070efe90de6222b6182e3f0710b89d2262$__636889b16e610d7d8a806133b8565b6040518363ffffffff1660e01b8152600401610d9a929190613312565b600060405180830381865af4158015610db7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610ddf9190810190613326565b90506000610df08960600135611ee6565b509050610dfd82336124e6565b610e2182610e0e60208c018c6133b8565b610e1b60408e018e6133b8565b86612571565b505050505050505050565b84843360418214610e4f5760405162461bcd60e51b8152600401610712906131fb565b82826000818110610e6257610e62613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610e985760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610eac848461222d565b6001600160a01b031614610ed25760405162461bcd60e51b81526004016107129061329b565b858560218114610ef45760405162461bcd60e51b8152600401610712906131fb565b81816000818110610f0757610f07613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610f55575081816000818110610f3a57610f3a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610f715760405162461bcd60e51b815260040161071290613256565b60068282604051610f839291906133fe565b9081526040519081900360200190205460ff16610fb25760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e90610fee908e908e90600401613312565b600060405180830381865af415801561100b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110339190810190613326565b9050611041818a8a8a61272f565b5050505050505050505050565b61105661288f565b600081116110bb5760405162461bcd60e51b815260206004820152602c60248201527f4950546f6b656e5374616b696e673a206d696e556e7374616b65416d6f756e7460448201526b02063616e6e6f7420626520360a41b6064820152608401610712565b6110e57f000000000000000000000000000000000000000000000000000000000000000082613456565b6110ef908261348e565b60035550565b6110fd61288f565b61110760006128bc565b565b8282336041821461112c5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061113f5761113f613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146111755760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611189848461222d565b6001600160a01b0316146111af5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e906111eb908a908a90600401613312565b600060405180830381865af4158015611208573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526112309190810190613326565b90506000600782604051611244919061339c565b908152602001604051809103902054116112b25760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2044656c656761746f72206d7573742068604482015268617665207374616b6560b81b6064820152608401610712565b42600554600a836040516112c6919061339c565b9081526020016040518091039020546112df91906134a1565b106113485760405162461bcd60e51b815260206004820152603360248201527f4950546f6b656e5374616b696e673a205769746864726177616c20616464726560448201527239b99031b430b733b29031b7b7b616b237bbb760691b6064820152608401610712565b42600a82604051611359919061339c565b9081526020016040518091039020819055507f9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca818660601b6bffffffffffffffffffffffff19166040516113ae9291906134b4565b60405180910390a150505050505050565b60015433906001600160a01b031681146113f75760405163118cdaa760e01b81526001600160a01b0382166004820152602401610712565b611400816128bc565b50565b61140d81806133b8565b336041821461142e5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061144157611441613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146114775760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661148b848461222d565b6001600160a01b0316146114b15760405162461bcd60e51b81526004016107129061329b565b6114be60208501856133b8565b602181146114de5760405162461bcd60e51b8152600401610712906131fb565b818160008181106114f1576114f1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061153f57508181600081811061152457611524613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b61155b5760405162461bcd60e51b815260040161071290613256565b6006828260405161156d9291906133fe565b9081526040519081900360200190205460ff1661159c5760405162461bcd60e51b81526004016107129061340e565b6115a960408701876133b8565b602181146115c95760405162461bcd60e51b8152600401610712906131fb565b818160008181106115dc576115dc613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061162a57508181600081811061160f5761160f613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6116465760405162461bcd60e51b815260040161071290613256565b600682826040516116589291906133fe565b9081526040519081900360200190205460ff166116875760405162461bcd60e51b81526004016107129061340e565b60006116968960600135611ee6565b509050600073__$070efe90de6222b6182e3f0710b89d2262$__636889b16e6116bf8c806133b8565b6040518363ffffffff1660e01b81526004016116dc929190613312565b600060405180830381865af41580156116f9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526117219190810190613326565b90506117478161173460208d018d6133b8565b61174160408f018f6133b8565b87612571565b50505050505050505050565b606061177d6009848460405161176a9291906133fe565b90815260200160405180910390206128d5565b90505b92915050565b838333604182146117a95760405162461bcd60e51b8152600401610712906131fb565b828260008181106117bc576117bc613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146117f25760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611806848461222d565b6001600160a01b03161461182c5760405162461bcd60e51b81526004016107129061329b565b84846021811461184e5760405162461bcd60e51b8152600401610712906131fb565b8181600081811061186157611861613240565b9050013560f81c60f81b6001600160f81b031916600260f81b14806118af57508181600081811061189457611894613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6118cb5760405162461bcd60e51b815260040161071290613256565b600682826040516118dd9291906133fe565b9081526040519081900360200190205460ff1661190c5760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e90611948908d908d90600401613312565b600060405180830381865af4158015611965573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261198d9190810190613326565b90506117478189896128e9565b80516020818301810180516006825292820191909301209152805460018201805460ff90921692916119cb906134d6565b80601f01602080910402602001604051908101604052809291908181526020018280546119f7906134d6565b8015611a445780601f10611a1957610100808354040283529160200191611a44565b820191906000526020600020905b815481529060010190602001808311611a2757829003601f168201915b50505050600283015460039093015491929163ffffffff80821692506401000000008204811691600160401b90041686565b838360218114611a985760405162461bcd60e51b8152600401610712906131fb565b81816000818110611aab57611aab613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611af9575081816000818110611ade57611ade613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611b155760405162461bcd60e51b815260040161071290613256565b838360218114611b375760405162461bcd60e51b8152600401610712906131fb565b81816000818110611b4a57611b4a613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611b98575081816000818110611b7d57611b7d613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611bb45760405162461bcd60e51b815260040161071290613256565b60068282604051611bc69291906133fe565b9081526040519081900360200190205460ff16611bf55760405162461bcd60e51b81526004016107129061340e565b61084a88888080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508a92508991506128e99050565b611c4061288f565b60008111611ca85760405162461bcd60e51b815260206004820152602f60248201527f4950546f6b656e5374616b696e673a206d696e526564656c6567617465416d6f60448201526e0756e742063616e6e6f74206265203608c1b6064820152608401610712565b611cd27f000000000000000000000000000000000000000000000000000000000000000082613456565b611cdc908261348e565b60045550565b848460218114611d045760405162461bcd60e51b8152600401610712906131fb565b81816000818110611d1757611d17613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611d65575081816000818110611d4a57611d4a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611d815760405162461bcd60e51b815260040161071290613256565b848460218114611da35760405162461bcd60e51b8152600401610712906131fb565b81816000818110611db657611db6613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611e04575081816000818110611de957611de9613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611e205760405162461bcd60e51b815260040161071290613256565b60068282604051611e329291906133fe565b9081526040519081900360200190205460ff16611e615760405162461bcd60e51b81526004016107129061340e565b611ea289898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152503392506124e6915050565b610e2189898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a915089905061272f565b600080611f137f000000000000000000000000000000000000000000000000000000000000000084613456565b9050611f1f818461348e565b9150915091565b611f2e61288f565b60008111611f915760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a206d696e5374616b65416d6f756e7420636044820152690616e6e6f7420626520360b41b6064820152608401610712565b611fbb7f000000000000000000000000000000000000000000000000000000000000000082613456565b611fc5908261348e565b60025550565b611fd361288f565b600081116120495760405162461bcd60e51b815260206004820152603e60248201527f4950546f6b656e5374616b696e673a206e65775769746864726177616c41646460448201527f726573734368616e6765496e74657276616c2063616e6e6f74206265203000006064820152608401610712565b600555565b61205661288f565b600180546001600160a01b0383166001600160a01b031990911681179091556120876000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b868633604182146120e25760405162461bcd60e51b8152600401610712906131fb565b828260008181106120f5576120f5613240565b9050013560f81c60f81b6001600160f81b031916600460f81b1461212b5760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661213f848461222d565b6001600160a01b0316146121655760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b815260009073__$070efe90de6222b6182e3f0710b89d2262$__90636889b16e906121a1908e908e90600401613312565b600060405180830381865af41580156121be573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526121e69190810190613326565b9050611041818a8a8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b91508a9050612284565b600061223c8260018186613510565b60405161224a9291906133fe565b6040519081900390209392505050565b600061177d836001600160a01b038416612a00565b600061177d836001600160a01b038416612a4f565b600685604051612294919061339c565b9081526040519081900360200190205460ff16156123055760405162461bcd60e51b815260206004820152602860248201527f4950546f6b656e5374616b696e673a2056616c696461746f7220616c72656164604482015267792065786973747360c01b6064820152608401610712565b60008061231134611ee6565b91509150600082116123355760405162461bcd60e51b81526004016107129061353a565b6040518060c001604052806001151581526020018781526020018381526020018663ffffffff1681526020018563ffffffff1681526020018463ffffffff16815250600688604051612387919061339c565b908152604051602091819003820190208251815460ff19169015151781559082015160018201906123b890826135cf565b506040828101516002830155606083015160039092018054608085015160a09095015163ffffffff908116600160401b026bffffffff0000000000000000199682166401000000000267ffffffffffffffff199093169190951617179390931691909117909155518290600790612430908a9061339c565b9081526020016040518091039020600082825461244d91906134a1565b9250508190555081600888604051612465919061339c565b908152602001604051809103902088604051612481919061339c565b9081526020016040518091039020600082825461249e91906134a1565b909155506124ad905081612b42565b7f5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a188787848888886040516113ae9695949392919061368e565b61250f816009846040516124fa919061339c565b90815260405190819003602001902090612bed565b61256d5760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2043616c6c6572206973206e6f7420616e6044820152681037b832b930ba37b960b91b6064820152608401610712565b5050565b80600887604051612582919061339c565b908152602001604051809103902086866040516125a09291906133fe565b90815260200160405180910390205410156125cd5760405162461bcd60e51b8152600401610712906136e1565b80600686866040516125e09291906133fe565b90815260200160405180910390206002016000828254612600919061348e565b92505081905550806006848460405161261a9291906133fe565b9081526020016040518091039020600201600082825461263a91906134a1565b9250508190555080600887604051612652919061339c565b908152602001604051809103902086866040516126709291906133fe565b9081526020016040518091039020600082825461268d919061348e565b92505081905550806008876040516126a5919061339c565b908152602001604051809103902084846040516126c39291906133fe565b908152602001604051809103902060008282546126e091906134a1565b90915550506040517fb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a9061271f9088908890889088908890889061372b565b60405180910390a1505050505050565b80600885604051612740919061339c565b9081526020016040518091039020848460405161275e9291906133fe565b908152602001604051809103902054101561278b5760405162461bcd60e51b8152600401610712906136e1565b806006848460405161279e9291906133fe565b908152602001604051809103902060020160008282546127be919061348e565b92505081905550806007856040516127d6919061339c565b908152602001604051809103902060008282546127f3919061348e565b925050819055508060088560405161280b919061339c565b908152602001604051809103902084846040516128299291906133fe565b90815260200160405180910390206000828254612846919061348e565b90915550506040517f0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b599061288190869086908690869061377a565b60405180910390a150505050565b6000546001600160a01b031633146111075760405163118cdaa760e01b8152336004820152602401610712565b600180546001600160a01b031916905561140081612c0f565b606060006128e283612c5f565b9392505050565b6000806128f534611ee6565b9150915060025482101561291b5760405162461bcd60e51b81526004016107129061353a565b816006858560405161292e9291906133fe565b9081526040519081900360200181206002018054909201909155829060079061295890889061339c565b9081526040519081900360200181208054909201909155829060089061297f90889061339c565b9081526020016040518091039020858560405161299d9291906133fe565b908152604051908190036020019020805490910190556129bc81612b42565b7fe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0858585856040516129f1949392919061377a565b60405180910390a15050505050565b6000818152600183016020526040812054612a4757508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611780565b506000611780565b60008181526001830160205260408120548015612b38576000612a7360018361348e565b8554909150600090612a879060019061348e565b9050808214612aec576000866000018281548110612aa757612aa7613240565b9060005260206000200154905080876000018481548110612aca57612aca613240565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080612afd57612afd6137b2565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611780565b6000915050611780565b604051600090339083908381818185875af1925050503d8060008114612b84576040519150601f19603f3d011682016040523d82523d6000602084013e612b89565b606091505b505090508061256d5760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a204661696c656420746f20726566756e64604482015269103932b6b0b4b73232b960b11b6064820152608401610712565b6001600160a01b0381166000908152600183016020526040812054151561177d565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b606081600001805480602002602001604051908101604052809291908181526020018280548015612caf57602002820191906000526020600020905b815481526020019060010190808311612c9b575b50505050509050919050565b60008083601f840112612ccd57600080fd5b5081356001600160401b03811115612ce457600080fd5b602083019150836020828501011115612cfc57600080fd5b9250929050565b80356001600160a01b0381168114612d1a57600080fd5b919050565b600080600060408486031215612d3457600080fd5b83356001600160401b03811115612d4a57600080fd5b612d5686828701612cbb565b9094509250612d69905060208501612d03565b90509250925092565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715612db057612db0612d72565b604052919050565b60006001600160401b03821115612dd157612dd1612d72565b50601f01601f191660200190565b600082601f830112612df057600080fd5b8135612e03612dfe82612db8565b612d88565b818152846020838601011115612e1857600080fd5b816020850160208301376000918101602001919091529392505050565b60008060408385031215612e4857600080fd5b82356001600160401b0380821115612e5f57600080fd5b612e6b86838701612ddf565b93506020850135915080821115612e8157600080fd5b50612e8e85828601612ddf565b9150509250929050565b60008060208385031215612eab57600080fd5b82356001600160401b03811115612ec157600080fd5b612ecd85828601612cbb565b90969095509350505050565b600060208284031215612eeb57600080fd5b81356001600160401b03811115612f0157600080fd5b8201608081850312156128e257600080fd5b600060208284031215612f2557600080fd5b81356001600160401b03811115612f3b57600080fd5b612f4784828501612ddf565b949350505050565b600080600080600060608688031215612f6757600080fd5b85356001600160401b0380821115612f7e57600080fd5b612f8a89838a01612cbb565b90975095506020880135915080821115612fa357600080fd5b50612fb088828901612cbb565b96999598509660400135949350505050565b600060208284031215612fd457600080fd5b5035919050565b6020808252825182820181905260009190848201906040850190845b8181101561301c5783516001600160a01b031683529284019291840191600101612ff7565b50909695505050505050565b6000806000806040858703121561303e57600080fd5b84356001600160401b038082111561305557600080fd5b61306188838901612cbb565b9096509450602087013591508082111561307a57600080fd5b5061308787828801612cbb565b95989497509550505050565b60005b838110156130ae578181015183820152602001613096565b50506000910152565b600081518084526130cf816020860160208601613093565b601f01601f19169290920160200192915050565b861515815260c0602082015260006130fe60c08301886130b7565b60408301969096525063ffffffff9384166060820152918316608083015290911660a09091015292915050565b60006020828403121561313d57600080fd5b61177d82612d03565b803563ffffffff81168114612d1a57600080fd5b600080600080600080600060a0888a03121561317557600080fd5b87356001600160401b038082111561318c57600080fd5b6131988b838c01612cbb565b909950975060208a01359150808211156131b157600080fd5b506131be8a828b01612cbb565b90965094506131d1905060408901613146565b92506131df60608901613146565b91506131ed60808901613146565b905092959891949750929550565b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206c6040820152640cadccee8d60db1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b657920706040820152640e4caccd2f60db1b606082015260800190565b6020808252602e908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206460408201526d657269766564206164647265737360901b606082015260800190565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b602081526000612f476020830184866132e9565b60006020828403121561333857600080fd5b81516001600160401b0381111561334e57600080fd5b8201601f8101841361335f57600080fd5b805161336d612dfe82612db8565b81815285602083850101111561338257600080fd5b613393826020830160208601613093565b95945050505050565b600082516133ae818460208701613093565b9190910192915050565b6000808335601e198436030181126133cf57600080fd5b8301803591506001600160401b038211156133e957600080fd5b602001915036819003821315612cfc57600080fd5b8183823760009101908152919050565b60208082526028908201527f4950546f6b656e5374616b696e673a2056616c696461746f7220646f6573206e6040820152671bdd08195e1a5cdd60c21b606082015260800190565b60008261347357634e487b7160e01b600052601260045260246000fd5b500690565b634e487b7160e01b600052601160045260246000fd5b8181038181111561178057611780613478565b8082018082111561178057611780613478565b6040815260006134c760408301856130b7565b90508260208301529392505050565b600181811c908216806134ea57607f821691505b60208210810361350a57634e487b7160e01b600052602260045260246000fd5b50919050565b6000808585111561352057600080fd5b8386111561352d57600080fd5b5050820193919092039150565b60208082526024908201527f4950546f6b656e5374616b696e673a205374616b6520616d6f756e7420746f6f604082015263206c6f7760e01b606082015260800190565b601f8211156135ca576000816000526020600020601f850160051c810160208610156135a75750805b601f850160051c820191505b818110156135c6578281556001016135b3565b5050505b505050565b81516001600160401b038111156135e8576135e8612d72565b6135fc816135f684546134d6565b8461357e565b602080601f83116001811461363157600084156136195750858301515b600019600386901b1c1916600185901b1785556135c6565b600085815260208120601f198616915b8281101561366057888601518255948401946001909101908401613641565b508582101561367e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60c0815260006136a160c08301896130b7565b82810360208401526136b381896130b7565b6040840197909752505063ffffffff9384166060820152918316608083015290911660a09091015292915050565b6020808252602a908201527f4950546f6b656e5374616b696e673a20496e73756666696369656e74207374616040820152691ad95908185b5bdd5b9d60b21b606082015260800190565b60808152600061373e60808301896130b7565b828103602084015261375181888a6132e9565b905082810360408401526137668186886132e9565b915050826060830152979650505050505050565b60608152600061378d60608301876130b7565b82810360208401526137a08186886132e9565b91505082604083015295945050505050565b634e487b7160e01b600052603160045260246000fdfea264697066735822122083dcde7a9cf30ea3ab7c2343bd23f63567f097d7f7d8321e9d2d3ceeac76555264736f6c63430008180033", +} + +// IPTokenStakingABI is the input ABI used to generate the binding from. +// Deprecated: Use IPTokenStakingMetaData.ABI instead. +var IPTokenStakingABI = IPTokenStakingMetaData.ABI + +// IPTokenStakingBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use IPTokenStakingMetaData.Bin instead. +var IPTokenStakingBin = IPTokenStakingMetaData.Bin + +// DeployIPTokenStaking deploys a new Ethereum contract, binding an instance of IPTokenStaking to it. +func DeployIPTokenStaking(auth *bind.TransactOpts, backend bind.ContractBackend, newOwner common.Address, _minStakeAmount *big.Int, _minUnstakeAmount *big.Int, _minRedelegateAmount *big.Int, stakingRounding *big.Int, _withdrawalAddressChangeInterval *big.Int, defaultCommissionRate uint32, defaultMaxCommissionRate uint32, defaultMaxCommissionChangeRate uint32) (common.Address, *types.Transaction, *IPTokenStaking, error) { + parsed, err := IPTokenStakingMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(IPTokenStakingBin), backend, newOwner, _minStakeAmount, _minUnstakeAmount, _minRedelegateAmount, stakingRounding, _withdrawalAddressChangeInterval, defaultCommissionRate, defaultMaxCommissionRate, defaultMaxCommissionChangeRate) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &IPTokenStaking{IPTokenStakingCaller: IPTokenStakingCaller{contract: contract}, IPTokenStakingTransactor: IPTokenStakingTransactor{contract: contract}, IPTokenStakingFilterer: IPTokenStakingFilterer{contract: contract}}, nil +} + +// IPTokenStaking is an auto generated Go binding around an Ethereum contract. +type IPTokenStaking struct { + IPTokenStakingCaller // Read-only binding to the contract + IPTokenStakingTransactor // Write-only binding to the contract + IPTokenStakingFilterer // Log filterer for contract events +} + +// IPTokenStakingCaller is an auto generated read-only Go binding around an Ethereum contract. +type IPTokenStakingCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPTokenStakingTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IPTokenStakingTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPTokenStakingFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IPTokenStakingFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPTokenStakingSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IPTokenStakingSession struct { + Contract *IPTokenStaking // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IPTokenStakingCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IPTokenStakingCallerSession struct { + Contract *IPTokenStakingCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IPTokenStakingTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IPTokenStakingTransactorSession struct { + Contract *IPTokenStakingTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IPTokenStakingRaw is an auto generated low-level Go binding around an Ethereum contract. +type IPTokenStakingRaw struct { + Contract *IPTokenStaking // Generic contract binding to access the raw methods on +} + +// IPTokenStakingCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IPTokenStakingCallerRaw struct { + Contract *IPTokenStakingCaller // Generic read-only contract binding to access the raw methods on +} + +// IPTokenStakingTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IPTokenStakingTransactorRaw struct { + Contract *IPTokenStakingTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIPTokenStaking creates a new instance of IPTokenStaking, bound to a specific deployed contract. +func NewIPTokenStaking(address common.Address, backend bind.ContractBackend) (*IPTokenStaking, error) { + contract, err := bindIPTokenStaking(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IPTokenStaking{IPTokenStakingCaller: IPTokenStakingCaller{contract: contract}, IPTokenStakingTransactor: IPTokenStakingTransactor{contract: contract}, IPTokenStakingFilterer: IPTokenStakingFilterer{contract: contract}}, nil +} + +// NewIPTokenStakingCaller creates a new read-only instance of IPTokenStaking, bound to a specific deployed contract. +func NewIPTokenStakingCaller(address common.Address, caller bind.ContractCaller) (*IPTokenStakingCaller, error) { + contract, err := bindIPTokenStaking(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IPTokenStakingCaller{contract: contract}, nil +} + +// NewIPTokenStakingTransactor creates a new write-only instance of IPTokenStaking, bound to a specific deployed contract. +func NewIPTokenStakingTransactor(address common.Address, transactor bind.ContractTransactor) (*IPTokenStakingTransactor, error) { + contract, err := bindIPTokenStaking(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IPTokenStakingTransactor{contract: contract}, nil +} + +// NewIPTokenStakingFilterer creates a new log filterer instance of IPTokenStaking, bound to a specific deployed contract. +func NewIPTokenStakingFilterer(address common.Address, filterer bind.ContractFilterer) (*IPTokenStakingFilterer, error) { + contract, err := bindIPTokenStaking(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IPTokenStakingFilterer{contract: contract}, nil +} + +// bindIPTokenStaking binds a generic wrapper to an already deployed contract. +func bindIPTokenStaking(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IPTokenStakingMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IPTokenStaking *IPTokenStakingRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IPTokenStaking.Contract.IPTokenStakingCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IPTokenStaking *IPTokenStakingRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenStaking.Contract.IPTokenStakingTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IPTokenStaking *IPTokenStakingRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IPTokenStaking.Contract.IPTokenStakingTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IPTokenStaking *IPTokenStakingCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IPTokenStaking.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IPTokenStaking *IPTokenStakingTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenStaking.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IPTokenStaking *IPTokenStakingTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IPTokenStaking.Contract.contract.Transact(opts, method, params...) +} + +// DEFAULTCOMMISSIONRATE is a free data retrieval call binding the contract method 0xb8db983e. +// +// Solidity: function DEFAULT_COMMISSION_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingCaller) DEFAULTCOMMISSIONRATE(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "DEFAULT_COMMISSION_RATE") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// DEFAULTCOMMISSIONRATE is a free data retrieval call binding the contract method 0xb8db983e. +// +// Solidity: function DEFAULT_COMMISSION_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingSession) DEFAULTCOMMISSIONRATE() (uint32, error) { + return _IPTokenStaking.Contract.DEFAULTCOMMISSIONRATE(&_IPTokenStaking.CallOpts) +} + +// DEFAULTCOMMISSIONRATE is a free data retrieval call binding the contract method 0xb8db983e. +// +// Solidity: function DEFAULT_COMMISSION_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingCallerSession) DEFAULTCOMMISSIONRATE() (uint32, error) { + return _IPTokenStaking.Contract.DEFAULTCOMMISSIONRATE(&_IPTokenStaking.CallOpts) +} + +// DEFAULTMAXCOMMISSIONCHANGERATE is a free data retrieval call binding the contract method 0xfc56c2a2. +// +// Solidity: function DEFAULT_MAX_COMMISSION_CHANGE_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingCaller) DEFAULTMAXCOMMISSIONCHANGERATE(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "DEFAULT_MAX_COMMISSION_CHANGE_RATE") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// DEFAULTMAXCOMMISSIONCHANGERATE is a free data retrieval call binding the contract method 0xfc56c2a2. +// +// Solidity: function DEFAULT_MAX_COMMISSION_CHANGE_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingSession) DEFAULTMAXCOMMISSIONCHANGERATE() (uint32, error) { + return _IPTokenStaking.Contract.DEFAULTMAXCOMMISSIONCHANGERATE(&_IPTokenStaking.CallOpts) +} + +// DEFAULTMAXCOMMISSIONCHANGERATE is a free data retrieval call binding the contract method 0xfc56c2a2. +// +// Solidity: function DEFAULT_MAX_COMMISSION_CHANGE_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingCallerSession) DEFAULTMAXCOMMISSIONCHANGERATE() (uint32, error) { + return _IPTokenStaking.Contract.DEFAULTMAXCOMMISSIONCHANGERATE(&_IPTokenStaking.CallOpts) +} + +// DEFAULTMAXCOMMISSIONRATE is a free data retrieval call binding the contract method 0x2ebc6034. +// +// Solidity: function DEFAULT_MAX_COMMISSION_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingCaller) DEFAULTMAXCOMMISSIONRATE(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "DEFAULT_MAX_COMMISSION_RATE") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// DEFAULTMAXCOMMISSIONRATE is a free data retrieval call binding the contract method 0x2ebc6034. +// +// Solidity: function DEFAULT_MAX_COMMISSION_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingSession) DEFAULTMAXCOMMISSIONRATE() (uint32, error) { + return _IPTokenStaking.Contract.DEFAULTMAXCOMMISSIONRATE(&_IPTokenStaking.CallOpts) +} + +// DEFAULTMAXCOMMISSIONRATE is a free data retrieval call binding the contract method 0x2ebc6034. +// +// Solidity: function DEFAULT_MAX_COMMISSION_RATE() view returns(uint32) +func (_IPTokenStaking *IPTokenStakingCallerSession) DEFAULTMAXCOMMISSIONRATE() (uint32, error) { + return _IPTokenStaking.Contract.DEFAULTMAXCOMMISSIONRATE(&_IPTokenStaking.CallOpts) +} + +// STAKEROUNDING is a free data retrieval call binding the contract method 0xbda16b15. +// +// Solidity: function STAKE_ROUNDING() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCaller) STAKEROUNDING(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "STAKE_ROUNDING") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// STAKEROUNDING is a free data retrieval call binding the contract method 0xbda16b15. +// +// Solidity: function STAKE_ROUNDING() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingSession) STAKEROUNDING() (*big.Int, error) { + return _IPTokenStaking.Contract.STAKEROUNDING(&_IPTokenStaking.CallOpts) +} + +// STAKEROUNDING is a free data retrieval call binding the contract method 0xbda16b15. +// +// Solidity: function STAKE_ROUNDING() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCallerSession) STAKEROUNDING() (*big.Int, error) { + return _IPTokenStaking.Contract.STAKEROUNDING(&_IPTokenStaking.CallOpts) +} + +// DelegatorTotalStakes is a free data retrieval call binding the contract method 0x57067503. +// +// Solidity: function delegatorTotalStakes(bytes delegatorPubkey) view returns(uint256 stakedAmount) +func (_IPTokenStaking *IPTokenStakingCaller) DelegatorTotalStakes(opts *bind.CallOpts, delegatorPubkey []byte) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "delegatorTotalStakes", delegatorPubkey) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// DelegatorTotalStakes is a free data retrieval call binding the contract method 0x57067503. +// +// Solidity: function delegatorTotalStakes(bytes delegatorPubkey) view returns(uint256 stakedAmount) +func (_IPTokenStaking *IPTokenStakingSession) DelegatorTotalStakes(delegatorPubkey []byte) (*big.Int, error) { + return _IPTokenStaking.Contract.DelegatorTotalStakes(&_IPTokenStaking.CallOpts, delegatorPubkey) +} + +// DelegatorTotalStakes is a free data retrieval call binding the contract method 0x57067503. +// +// Solidity: function delegatorTotalStakes(bytes delegatorPubkey) view returns(uint256 stakedAmount) +func (_IPTokenStaking *IPTokenStakingCallerSession) DelegatorTotalStakes(delegatorPubkey []byte) (*big.Int, error) { + return _IPTokenStaking.Contract.DelegatorTotalStakes(&_IPTokenStaking.CallOpts, delegatorPubkey) +} + +// DelegatorValidatorStakes is a free data retrieval call binding the contract method 0x2d1e973e. +// +// Solidity: function delegatorValidatorStakes(bytes delegatorPubkey, bytes validatorPubkey) view returns(uint256 stakedAmount) +func (_IPTokenStaking *IPTokenStakingCaller) DelegatorValidatorStakes(opts *bind.CallOpts, delegatorPubkey []byte, validatorPubkey []byte) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "delegatorValidatorStakes", delegatorPubkey, validatorPubkey) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// DelegatorValidatorStakes is a free data retrieval call binding the contract method 0x2d1e973e. +// +// Solidity: function delegatorValidatorStakes(bytes delegatorPubkey, bytes validatorPubkey) view returns(uint256 stakedAmount) +func (_IPTokenStaking *IPTokenStakingSession) DelegatorValidatorStakes(delegatorPubkey []byte, validatorPubkey []byte) (*big.Int, error) { + return _IPTokenStaking.Contract.DelegatorValidatorStakes(&_IPTokenStaking.CallOpts, delegatorPubkey, validatorPubkey) +} + +// DelegatorValidatorStakes is a free data retrieval call binding the contract method 0x2d1e973e. +// +// Solidity: function delegatorValidatorStakes(bytes delegatorPubkey, bytes validatorPubkey) view returns(uint256 stakedAmount) +func (_IPTokenStaking *IPTokenStakingCallerSession) DelegatorValidatorStakes(delegatorPubkey []byte, validatorPubkey []byte) (*big.Int, error) { + return _IPTokenStaking.Contract.DelegatorValidatorStakes(&_IPTokenStaking.CallOpts, delegatorPubkey, validatorPubkey) +} + +// GetOperators is a free data retrieval call binding the contract method 0x83dffd6f. +// +// Solidity: function getOperators(bytes pubkey) view returns(address[]) +func (_IPTokenStaking *IPTokenStakingCaller) GetOperators(opts *bind.CallOpts, pubkey []byte) ([]common.Address, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "getOperators", pubkey) + + if err != nil { + return *new([]common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]common.Address)).(*[]common.Address) + + return out0, err + +} + +// GetOperators is a free data retrieval call binding the contract method 0x83dffd6f. +// +// Solidity: function getOperators(bytes pubkey) view returns(address[]) +func (_IPTokenStaking *IPTokenStakingSession) GetOperators(pubkey []byte) ([]common.Address, error) { + return _IPTokenStaking.Contract.GetOperators(&_IPTokenStaking.CallOpts, pubkey) +} + +// GetOperators is a free data retrieval call binding the contract method 0x83dffd6f. +// +// Solidity: function getOperators(bytes pubkey) view returns(address[]) +func (_IPTokenStaking *IPTokenStakingCallerSession) GetOperators(pubkey []byte) ([]common.Address, error) { + return _IPTokenStaking.Contract.GetOperators(&_IPTokenStaking.CallOpts, pubkey) +} + +// MinRedelegateAmount is a free data retrieval call binding the contract method 0x5a69825d. +// +// Solidity: function minRedelegateAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCaller) MinRedelegateAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "minRedelegateAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MinRedelegateAmount is a free data retrieval call binding the contract method 0x5a69825d. +// +// Solidity: function minRedelegateAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingSession) MinRedelegateAmount() (*big.Int, error) { + return _IPTokenStaking.Contract.MinRedelegateAmount(&_IPTokenStaking.CallOpts) +} + +// MinRedelegateAmount is a free data retrieval call binding the contract method 0x5a69825d. +// +// Solidity: function minRedelegateAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCallerSession) MinRedelegateAmount() (*big.Int, error) { + return _IPTokenStaking.Contract.MinRedelegateAmount(&_IPTokenStaking.CallOpts) +} + +// MinStakeAmount is a free data retrieval call binding the contract method 0xf1887684. +// +// Solidity: function minStakeAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCaller) MinStakeAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "minStakeAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MinStakeAmount is a free data retrieval call binding the contract method 0xf1887684. +// +// Solidity: function minStakeAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingSession) MinStakeAmount() (*big.Int, error) { + return _IPTokenStaking.Contract.MinStakeAmount(&_IPTokenStaking.CallOpts) +} + +// MinStakeAmount is a free data retrieval call binding the contract method 0xf1887684. +// +// Solidity: function minStakeAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCallerSession) MinStakeAmount() (*big.Int, error) { + return _IPTokenStaking.Contract.MinStakeAmount(&_IPTokenStaking.CallOpts) +} + +// MinUnstakeAmount is a free data retrieval call binding the contract method 0x39ec4df9. +// +// Solidity: function minUnstakeAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCaller) MinUnstakeAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "minUnstakeAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MinUnstakeAmount is a free data retrieval call binding the contract method 0x39ec4df9. +// +// Solidity: function minUnstakeAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingSession) MinUnstakeAmount() (*big.Int, error) { + return _IPTokenStaking.Contract.MinUnstakeAmount(&_IPTokenStaking.CallOpts) +} + +// MinUnstakeAmount is a free data retrieval call binding the contract method 0x39ec4df9. +// +// Solidity: function minUnstakeAmount() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCallerSession) MinUnstakeAmount() (*big.Int, error) { + return _IPTokenStaking.Contract.MinUnstakeAmount(&_IPTokenStaking.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IPTokenStaking *IPTokenStakingCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IPTokenStaking *IPTokenStakingSession) Owner() (common.Address, error) { + return _IPTokenStaking.Contract.Owner(&_IPTokenStaking.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_IPTokenStaking *IPTokenStakingCallerSession) Owner() (common.Address, error) { + return _IPTokenStaking.Contract.Owner(&_IPTokenStaking.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_IPTokenStaking *IPTokenStakingCaller) PendingOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "pendingOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_IPTokenStaking *IPTokenStakingSession) PendingOwner() (common.Address, error) { + return _IPTokenStaking.Contract.PendingOwner(&_IPTokenStaking.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_IPTokenStaking *IPTokenStakingCallerSession) PendingOwner() (common.Address, error) { + return _IPTokenStaking.Contract.PendingOwner(&_IPTokenStaking.CallOpts) +} + +// RoundedStakeAmount is a free data retrieval call binding the contract method 0xd2e1f5b8. +// +// Solidity: function roundedStakeAmount(uint256 rawAmount) view returns(uint256 amount, uint256 remainder) +func (_IPTokenStaking *IPTokenStakingCaller) RoundedStakeAmount(opts *bind.CallOpts, rawAmount *big.Int) (struct { + Amount *big.Int + Remainder *big.Int +}, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "roundedStakeAmount", rawAmount) + + outstruct := new(struct { + Amount *big.Int + Remainder *big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.Amount = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + outstruct.Remainder = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int) + + return *outstruct, err + +} + +// RoundedStakeAmount is a free data retrieval call binding the contract method 0xd2e1f5b8. +// +// Solidity: function roundedStakeAmount(uint256 rawAmount) view returns(uint256 amount, uint256 remainder) +func (_IPTokenStaking *IPTokenStakingSession) RoundedStakeAmount(rawAmount *big.Int) (struct { + Amount *big.Int + Remainder *big.Int +}, error) { + return _IPTokenStaking.Contract.RoundedStakeAmount(&_IPTokenStaking.CallOpts, rawAmount) +} + +// RoundedStakeAmount is a free data retrieval call binding the contract method 0xd2e1f5b8. +// +// Solidity: function roundedStakeAmount(uint256 rawAmount) view returns(uint256 amount, uint256 remainder) +func (_IPTokenStaking *IPTokenStakingCallerSession) RoundedStakeAmount(rawAmount *big.Int) (struct { + Amount *big.Int + Remainder *big.Int +}, error) { + return _IPTokenStaking.Contract.RoundedStakeAmount(&_IPTokenStaking.CallOpts, rawAmount) +} + +// ValidatorMetadata is a free data retrieval call binding the contract method 0x8d3e1e41. +// +// Solidity: function validatorMetadata(bytes validatorPubkey) view returns(bool exists, string moniker, uint256 totalStake, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) +func (_IPTokenStaking *IPTokenStakingCaller) ValidatorMetadata(opts *bind.CallOpts, validatorPubkey []byte) (struct { + Exists bool + Moniker string + TotalStake *big.Int + CommissionRate uint32 + MaxCommissionRate uint32 + MaxCommissionChangeRate uint32 +}, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "validatorMetadata", validatorPubkey) + + outstruct := new(struct { + Exists bool + Moniker string + TotalStake *big.Int + CommissionRate uint32 + MaxCommissionRate uint32 + MaxCommissionChangeRate uint32 + }) + if err != nil { + return *outstruct, err + } + + outstruct.Exists = *abi.ConvertType(out[0], new(bool)).(*bool) + outstruct.Moniker = *abi.ConvertType(out[1], new(string)).(*string) + outstruct.TotalStake = *abi.ConvertType(out[2], new(*big.Int)).(**big.Int) + outstruct.CommissionRate = *abi.ConvertType(out[3], new(uint32)).(*uint32) + outstruct.MaxCommissionRate = *abi.ConvertType(out[4], new(uint32)).(*uint32) + outstruct.MaxCommissionChangeRate = *abi.ConvertType(out[5], new(uint32)).(*uint32) + + return *outstruct, err + +} + +// ValidatorMetadata is a free data retrieval call binding the contract method 0x8d3e1e41. +// +// Solidity: function validatorMetadata(bytes validatorPubkey) view returns(bool exists, string moniker, uint256 totalStake, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) +func (_IPTokenStaking *IPTokenStakingSession) ValidatorMetadata(validatorPubkey []byte) (struct { + Exists bool + Moniker string + TotalStake *big.Int + CommissionRate uint32 + MaxCommissionRate uint32 + MaxCommissionChangeRate uint32 +}, error) { + return _IPTokenStaking.Contract.ValidatorMetadata(&_IPTokenStaking.CallOpts, validatorPubkey) +} + +// ValidatorMetadata is a free data retrieval call binding the contract method 0x8d3e1e41. +// +// Solidity: function validatorMetadata(bytes validatorPubkey) view returns(bool exists, string moniker, uint256 totalStake, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) +func (_IPTokenStaking *IPTokenStakingCallerSession) ValidatorMetadata(validatorPubkey []byte) (struct { + Exists bool + Moniker string + TotalStake *big.Int + CommissionRate uint32 + MaxCommissionRate uint32 + MaxCommissionChangeRate uint32 +}, error) { + return _IPTokenStaking.Contract.ValidatorMetadata(&_IPTokenStaking.CallOpts, validatorPubkey) +} + +// WithdrawalAddressChange is a free data retrieval call binding the contract method 0xc24ae586. +// +// Solidity: function withdrawalAddressChange(bytes delegatorPubkey) view returns(uint256 lastChange) +func (_IPTokenStaking *IPTokenStakingCaller) WithdrawalAddressChange(opts *bind.CallOpts, delegatorPubkey []byte) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "withdrawalAddressChange", delegatorPubkey) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// WithdrawalAddressChange is a free data retrieval call binding the contract method 0xc24ae586. +// +// Solidity: function withdrawalAddressChange(bytes delegatorPubkey) view returns(uint256 lastChange) +func (_IPTokenStaking *IPTokenStakingSession) WithdrawalAddressChange(delegatorPubkey []byte) (*big.Int, error) { + return _IPTokenStaking.Contract.WithdrawalAddressChange(&_IPTokenStaking.CallOpts, delegatorPubkey) +} + +// WithdrawalAddressChange is a free data retrieval call binding the contract method 0xc24ae586. +// +// Solidity: function withdrawalAddressChange(bytes delegatorPubkey) view returns(uint256 lastChange) +func (_IPTokenStaking *IPTokenStakingCallerSession) WithdrawalAddressChange(delegatorPubkey []byte) (*big.Int, error) { + return _IPTokenStaking.Contract.WithdrawalAddressChange(&_IPTokenStaking.CallOpts, delegatorPubkey) +} + +// WithdrawalAddressChangeInterval is a free data retrieval call binding the contract method 0x060ceab0. +// +// Solidity: function withdrawalAddressChangeInterval() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCaller) WithdrawalAddressChangeInterval(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _IPTokenStaking.contract.Call(opts, &out, "withdrawalAddressChangeInterval") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// WithdrawalAddressChangeInterval is a free data retrieval call binding the contract method 0x060ceab0. +// +// Solidity: function withdrawalAddressChangeInterval() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingSession) WithdrawalAddressChangeInterval() (*big.Int, error) { + return _IPTokenStaking.Contract.WithdrawalAddressChangeInterval(&_IPTokenStaking.CallOpts) +} + +// WithdrawalAddressChangeInterval is a free data retrieval call binding the contract method 0x060ceab0. +// +// Solidity: function withdrawalAddressChangeInterval() view returns(uint256) +func (_IPTokenStaking *IPTokenStakingCallerSession) WithdrawalAddressChangeInterval() (*big.Int, error) { + return _IPTokenStaking.Contract.WithdrawalAddressChangeInterval(&_IPTokenStaking.CallOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_IPTokenStaking *IPTokenStakingTransactor) AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "acceptOwnership") +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_IPTokenStaking *IPTokenStakingSession) AcceptOwnership() (*types.Transaction, error) { + return _IPTokenStaking.Contract.AcceptOwnership(&_IPTokenStaking.TransactOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) AcceptOwnership() (*types.Transaction, error) { + return _IPTokenStaking.Contract.AcceptOwnership(&_IPTokenStaking.TransactOpts) +} + +// AddOperator is a paid mutator transaction binding the contract method 0x057b9296. +// +// Solidity: function addOperator(bytes uncmpPubkey, address operator) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) AddOperator(opts *bind.TransactOpts, uncmpPubkey []byte, operator common.Address) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "addOperator", uncmpPubkey, operator) +} + +// AddOperator is a paid mutator transaction binding the contract method 0x057b9296. +// +// Solidity: function addOperator(bytes uncmpPubkey, address operator) returns() +func (_IPTokenStaking *IPTokenStakingSession) AddOperator(uncmpPubkey []byte, operator common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.AddOperator(&_IPTokenStaking.TransactOpts, uncmpPubkey, operator) +} + +// AddOperator is a paid mutator transaction binding the contract method 0x057b9296. +// +// Solidity: function addOperator(bytes uncmpPubkey, address operator) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) AddOperator(uncmpPubkey []byte, operator common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.AddOperator(&_IPTokenStaking.TransactOpts, uncmpPubkey, operator) +} + +// CreateValidator is a paid mutator transaction binding the contract method 0xfc2e5932. +// +// Solidity: function createValidator(bytes validatorUncmpPubkey, string moniker, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactor) CreateValidator(opts *bind.TransactOpts, validatorUncmpPubkey []byte, moniker string, commissionRate uint32, maxCommissionRate uint32, maxCommissionChangeRate uint32) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "createValidator", validatorUncmpPubkey, moniker, commissionRate, maxCommissionRate, maxCommissionChangeRate) +} + +// CreateValidator is a paid mutator transaction binding the contract method 0xfc2e5932. +// +// Solidity: function createValidator(bytes validatorUncmpPubkey, string moniker, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) payable returns() +func (_IPTokenStaking *IPTokenStakingSession) CreateValidator(validatorUncmpPubkey []byte, moniker string, commissionRate uint32, maxCommissionRate uint32, maxCommissionChangeRate uint32) (*types.Transaction, error) { + return _IPTokenStaking.Contract.CreateValidator(&_IPTokenStaking.TransactOpts, validatorUncmpPubkey, moniker, commissionRate, maxCommissionRate, maxCommissionChangeRate) +} + +// CreateValidator is a paid mutator transaction binding the contract method 0xfc2e5932. +// +// Solidity: function createValidator(bytes validatorUncmpPubkey, string moniker, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) CreateValidator(validatorUncmpPubkey []byte, moniker string, commissionRate uint32, maxCommissionRate uint32, maxCommissionChangeRate uint32) (*types.Transaction, error) { + return _IPTokenStaking.Contract.CreateValidator(&_IPTokenStaking.TransactOpts, validatorUncmpPubkey, moniker, commissionRate, maxCommissionRate, maxCommissionChangeRate) +} + +// CreateValidatorOnBehalf is a paid mutator transaction binding the contract method 0x48903e38. +// +// Solidity: function createValidatorOnBehalf(bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactor) CreateValidatorOnBehalf(opts *bind.TransactOpts, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "createValidatorOnBehalf", validatorPubkey) +} + +// CreateValidatorOnBehalf is a paid mutator transaction binding the contract method 0x48903e38. +// +// Solidity: function createValidatorOnBehalf(bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingSession) CreateValidatorOnBehalf(validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.Contract.CreateValidatorOnBehalf(&_IPTokenStaking.TransactOpts, validatorPubkey) +} + +// CreateValidatorOnBehalf is a paid mutator transaction binding the contract method 0x48903e38. +// +// Solidity: function createValidatorOnBehalf(bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) CreateValidatorOnBehalf(validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.Contract.CreateValidatorOnBehalf(&_IPTokenStaking.TransactOpts, validatorPubkey) +} + +// Redelegate is a paid mutator transaction binding the contract method 0x7b6e842c. +// +// Solidity: function redelegate((bytes,bytes,bytes,uint256) p) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) Redelegate(opts *bind.TransactOpts, p IIPTokenStakingRedelegateParams) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "redelegate", p) +} + +// Redelegate is a paid mutator transaction binding the contract method 0x7b6e842c. +// +// Solidity: function redelegate((bytes,bytes,bytes,uint256) p) returns() +func (_IPTokenStaking *IPTokenStakingSession) Redelegate(p IIPTokenStakingRedelegateParams) (*types.Transaction, error) { + return _IPTokenStaking.Contract.Redelegate(&_IPTokenStaking.TransactOpts, p) +} + +// Redelegate is a paid mutator transaction binding the contract method 0x7b6e842c. +// +// Solidity: function redelegate((bytes,bytes,bytes,uint256) p) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) Redelegate(p IIPTokenStakingRedelegateParams) (*types.Transaction, error) { + return _IPTokenStaking.Contract.Redelegate(&_IPTokenStaking.TransactOpts, p) +} + +// RedelegateOnBehalf is a paid mutator transaction binding the contract method 0x53972c2a. +// +// Solidity: function redelegateOnBehalf((bytes,bytes,bytes,uint256) p) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) RedelegateOnBehalf(opts *bind.TransactOpts, p IIPTokenStakingRedelegateParams) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "redelegateOnBehalf", p) +} + +// RedelegateOnBehalf is a paid mutator transaction binding the contract method 0x53972c2a. +// +// Solidity: function redelegateOnBehalf((bytes,bytes,bytes,uint256) p) returns() +func (_IPTokenStaking *IPTokenStakingSession) RedelegateOnBehalf(p IIPTokenStakingRedelegateParams) (*types.Transaction, error) { + return _IPTokenStaking.Contract.RedelegateOnBehalf(&_IPTokenStaking.TransactOpts, p) +} + +// RedelegateOnBehalf is a paid mutator transaction binding the contract method 0x53972c2a. +// +// Solidity: function redelegateOnBehalf((bytes,bytes,bytes,uint256) p) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) RedelegateOnBehalf(p IIPTokenStakingRedelegateParams) (*types.Transaction, error) { + return _IPTokenStaking.Contract.RedelegateOnBehalf(&_IPTokenStaking.TransactOpts, p) +} + +// RemoveOperator is a paid mutator transaction binding the contract method 0x17e42e12. +// +// Solidity: function removeOperator(bytes uncmpPubkey, address operator) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) RemoveOperator(opts *bind.TransactOpts, uncmpPubkey []byte, operator common.Address) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "removeOperator", uncmpPubkey, operator) +} + +// RemoveOperator is a paid mutator transaction binding the contract method 0x17e42e12. +// +// Solidity: function removeOperator(bytes uncmpPubkey, address operator) returns() +func (_IPTokenStaking *IPTokenStakingSession) RemoveOperator(uncmpPubkey []byte, operator common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.RemoveOperator(&_IPTokenStaking.TransactOpts, uncmpPubkey, operator) +} + +// RemoveOperator is a paid mutator transaction binding the contract method 0x17e42e12. +// +// Solidity: function removeOperator(bytes uncmpPubkey, address operator) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) RemoveOperator(uncmpPubkey []byte, operator common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.RemoveOperator(&_IPTokenStaking.TransactOpts, uncmpPubkey, operator) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IPTokenStaking *IPTokenStakingTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IPTokenStaking *IPTokenStakingSession) RenounceOwnership() (*types.Transaction, error) { + return _IPTokenStaking.Contract.RenounceOwnership(&_IPTokenStaking.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _IPTokenStaking.Contract.RenounceOwnership(&_IPTokenStaking.TransactOpts) +} + +// SetMinRedelegateAmount is a paid mutator transaction binding the contract method 0x9855c8b5. +// +// Solidity: function setMinRedelegateAmount(uint256 newMinRedelegateAmount) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) SetMinRedelegateAmount(opts *bind.TransactOpts, newMinRedelegateAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "setMinRedelegateAmount", newMinRedelegateAmount) +} + +// SetMinRedelegateAmount is a paid mutator transaction binding the contract method 0x9855c8b5. +// +// Solidity: function setMinRedelegateAmount(uint256 newMinRedelegateAmount) returns() +func (_IPTokenStaking *IPTokenStakingSession) SetMinRedelegateAmount(newMinRedelegateAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetMinRedelegateAmount(&_IPTokenStaking.TransactOpts, newMinRedelegateAmount) +} + +// SetMinRedelegateAmount is a paid mutator transaction binding the contract method 0x9855c8b5. +// +// Solidity: function setMinRedelegateAmount(uint256 newMinRedelegateAmount) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) SetMinRedelegateAmount(newMinRedelegateAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetMinRedelegateAmount(&_IPTokenStaking.TransactOpts, newMinRedelegateAmount) +} + +// SetMinStakeAmount is a paid mutator transaction binding the contract method 0xeb4af045. +// +// Solidity: function setMinStakeAmount(uint256 newMinStakeAmount) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) SetMinStakeAmount(opts *bind.TransactOpts, newMinStakeAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "setMinStakeAmount", newMinStakeAmount) +} + +// SetMinStakeAmount is a paid mutator transaction binding the contract method 0xeb4af045. +// +// Solidity: function setMinStakeAmount(uint256 newMinStakeAmount) returns() +func (_IPTokenStaking *IPTokenStakingSession) SetMinStakeAmount(newMinStakeAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetMinStakeAmount(&_IPTokenStaking.TransactOpts, newMinStakeAmount) +} + +// SetMinStakeAmount is a paid mutator transaction binding the contract method 0xeb4af045. +// +// Solidity: function setMinStakeAmount(uint256 newMinStakeAmount) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) SetMinStakeAmount(newMinStakeAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetMinStakeAmount(&_IPTokenStaking.TransactOpts, newMinStakeAmount) +} + +// SetMinUnstakeAmount is a paid mutator transaction binding the contract method 0x6ea3a228. +// +// Solidity: function setMinUnstakeAmount(uint256 newMinUnstakeAmount) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) SetMinUnstakeAmount(opts *bind.TransactOpts, newMinUnstakeAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "setMinUnstakeAmount", newMinUnstakeAmount) +} + +// SetMinUnstakeAmount is a paid mutator transaction binding the contract method 0x6ea3a228. +// +// Solidity: function setMinUnstakeAmount(uint256 newMinUnstakeAmount) returns() +func (_IPTokenStaking *IPTokenStakingSession) SetMinUnstakeAmount(newMinUnstakeAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetMinUnstakeAmount(&_IPTokenStaking.TransactOpts, newMinUnstakeAmount) +} + +// SetMinUnstakeAmount is a paid mutator transaction binding the contract method 0x6ea3a228. +// +// Solidity: function setMinUnstakeAmount(uint256 newMinUnstakeAmount) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) SetMinUnstakeAmount(newMinUnstakeAmount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetMinUnstakeAmount(&_IPTokenStaking.TransactOpts, newMinUnstakeAmount) +} + +// SetWithdrawalAddress is a paid mutator transaction binding the contract method 0x787f82c8. +// +// Solidity: function setWithdrawalAddress(bytes delegatorUncmpPubkey, address newWithdrawalAddress) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) SetWithdrawalAddress(opts *bind.TransactOpts, delegatorUncmpPubkey []byte, newWithdrawalAddress common.Address) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "setWithdrawalAddress", delegatorUncmpPubkey, newWithdrawalAddress) +} + +// SetWithdrawalAddress is a paid mutator transaction binding the contract method 0x787f82c8. +// +// Solidity: function setWithdrawalAddress(bytes delegatorUncmpPubkey, address newWithdrawalAddress) returns() +func (_IPTokenStaking *IPTokenStakingSession) SetWithdrawalAddress(delegatorUncmpPubkey []byte, newWithdrawalAddress common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetWithdrawalAddress(&_IPTokenStaking.TransactOpts, delegatorUncmpPubkey, newWithdrawalAddress) +} + +// SetWithdrawalAddress is a paid mutator transaction binding the contract method 0x787f82c8. +// +// Solidity: function setWithdrawalAddress(bytes delegatorUncmpPubkey, address newWithdrawalAddress) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) SetWithdrawalAddress(delegatorUncmpPubkey []byte, newWithdrawalAddress common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetWithdrawalAddress(&_IPTokenStaking.TransactOpts, delegatorUncmpPubkey, newWithdrawalAddress) +} + +// SetWithdrawalAddressChangeInterval is a paid mutator transaction binding the contract method 0xeee5cead. +// +// Solidity: function setWithdrawalAddressChangeInterval(uint256 newWithdrawalAddressChangeInterval) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) SetWithdrawalAddressChangeInterval(opts *bind.TransactOpts, newWithdrawalAddressChangeInterval *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "setWithdrawalAddressChangeInterval", newWithdrawalAddressChangeInterval) +} + +// SetWithdrawalAddressChangeInterval is a paid mutator transaction binding the contract method 0xeee5cead. +// +// Solidity: function setWithdrawalAddressChangeInterval(uint256 newWithdrawalAddressChangeInterval) returns() +func (_IPTokenStaking *IPTokenStakingSession) SetWithdrawalAddressChangeInterval(newWithdrawalAddressChangeInterval *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetWithdrawalAddressChangeInterval(&_IPTokenStaking.TransactOpts, newWithdrawalAddressChangeInterval) +} + +// SetWithdrawalAddressChangeInterval is a paid mutator transaction binding the contract method 0xeee5cead. +// +// Solidity: function setWithdrawalAddressChangeInterval(uint256 newWithdrawalAddressChangeInterval) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) SetWithdrawalAddressChangeInterval(newWithdrawalAddressChangeInterval *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.SetWithdrawalAddressChangeInterval(&_IPTokenStaking.TransactOpts, newWithdrawalAddressChangeInterval) +} + +// Stake is a paid mutator transaction binding the contract method 0x86eec4a1. +// +// Solidity: function stake(bytes delegatorUncmpPubkey, bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactor) Stake(opts *bind.TransactOpts, delegatorUncmpPubkey []byte, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "stake", delegatorUncmpPubkey, validatorPubkey) +} + +// Stake is a paid mutator transaction binding the contract method 0x86eec4a1. +// +// Solidity: function stake(bytes delegatorUncmpPubkey, bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingSession) Stake(delegatorUncmpPubkey []byte, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.Contract.Stake(&_IPTokenStaking.TransactOpts, delegatorUncmpPubkey, validatorPubkey) +} + +// Stake is a paid mutator transaction binding the contract method 0x86eec4a1. +// +// Solidity: function stake(bytes delegatorUncmpPubkey, bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) Stake(delegatorUncmpPubkey []byte, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.Contract.Stake(&_IPTokenStaking.TransactOpts, delegatorUncmpPubkey, validatorPubkey) +} + +// StakeOnBehalf is a paid mutator transaction binding the contract method 0x8f37ec19. +// +// Solidity: function stakeOnBehalf(bytes delegatorPubkey, bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactor) StakeOnBehalf(opts *bind.TransactOpts, delegatorPubkey []byte, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "stakeOnBehalf", delegatorPubkey, validatorPubkey) +} + +// StakeOnBehalf is a paid mutator transaction binding the contract method 0x8f37ec19. +// +// Solidity: function stakeOnBehalf(bytes delegatorPubkey, bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingSession) StakeOnBehalf(delegatorPubkey []byte, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.Contract.StakeOnBehalf(&_IPTokenStaking.TransactOpts, delegatorPubkey, validatorPubkey) +} + +// StakeOnBehalf is a paid mutator transaction binding the contract method 0x8f37ec19. +// +// Solidity: function stakeOnBehalf(bytes delegatorPubkey, bytes validatorPubkey) payable returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) StakeOnBehalf(delegatorPubkey []byte, validatorPubkey []byte) (*types.Transaction, error) { + return _IPTokenStaking.Contract.StakeOnBehalf(&_IPTokenStaking.TransactOpts, delegatorPubkey, validatorPubkey) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IPTokenStaking *IPTokenStakingSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.TransferOwnership(&_IPTokenStaking.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _IPTokenStaking.Contract.TransferOwnership(&_IPTokenStaking.TransactOpts, newOwner) +} + +// Unstake is a paid mutator transaction binding the contract method 0x5d5ab968. +// +// Solidity: function unstake(bytes delegatorUncmpPubkey, bytes validatorPubkey, uint256 amount) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) Unstake(opts *bind.TransactOpts, delegatorUncmpPubkey []byte, validatorPubkey []byte, amount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "unstake", delegatorUncmpPubkey, validatorPubkey, amount) +} + +// Unstake is a paid mutator transaction binding the contract method 0x5d5ab968. +// +// Solidity: function unstake(bytes delegatorUncmpPubkey, bytes validatorPubkey, uint256 amount) returns() +func (_IPTokenStaking *IPTokenStakingSession) Unstake(delegatorUncmpPubkey []byte, validatorPubkey []byte, amount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.Unstake(&_IPTokenStaking.TransactOpts, delegatorUncmpPubkey, validatorPubkey, amount) +} + +// Unstake is a paid mutator transaction binding the contract method 0x5d5ab968. +// +// Solidity: function unstake(bytes delegatorUncmpPubkey, bytes validatorPubkey, uint256 amount) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) Unstake(delegatorUncmpPubkey []byte, validatorPubkey []byte, amount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.Unstake(&_IPTokenStaking.TransactOpts, delegatorUncmpPubkey, validatorPubkey, amount) +} + +// UnstakeOnBehalf is a paid mutator transaction binding the contract method 0xa1cb1846. +// +// Solidity: function unstakeOnBehalf(bytes delegatorCmpPubkey, bytes validatorPubkey, uint256 amount) returns() +func (_IPTokenStaking *IPTokenStakingTransactor) UnstakeOnBehalf(opts *bind.TransactOpts, delegatorCmpPubkey []byte, validatorPubkey []byte, amount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.contract.Transact(opts, "unstakeOnBehalf", delegatorCmpPubkey, validatorPubkey, amount) +} + +// UnstakeOnBehalf is a paid mutator transaction binding the contract method 0xa1cb1846. +// +// Solidity: function unstakeOnBehalf(bytes delegatorCmpPubkey, bytes validatorPubkey, uint256 amount) returns() +func (_IPTokenStaking *IPTokenStakingSession) UnstakeOnBehalf(delegatorCmpPubkey []byte, validatorPubkey []byte, amount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.UnstakeOnBehalf(&_IPTokenStaking.TransactOpts, delegatorCmpPubkey, validatorPubkey, amount) +} + +// UnstakeOnBehalf is a paid mutator transaction binding the contract method 0xa1cb1846. +// +// Solidity: function unstakeOnBehalf(bytes delegatorCmpPubkey, bytes validatorPubkey, uint256 amount) returns() +func (_IPTokenStaking *IPTokenStakingTransactorSession) UnstakeOnBehalf(delegatorCmpPubkey []byte, validatorPubkey []byte, amount *big.Int) (*types.Transaction, error) { + return _IPTokenStaking.Contract.UnstakeOnBehalf(&_IPTokenStaking.TransactOpts, delegatorCmpPubkey, validatorPubkey, amount) +} + +// IPTokenStakingCreateValidatorIterator is returned from FilterCreateValidator and is used to iterate over the raw logs and unpacked data for CreateValidator events raised by the IPTokenStaking contract. +type IPTokenStakingCreateValidatorIterator struct { + Event *IPTokenStakingCreateValidator // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingCreateValidatorIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingCreateValidator) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingCreateValidator) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingCreateValidatorIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingCreateValidatorIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingCreateValidator represents a CreateValidator event raised by the IPTokenStaking contract. +type IPTokenStakingCreateValidator struct { + ValidatorPubkey []byte + Moniker string + StakeAmount *big.Int + CommissionRate uint32 + MaxCommissionRate uint32 + MaxCommissionChangeRate uint32 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterCreateValidator is a free log retrieval operation binding the contract event 0x5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a18. +// +// Solidity: event CreateValidator(bytes validatorPubkey, string moniker, uint256 stakeAmount, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterCreateValidator(opts *bind.FilterOpts) (*IPTokenStakingCreateValidatorIterator, error) { + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "CreateValidator") + if err != nil { + return nil, err + } + return &IPTokenStakingCreateValidatorIterator{contract: _IPTokenStaking.contract, event: "CreateValidator", logs: logs, sub: sub}, nil +} + +// WatchCreateValidator is a free log subscription operation binding the contract event 0x5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a18. +// +// Solidity: event CreateValidator(bytes validatorPubkey, string moniker, uint256 stakeAmount, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchCreateValidator(opts *bind.WatchOpts, sink chan<- *IPTokenStakingCreateValidator) (event.Subscription, error) { + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "CreateValidator") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingCreateValidator) + if err := _IPTokenStaking.contract.UnpackLog(event, "CreateValidator", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseCreateValidator is a log parse operation binding the contract event 0x5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a18. +// +// Solidity: event CreateValidator(bytes validatorPubkey, string moniker, uint256 stakeAmount, uint32 commissionRate, uint32 maxCommissionRate, uint32 maxCommissionChangeRate) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseCreateValidator(log types.Log) (*IPTokenStakingCreateValidator, error) { + event := new(IPTokenStakingCreateValidator) + if err := _IPTokenStaking.contract.UnpackLog(event, "CreateValidator", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenStakingDepositIterator is returned from FilterDeposit and is used to iterate over the raw logs and unpacked data for Deposit events raised by the IPTokenStaking contract. +type IPTokenStakingDepositIterator struct { + Event *IPTokenStakingDeposit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingDepositIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingDepositIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingDepositIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingDeposit represents a Deposit event raised by the IPTokenStaking contract. +type IPTokenStakingDeposit struct { + DepositorPubkey []byte + ValidatorPubkey []byte + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDeposit is a free log retrieval operation binding the contract event 0xe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0. +// +// Solidity: event Deposit(bytes depositorPubkey, bytes validatorPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterDeposit(opts *bind.FilterOpts) (*IPTokenStakingDepositIterator, error) { + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "Deposit") + if err != nil { + return nil, err + } + return &IPTokenStakingDepositIterator{contract: _IPTokenStaking.contract, event: "Deposit", logs: logs, sub: sub}, nil +} + +// WatchDeposit is a free log subscription operation binding the contract event 0xe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0. +// +// Solidity: event Deposit(bytes depositorPubkey, bytes validatorPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IPTokenStakingDeposit) (event.Subscription, error) { + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "Deposit") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingDeposit) + if err := _IPTokenStaking.contract.UnpackLog(event, "Deposit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDeposit is a log parse operation binding the contract event 0xe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0. +// +// Solidity: event Deposit(bytes depositorPubkey, bytes validatorPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseDeposit(log types.Log) (*IPTokenStakingDeposit, error) { + event := new(IPTokenStakingDeposit) + if err := _IPTokenStaking.contract.UnpackLog(event, "Deposit", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenStakingOwnershipTransferStartedIterator is returned from FilterOwnershipTransferStarted and is used to iterate over the raw logs and unpacked data for OwnershipTransferStarted events raised by the IPTokenStaking contract. +type IPTokenStakingOwnershipTransferStartedIterator struct { + Event *IPTokenStakingOwnershipTransferStarted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingOwnershipTransferStartedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingOwnershipTransferStartedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingOwnershipTransferStartedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingOwnershipTransferStarted represents a OwnershipTransferStarted event raised by the IPTokenStaking contract. +type IPTokenStakingOwnershipTransferStarted struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferStarted is a free log retrieval operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterOwnershipTransferStarted(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*IPTokenStakingOwnershipTransferStartedIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &IPTokenStakingOwnershipTransferStartedIterator{contract: _IPTokenStaking.contract, event: "OwnershipTransferStarted", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferStarted is a free log subscription operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchOwnershipTransferStarted(opts *bind.WatchOpts, sink chan<- *IPTokenStakingOwnershipTransferStarted, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingOwnershipTransferStarted) + if err := _IPTokenStaking.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferStarted is a log parse operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseOwnershipTransferStarted(log types.Log) (*IPTokenStakingOwnershipTransferStarted, error) { + event := new(IPTokenStakingOwnershipTransferStarted) + if err := _IPTokenStaking.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenStakingOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the IPTokenStaking contract. +type IPTokenStakingOwnershipTransferredIterator struct { + Event *IPTokenStakingOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingOwnershipTransferred represents a OwnershipTransferred event raised by the IPTokenStaking contract. +type IPTokenStakingOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*IPTokenStakingOwnershipTransferredIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &IPTokenStakingOwnershipTransferredIterator{contract: _IPTokenStaking.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *IPTokenStakingOwnershipTransferred, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingOwnershipTransferred) + if err := _IPTokenStaking.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseOwnershipTransferred(log types.Log) (*IPTokenStakingOwnershipTransferred, error) { + event := new(IPTokenStakingOwnershipTransferred) + if err := _IPTokenStaking.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenStakingRedelegateIterator is returned from FilterRedelegate and is used to iterate over the raw logs and unpacked data for Redelegate events raised by the IPTokenStaking contract. +type IPTokenStakingRedelegateIterator struct { + Event *IPTokenStakingRedelegate // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingRedelegateIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingRedelegate) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingRedelegate) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingRedelegateIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingRedelegateIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingRedelegate represents a Redelegate event raised by the IPTokenStaking contract. +type IPTokenStakingRedelegate struct { + DepositorPubkey []byte + ValidatorSrcPubkey []byte + ValidatorDstPubkey []byte + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRedelegate is a free log retrieval operation binding the contract event 0xb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a. +// +// Solidity: event Redelegate(bytes depositorPubkey, bytes validatorSrcPubkey, bytes validatorDstPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterRedelegate(opts *bind.FilterOpts) (*IPTokenStakingRedelegateIterator, error) { + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "Redelegate") + if err != nil { + return nil, err + } + return &IPTokenStakingRedelegateIterator{contract: _IPTokenStaking.contract, event: "Redelegate", logs: logs, sub: sub}, nil +} + +// WatchRedelegate is a free log subscription operation binding the contract event 0xb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a. +// +// Solidity: event Redelegate(bytes depositorPubkey, bytes validatorSrcPubkey, bytes validatorDstPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchRedelegate(opts *bind.WatchOpts, sink chan<- *IPTokenStakingRedelegate) (event.Subscription, error) { + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "Redelegate") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingRedelegate) + if err := _IPTokenStaking.contract.UnpackLog(event, "Redelegate", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRedelegate is a log parse operation binding the contract event 0xb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a. +// +// Solidity: event Redelegate(bytes depositorPubkey, bytes validatorSrcPubkey, bytes validatorDstPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseRedelegate(log types.Log) (*IPTokenStakingRedelegate, error) { + event := new(IPTokenStakingRedelegate) + if err := _IPTokenStaking.contract.UnpackLog(event, "Redelegate", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenStakingSetWithdrawalAddressIterator is returned from FilterSetWithdrawalAddress and is used to iterate over the raw logs and unpacked data for SetWithdrawalAddress events raised by the IPTokenStaking contract. +type IPTokenStakingSetWithdrawalAddressIterator struct { + Event *IPTokenStakingSetWithdrawalAddress // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingSetWithdrawalAddressIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingSetWithdrawalAddress) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingSetWithdrawalAddress) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingSetWithdrawalAddressIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingSetWithdrawalAddressIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingSetWithdrawalAddress represents a SetWithdrawalAddress event raised by the IPTokenStaking contract. +type IPTokenStakingSetWithdrawalAddress struct { + DepositorPubkey []byte + ExecutionAddress [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSetWithdrawalAddress is a free log retrieval operation binding the contract event 0x9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca. +// +// Solidity: event SetWithdrawalAddress(bytes depositorPubkey, bytes32 executionAddress) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterSetWithdrawalAddress(opts *bind.FilterOpts) (*IPTokenStakingSetWithdrawalAddressIterator, error) { + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "SetWithdrawalAddress") + if err != nil { + return nil, err + } + return &IPTokenStakingSetWithdrawalAddressIterator{contract: _IPTokenStaking.contract, event: "SetWithdrawalAddress", logs: logs, sub: sub}, nil +} + +// WatchSetWithdrawalAddress is a free log subscription operation binding the contract event 0x9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca. +// +// Solidity: event SetWithdrawalAddress(bytes depositorPubkey, bytes32 executionAddress) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchSetWithdrawalAddress(opts *bind.WatchOpts, sink chan<- *IPTokenStakingSetWithdrawalAddress) (event.Subscription, error) { + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "SetWithdrawalAddress") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingSetWithdrawalAddress) + if err := _IPTokenStaking.contract.UnpackLog(event, "SetWithdrawalAddress", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSetWithdrawalAddress is a log parse operation binding the contract event 0x9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca. +// +// Solidity: event SetWithdrawalAddress(bytes depositorPubkey, bytes32 executionAddress) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseSetWithdrawalAddress(log types.Log) (*IPTokenStakingSetWithdrawalAddress, error) { + event := new(IPTokenStakingSetWithdrawalAddress) + if err := _IPTokenStaking.contract.UnpackLog(event, "SetWithdrawalAddress", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IPTokenStakingWithdrawIterator is returned from FilterWithdraw and is used to iterate over the raw logs and unpacked data for Withdraw events raised by the IPTokenStaking contract. +type IPTokenStakingWithdrawIterator struct { + Event *IPTokenStakingWithdraw // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IPTokenStakingWithdrawIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingWithdraw) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IPTokenStakingWithdraw) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IPTokenStakingWithdrawIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IPTokenStakingWithdrawIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IPTokenStakingWithdraw represents a Withdraw event raised by the IPTokenStaking contract. +type IPTokenStakingWithdraw struct { + DepositorPubkey []byte + ValidatorPubkey []byte + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdraw is a free log retrieval operation binding the contract event 0x0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b59. +// +// Solidity: event Withdraw(bytes depositorPubkey, bytes validatorPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) FilterWithdraw(opts *bind.FilterOpts) (*IPTokenStakingWithdrawIterator, error) { + + logs, sub, err := _IPTokenStaking.contract.FilterLogs(opts, "Withdraw") + if err != nil { + return nil, err + } + return &IPTokenStakingWithdrawIterator{contract: _IPTokenStaking.contract, event: "Withdraw", logs: logs, sub: sub}, nil +} + +// WatchWithdraw is a free log subscription operation binding the contract event 0x0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b59. +// +// Solidity: event Withdraw(bytes depositorPubkey, bytes validatorPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) WatchWithdraw(opts *bind.WatchOpts, sink chan<- *IPTokenStakingWithdraw) (event.Subscription, error) { + + logs, sub, err := _IPTokenStaking.contract.WatchLogs(opts, "Withdraw") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IPTokenStakingWithdraw) + if err := _IPTokenStaking.contract.UnpackLog(event, "Withdraw", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdraw is a log parse operation binding the contract event 0x0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b59. +// +// Solidity: event Withdraw(bytes depositorPubkey, bytes validatorPubkey, uint256 amount) +func (_IPTokenStaking *IPTokenStakingFilterer) ParseWithdraw(log types.Log) (*IPTokenStakingWithdraw, error) { + event := new(IPTokenStakingWithdraw) + if err := _IPTokenStaking.contract.UnpackLog(event, "Withdraw", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/contracts/bindings/iptokenstaking_more.go b/contracts/bindings/iptokenstaking_more.go new file mode 100644 index 00000000..42c1dc28 --- /dev/null +++ b/contracts/bindings/iptokenstaking_more.go @@ -0,0 +1,14 @@ +package bindings + +import ( + _ "embed" +) + +const ( + IPTokenStakingDeployedBytecode = "0x6080604052600436106101f95760003560e01c806386eec4a11161010d578063c24ae586116100a0578063eee5cead1161006f578063eee5cead14610652578063f188768414610672578063f2fde38b14610688578063fc2e5932146106a8578063fc56c2a2146106bb57600080fd5b8063c24ae586146105a7578063d2e1f5b8146105df578063e30c397814610614578063eb4af0451461063257600080fd5b80639855c8b5116100dc5780639855c8b5146104ff578063a1cb18461461051f578063b8db983e1461053f578063bda16b151461057357600080fd5b806386eec4a1146104755780638d3e1e41146104885780638da5cb5b146104ba5780638f37ec19146104ec57600080fd5b80635706750311610190578063715018a61161015f578063715018a6146103de578063787f82c8146103f357806379ba5097146104135780637b6e842c1461042857806383dffd6f1461044857600080fd5b806357067503146103505780635a69825d146103885780635d5ab9681461039e5780636ea3a228146103be57600080fd5b80632ebc6034116101cc5780632ebc6034146102be57806339ec4df91461030757806348903e381461031d57806353972c2a1461033057600080fd5b8063057b9296146101fe578063060ceab01461022057806317e42e12146102495780632d1e973e14610269575b600080fd5b34801561020a57600080fd5b5061021e610219366004612d1f565b6106ef565b005b34801561022c57600080fd5b5061023660055481565b6040519081526020015b60405180910390f35b34801561025557600080fd5b5061021e610264366004612d1f565b610854565b34801561027557600080fd5b50610236610284366004612e35565b8151602081840181018051600882529282019482019490942091909352815180830184018051928152908401929093019190912091525481565b3480156102ca57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b60405163ffffffff9091168152602001610240565b34801561031357600080fd5b5061023660035481565b61021e61032b366004612e98565b6109a6565b34801561033c57600080fd5b5061021e61034b366004612ed9565b610b0e565b34801561035c57600080fd5b5061023661036b366004612f13565b805160208183018101805160078252928201919093012091525481565b34801561039457600080fd5b5061023660045481565b3480156103aa57600080fd5b5061021e6103b9366004612f4f565b610e2c565b3480156103ca57600080fd5b5061021e6103d9366004612fc2565b61104e565b3480156103ea57600080fd5b5061021e6110f5565b3480156103ff57600080fd5b5061021e61040e366004612d1f565b611109565b34801561041f57600080fd5b5061021e6113bf565b34801561043457600080fd5b5061021e610443366004612ed9565b611403565b34801561045457600080fd5b50610468610463366004612e98565b611753565b6040516102409190612fdb565b61021e610483366004613028565b611786565b34801561049457600080fd5b506104a86104a3366004612f13565b61199a565b604051610240969594939291906130e3565b3480156104c657600080fd5b506000546001600160a01b03165b6040516001600160a01b039091168152602001610240565b61021e6104fa366004613028565b611a76565b34801561050b57600080fd5b5061021e61051a366004612fc2565b611c38565b34801561052b57600080fd5b5061021e61053a366004612f4f565b611ce2565b34801561054b57600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b34801561057f57600080fd5b506102367f000000000000000000000000000000000000000000000000000000000000000081565b3480156105b357600080fd5b506102366105c2366004612f13565b8051602081830181018051600a8252928201919093012091525481565b3480156105eb57600080fd5b506105ff6105fa366004612fc2565b611ee6565b60408051928352602083019190915201610240565b34801561062057600080fd5b506001546001600160a01b03166104d4565b34801561063e57600080fd5b5061021e61064d366004612fc2565b611f26565b34801561065e57600080fd5b5061021e61066d366004612fc2565b611fcb565b34801561067e57600080fd5b5061023660025481565b34801561069457600080fd5b5061021e6106a336600461312b565b61204e565b61021e6106b636600461315a565b6120bf565b3480156106c757600080fd5b506102f27f000000000000000000000000000000000000000000000000000000000000000081565b8282336041821461071b5760405162461bcd60e51b8152600401610712906131fb565b60405180910390fd5b8282600081811061072e5761072e613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146107645760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610778848461222d565b6001600160a01b03161461079e5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906107da908a908a90600401613312565b600060405180830381865af41580156107f7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261081f9190810190613326565b905061084a85600983604051610835919061339c565b9081526040519081900360200190209061225a565b5050505050505050565b828233604182146108775760405162461bcd60e51b8152600401610712906131fb565b8282600081811061088a5761088a613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146108c05760405162461bcd60e51b815260040161071290613256565b806001600160a01b03166108d4848461222d565b6001600160a01b0316146108fa5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90610936908a908a90600401613312565b600060405180830381865af4158015610953573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261097b9190810190613326565b905061084a85600983604051610991919061339c565b9081526040519081900360200190209061226f565b8181602181146109c85760405162461bcd60e51b8152600401610712906131fb565b818160008181106109db576109db613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610a29575081816000818110610a0e57610a0e613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610a455760405162461bcd60e51b815260040161071290613256565b610b0884848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250506040805180820190915260098152683b30b634b230ba37b960b91b602082015291507f000000000000000000000000000000000000000000000000000000000000000090507f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000612284565b50505050565b610b1881806133b8565b60418114610b385760405162461bcd60e51b8152600401610712906131fb565b81816000818110610b4b57610b4b613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610b815760405162461bcd60e51b815260040161071290613256565b610b8e60208401846133b8565b60218114610bae5760405162461bcd60e51b8152600401610712906131fb565b81816000818110610bc157610bc1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610c0f575081816000818110610bf457610bf4613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610c2b5760405162461bcd60e51b815260040161071290613256565b60068282604051610c3d9291906133fe565b9081526040519081900360200190205460ff16610c6c5760405162461bcd60e51b81526004016107129061340e565b610c7960408601866133b8565b60218114610c995760405162461bcd60e51b8152600401610712906131fb565b81816000818110610cac57610cac613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610cfa575081816000818110610cdf57610cdf613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610d165760405162461bcd60e51b815260040161071290613256565b60068282604051610d289291906133fe565b9081526040519081900360200190205460ff16610d575760405162461bcd60e51b81526004016107129061340e565b60007300000000000000000000000000000000000256f1636889b16e610d7d8a806133b8565b6040518363ffffffff1660e01b8152600401610d9a929190613312565b600060405180830381865af4158015610db7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610ddf9190810190613326565b90506000610df08960600135611ee6565b509050610dfd82336124e6565b610e2182610e0e60208c018c6133b8565b610e1b60408e018e6133b8565b86612571565b505050505050505050565b84843360418214610e4f5760405162461bcd60e51b8152600401610712906131fb565b82826000818110610e6257610e62613240565b9050013560f81c60f81b6001600160f81b031916600460f81b14610e985760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316610eac848461222d565b6001600160a01b031614610ed25760405162461bcd60e51b81526004016107129061329b565b858560218114610ef45760405162461bcd60e51b8152600401610712906131fb565b81816000818110610f0757610f07613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480610f55575081816000818110610f3a57610f3a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b610f715760405162461bcd60e51b815260040161071290613256565b60068282604051610f839291906133fe565b9081526040519081900360200190205460ff16610fb25760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90610fee908e908e90600401613312565b600060405180830381865af415801561100b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110339190810190613326565b9050611041818a8a8a61272f565b5050505050505050505050565b61105661288f565b600081116110bb5760405162461bcd60e51b815260206004820152602c60248201527f4950546f6b656e5374616b696e673a206d696e556e7374616b65416d6f756e7460448201526b02063616e6e6f7420626520360a41b6064820152608401610712565b6110e57f000000000000000000000000000000000000000000000000000000000000000082613456565b6110ef908261348e565b60035550565b6110fd61288f565b61110760006128bc565b565b8282336041821461112c5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061113f5761113f613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146111755760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611189848461222d565b6001600160a01b0316146111af5760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906111eb908a908a90600401613312565b600060405180830381865af4158015611208573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526112309190810190613326565b90506000600782604051611244919061339c565b908152602001604051809103902054116112b25760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2044656c656761746f72206d7573742068604482015268617665207374616b6560b81b6064820152608401610712565b42600554600a836040516112c6919061339c565b9081526020016040518091039020546112df91906134a1565b106113485760405162461bcd60e51b815260206004820152603360248201527f4950546f6b656e5374616b696e673a205769746864726177616c20616464726560448201527239b99031b430b733b29031b7b7b616b237bbb760691b6064820152608401610712565b42600a82604051611359919061339c565b9081526020016040518091039020819055507f9f7f04f688298f474ed4c786abb29e0ca0173d70516d55d9eac515609b45fbca818660601b6bffffffffffffffffffffffff19166040516113ae9291906134b4565b60405180910390a150505050505050565b60015433906001600160a01b031681146113f75760405163118cdaa760e01b81526001600160a01b0382166004820152602401610712565b611400816128bc565b50565b61140d81806133b8565b336041821461142e5760405162461bcd60e51b8152600401610712906131fb565b8282600081811061144157611441613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146114775760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661148b848461222d565b6001600160a01b0316146114b15760405162461bcd60e51b81526004016107129061329b565b6114be60208501856133b8565b602181146114de5760405162461bcd60e51b8152600401610712906131fb565b818160008181106114f1576114f1613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061153f57508181600081811061152457611524613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b61155b5760405162461bcd60e51b815260040161071290613256565b6006828260405161156d9291906133fe565b9081526040519081900360200190205460ff1661159c5760405162461bcd60e51b81526004016107129061340e565b6115a960408701876133b8565b602181146115c95760405162461bcd60e51b8152600401610712906131fb565b818160008181106115dc576115dc613240565b9050013560f81c60f81b6001600160f81b031916600260f81b148061162a57508181600081811061160f5761160f613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6116465760405162461bcd60e51b815260040161071290613256565b600682826040516116589291906133fe565b9081526040519081900360200190205460ff166116875760405162461bcd60e51b81526004016107129061340e565b60006116968960600135611ee6565b50905060007300000000000000000000000000000000000256f1636889b16e6116bf8c806133b8565b6040518363ffffffff1660e01b81526004016116dc929190613312565b600060405180830381865af41580156116f9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526117219190810190613326565b90506117478161173460208d018d6133b8565b61174160408f018f6133b8565b87612571565b50505050505050505050565b606061177d6009848460405161176a9291906133fe565b90815260200160405180910390206128d5565b90505b92915050565b838333604182146117a95760405162461bcd60e51b8152600401610712906131fb565b828260008181106117bc576117bc613240565b9050013560f81c60f81b6001600160f81b031916600460f81b146117f25760405162461bcd60e51b815260040161071290613256565b806001600160a01b0316611806848461222d565b6001600160a01b03161461182c5760405162461bcd60e51b81526004016107129061329b565b84846021811461184e5760405162461bcd60e51b8152600401610712906131fb565b8181600081811061186157611861613240565b9050013560f81c60f81b6001600160f81b031916600260f81b14806118af57508181600081811061189457611894613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b6118cb5760405162461bcd60e51b815260040161071290613256565b600682826040516118dd9291906133fe565b9081526040519081900360200190205460ff1661190c5760405162461bcd60e51b81526004016107129061340e565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e90611948908d908d90600401613312565b600060405180830381865af4158015611965573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261198d9190810190613326565b90506117478189896128e9565b80516020818301810180516006825292820191909301209152805460018201805460ff90921692916119cb906134d6565b80601f01602080910402602001604051908101604052809291908181526020018280546119f7906134d6565b8015611a445780601f10611a1957610100808354040283529160200191611a44565b820191906000526020600020905b815481529060010190602001808311611a2757829003601f168201915b50505050600283015460039093015491929163ffffffff80821692506401000000008204811691600160401b90041686565b838360218114611a985760405162461bcd60e51b8152600401610712906131fb565b81816000818110611aab57611aab613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611af9575081816000818110611ade57611ade613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611b155760405162461bcd60e51b815260040161071290613256565b838360218114611b375760405162461bcd60e51b8152600401610712906131fb565b81816000818110611b4a57611b4a613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611b98575081816000818110611b7d57611b7d613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611bb45760405162461bcd60e51b815260040161071290613256565b60068282604051611bc69291906133fe565b9081526040519081900360200190205460ff16611bf55760405162461bcd60e51b81526004016107129061340e565b61084a88888080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508a92508991506128e99050565b611c4061288f565b60008111611ca85760405162461bcd60e51b815260206004820152602f60248201527f4950546f6b656e5374616b696e673a206d696e526564656c6567617465416d6f60448201526e0756e742063616e6e6f74206265203608c1b6064820152608401610712565b611cd27f000000000000000000000000000000000000000000000000000000000000000082613456565b611cdc908261348e565b60045550565b848460218114611d045760405162461bcd60e51b8152600401610712906131fb565b81816000818110611d1757611d17613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611d65575081816000818110611d4a57611d4a613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611d815760405162461bcd60e51b815260040161071290613256565b848460218114611da35760405162461bcd60e51b8152600401610712906131fb565b81816000818110611db657611db6613240565b9050013560f81c60f81b6001600160f81b031916600260f81b1480611e04575081816000818110611de957611de9613240565b9050013560f81c60f81b6001600160f81b031916600360f81b145b611e205760405162461bcd60e51b815260040161071290613256565b60068282604051611e329291906133fe565b9081526040519081900360200190205460ff16611e615760405162461bcd60e51b81526004016107129061340e565b611ea289898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152503392506124e6915050565b610e2189898080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a915089905061272f565b600080611f137f000000000000000000000000000000000000000000000000000000000000000084613456565b9050611f1f818461348e565b9150915091565b611f2e61288f565b60008111611f915760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a206d696e5374616b65416d6f756e7420636044820152690616e6e6f7420626520360b41b6064820152608401610712565b611fbb7f000000000000000000000000000000000000000000000000000000000000000082613456565b611fc5908261348e565b60025550565b611fd361288f565b600081116120495760405162461bcd60e51b815260206004820152603e60248201527f4950546f6b656e5374616b696e673a206e65775769746864726177616c41646460448201527f726573734368616e6765496e74657276616c2063616e6e6f74206265203000006064820152608401610712565b600555565b61205661288f565b600180546001600160a01b0383166001600160a01b031990911681179091556120876000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b868633604182146120e25760405162461bcd60e51b8152600401610712906131fb565b828260008181106120f5576120f5613240565b9050013560f81c60f81b6001600160f81b031916600460f81b1461212b5760405162461bcd60e51b815260040161071290613256565b806001600160a01b031661213f848461222d565b6001600160a01b0316146121655760405162461bcd60e51b81526004016107129061329b565b604051633444d8b760e11b81526000907300000000000000000000000000000000000256f190636889b16e906121a1908e908e90600401613312565b600060405180830381865af41580156121be573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526121e69190810190613326565b9050611041818a8a8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b91508a9050612284565b600061223c8260018186613510565b60405161224a9291906133fe565b6040519081900390209392505050565b600061177d836001600160a01b038416612a00565b600061177d836001600160a01b038416612a4f565b600685604051612294919061339c565b9081526040519081900360200190205460ff16156123055760405162461bcd60e51b815260206004820152602860248201527f4950546f6b656e5374616b696e673a2056616c696461746f7220616c72656164604482015267792065786973747360c01b6064820152608401610712565b60008061231134611ee6565b91509150600082116123355760405162461bcd60e51b81526004016107129061353a565b6040518060c001604052806001151581526020018781526020018381526020018663ffffffff1681526020018563ffffffff1681526020018463ffffffff16815250600688604051612387919061339c565b908152604051602091819003820190208251815460ff19169015151781559082015160018201906123b890826135cf565b506040828101516002830155606083015160039092018054608085015160a09095015163ffffffff908116600160401b026bffffffff0000000000000000199682166401000000000267ffffffffffffffff199093169190951617179390931691909117909155518290600790612430908a9061339c565b9081526020016040518091039020600082825461244d91906134a1565b9250508190555081600888604051612465919061339c565b908152602001604051809103902088604051612481919061339c565b9081526020016040518091039020600082825461249e91906134a1565b909155506124ad905081612b42565b7f5cecf4ee8b0c1d212b07dbc464fc303e4ffc458fd0f61135d4b9bf7f60197a188787848888886040516113ae9695949392919061368e565b61250f816009846040516124fa919061339c565b90815260405190819003602001902090612bed565b61256d5760405162461bcd60e51b815260206004820152602960248201527f4950546f6b656e5374616b696e673a2043616c6c6572206973206e6f7420616e6044820152681037b832b930ba37b960b91b6064820152608401610712565b5050565b80600887604051612582919061339c565b908152602001604051809103902086866040516125a09291906133fe565b90815260200160405180910390205410156125cd5760405162461bcd60e51b8152600401610712906136e1565b80600686866040516125e09291906133fe565b90815260200160405180910390206002016000828254612600919061348e565b92505081905550806006848460405161261a9291906133fe565b9081526020016040518091039020600201600082825461263a91906134a1565b9250508190555080600887604051612652919061339c565b908152602001604051809103902086866040516126709291906133fe565b9081526020016040518091039020600082825461268d919061348e565b92505081905550806008876040516126a5919061339c565b908152602001604051809103902084846040516126c39291906133fe565b908152602001604051809103902060008282546126e091906134a1565b90915550506040517fb025fa2a574dd306182c6ac63bf7b05482b99680c1b38a42d8401a0adfd3775a9061271f9088908890889088908890889061372b565b60405180910390a1505050505050565b80600885604051612740919061339c565b9081526020016040518091039020848460405161275e9291906133fe565b908152602001604051809103902054101561278b5760405162461bcd60e51b8152600401610712906136e1565b806006848460405161279e9291906133fe565b908152602001604051809103902060020160008282546127be919061348e565b92505081905550806007856040516127d6919061339c565b908152602001604051809103902060008282546127f3919061348e565b925050819055508060088560405161280b919061339c565b908152602001604051809103902084846040516128299291906133fe565b90815260200160405180910390206000828254612846919061348e565b90915550506040517f0526a04a9b113a046b17e2350e42123a2515b5558b3aea91576ccdb1270c1b599061288190869086908690869061377a565b60405180910390a150505050565b6000546001600160a01b031633146111075760405163118cdaa760e01b8152336004820152602401610712565b600180546001600160a01b031916905561140081612c0f565b606060006128e283612c5f565b9392505050565b6000806128f534611ee6565b9150915060025482101561291b5760405162461bcd60e51b81526004016107129061353a565b816006858560405161292e9291906133fe565b9081526040519081900360200181206002018054909201909155829060079061295890889061339c565b9081526040519081900360200181208054909201909155829060089061297f90889061339c565b9081526020016040518091039020858560405161299d9291906133fe565b908152604051908190036020019020805490910190556129bc81612b42565b7fe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0858585856040516129f1949392919061377a565b60405180910390a15050505050565b6000818152600183016020526040812054612a4757508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611780565b506000611780565b60008181526001830160205260408120548015612b38576000612a7360018361348e565b8554909150600090612a879060019061348e565b9050808214612aec576000866000018281548110612aa757612aa7613240565b9060005260206000200154905080876000018481548110612aca57612aca613240565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080612afd57612afd6137b2565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611780565b6000915050611780565b604051600090339083908381818185875af1925050503d8060008114612b84576040519150601f19603f3d011682016040523d82523d6000602084013e612b89565b606091505b505090508061256d5760405162461bcd60e51b815260206004820152602a60248201527f4950546f6b656e5374616b696e673a204661696c656420746f20726566756e64604482015269103932b6b0b4b73232b960b11b6064820152608401610712565b6001600160a01b0381166000908152600183016020526040812054151561177d565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b606081600001805480602002602001604051908101604052809291908181526020018280548015612caf57602002820191906000526020600020905b815481526020019060010190808311612c9b575b50505050509050919050565b60008083601f840112612ccd57600080fd5b5081356001600160401b03811115612ce457600080fd5b602083019150836020828501011115612cfc57600080fd5b9250929050565b80356001600160a01b0381168114612d1a57600080fd5b919050565b600080600060408486031215612d3457600080fd5b83356001600160401b03811115612d4a57600080fd5b612d5686828701612cbb565b9094509250612d69905060208501612d03565b90509250925092565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715612db057612db0612d72565b604052919050565b60006001600160401b03821115612dd157612dd1612d72565b50601f01601f191660200190565b600082601f830112612df057600080fd5b8135612e03612dfe82612db8565b612d88565b818152846020838601011115612e1857600080fd5b816020850160208301376000918101602001919091529392505050565b60008060408385031215612e4857600080fd5b82356001600160401b0380821115612e5f57600080fd5b612e6b86838701612ddf565b93506020850135915080821115612e8157600080fd5b50612e8e85828601612ddf565b9150509250929050565b60008060208385031215612eab57600080fd5b82356001600160401b03811115612ec157600080fd5b612ecd85828601612cbb565b90969095509350505050565b600060208284031215612eeb57600080fd5b81356001600160401b03811115612f0157600080fd5b8201608081850312156128e257600080fd5b600060208284031215612f2557600080fd5b81356001600160401b03811115612f3b57600080fd5b612f4784828501612ddf565b949350505050565b600080600080600060608688031215612f6757600080fd5b85356001600160401b0380821115612f7e57600080fd5b612f8a89838a01612cbb565b90975095506020880135915080821115612fa357600080fd5b50612fb088828901612cbb565b96999598509660400135949350505050565b600060208284031215612fd457600080fd5b5035919050565b6020808252825182820181905260009190848201906040850190845b8181101561301c5783516001600160a01b031683529284019291840191600101612ff7565b50909695505050505050565b6000806000806040858703121561303e57600080fd5b84356001600160401b038082111561305557600080fd5b61306188838901612cbb565b9096509450602087013591508082111561307a57600080fd5b5061308787828801612cbb565b95989497509550505050565b60005b838110156130ae578181015183820152602001613096565b50506000910152565b600081518084526130cf816020860160208601613093565b601f01601f19169290920160200192915050565b861515815260c0602082015260006130fe60c08301886130b7565b60408301969096525063ffffffff9384166060820152918316608083015290911660a09091015292915050565b60006020828403121561313d57600080fd5b61177d82612d03565b803563ffffffff81168114612d1a57600080fd5b600080600080600080600060a0888a03121561317557600080fd5b87356001600160401b038082111561318c57600080fd5b6131988b838c01612cbb565b909950975060208a01359150808211156131b157600080fd5b506131be8a828b01612cbb565b90965094506131d1905060408901613146565b92506131df60608901613146565b91506131ed60808901613146565b905092959891949750929550565b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206c6040820152640cadccee8d60db1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60208082526025908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b657920706040820152640e4caccd2f60db1b606082015260800190565b6020808252602e908201527f4950546f6b656e5374616b696e673a20496e76616c6964207075626b6579206460408201526d657269766564206164647265737360901b606082015260800190565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b602081526000612f476020830184866132e9565b60006020828403121561333857600080fd5b81516001600160401b0381111561334e57600080fd5b8201601f8101841361335f57600080fd5b805161336d612dfe82612db8565b81815285602083850101111561338257600080fd5b613393826020830160208601613093565b95945050505050565b600082516133ae818460208701613093565b9190910192915050565b6000808335601e198436030181126133cf57600080fd5b8301803591506001600160401b038211156133e957600080fd5b602001915036819003821315612cfc57600080fd5b8183823760009101908152919050565b60208082526028908201527f4950546f6b656e5374616b696e673a2056616c696461746f7220646f6573206e6040820152671bdd08195e1a5cdd60c21b606082015260800190565b60008261347357634e487b7160e01b600052601260045260246000fd5b500690565b634e487b7160e01b600052601160045260246000fd5b8181038181111561178057611780613478565b8082018082111561178057611780613478565b6040815260006134c760408301856130b7565b90508260208301529392505050565b600181811c908216806134ea57607f821691505b60208210810361350a57634e487b7160e01b600052602260045260246000fd5b50919050565b6000808585111561352057600080fd5b8386111561352d57600080fd5b5050820193919092039150565b60208082526024908201527f4950546f6b656e5374616b696e673a205374616b6520616d6f756e7420746f6f604082015263206c6f7760e01b606082015260800190565b601f8211156135ca576000816000526020600020601f850160051c810160208610156135a75750805b601f850160051c820191505b818110156135c6578281556001016135b3565b5050505b505050565b81516001600160401b038111156135e8576135e8612d72565b6135fc816135f684546134d6565b8461357e565b602080601f83116001811461363157600084156136195750858301515b600019600386901b1c1916600185901b1785556135c6565b600085815260208120601f198616915b8281101561366057888601518255948401946001909101908401613641565b508582101561367e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60c0815260006136a160c08301896130b7565b82810360208401526136b381896130b7565b6040840197909752505063ffffffff9384166060820152918316608083015290911660a09091015292915050565b6020808252602a908201527f4950546f6b656e5374616b696e673a20496e73756666696369656e74207374616040820152691ad95908185b5bdd5b9d60b21b606082015260800190565b60808152600061373e60808301896130b7565b828103602084015261375181888a6132e9565b905082810360408401526137668186886132e9565b915050826060830152979650505050505050565b60608152600061378d60608301876130b7565b82810360208401526137a08186886132e9565b91505082604083015295945050505050565b634e487b7160e01b600052603160045260246000fdfea2646970667358221220cbf37ead0066eb6657879ff233410633a0ad7abf7542d7cf2a2155f0dd30195b64736f6c63430008180033" +) + +//go:embed iptokenstaking_storage_layout.json +var iptokenstakingStorageLayoutJSON []byte + +var IPTokenStakingStorageLayout = mustGetStorageLayout(iptokenstakingStorageLayoutJSON) diff --git a/contracts/bindings/iptokenstaking_storage_layout.json b/contracts/bindings/iptokenstaking_storage_layout.json new file mode 100644 index 00000000..4ccd4d8b --- /dev/null +++ b/contracts/bindings/iptokenstaking_storage_layout.json @@ -0,0 +1,263 @@ +{ + "storage": [ + { + "astId": 8, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 156, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "_pendingOwner", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 40493, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "minStakeAmount", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 40496, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "minUnstakeAmount", + "offset": 0, + "slot": "3", + "type": "t_uint256" + }, + { + "astId": 40499, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "minRedelegateAmount", + "offset": 0, + "slot": "4", + "type": "t_uint256" + }, + { + "astId": 40502, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "withdrawalAddressChangeInterval", + "offset": 0, + "slot": "5", + "type": "t_uint256" + }, + { + "astId": 40508, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "validatorMetadata", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_bytes_memory_ptr,t_struct(ValidatorMetadata)40039_storage)" + }, + { + "astId": 40513, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "delegatorTotalStakes", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_bytes_memory_ptr,t_uint256)" + }, + { + "astId": 40520, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "delegatorValidatorStakes", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_bytes_memory_ptr,t_mapping(t_bytes_memory_ptr,t_uint256))" + }, + { + "astId": 40526, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "delegatorOperators", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_bytes_memory_ptr,t_struct(AddressSet)589_storage)" + }, + { + "astId": 40531, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "withdrawalAddressChange", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_bytes_memory_ptr,t_uint256)" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "encoding": "dynamic_array", + "label": "bytes32[]", + "numberOfBytes": "32", + "base": "t_bytes32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_memory_ptr": { + "encoding": "bytes", + "label": "bytes", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_bytes_memory_ptr,t_mapping(t_bytes_memory_ptr,t_uint256))": { + "encoding": "mapping", + "key": "t_bytes_memory_ptr", + "label": "mapping(bytes => mapping(bytes => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_bytes_memory_ptr,t_uint256)" + }, + "t_mapping(t_bytes_memory_ptr,t_struct(AddressSet)589_storage)": { + "encoding": "mapping", + "key": "t_bytes_memory_ptr", + "label": "mapping(bytes => struct EnumerableSet.AddressSet)", + "numberOfBytes": "32", + "value": "t_struct(AddressSet)589_storage" + }, + "t_mapping(t_bytes_memory_ptr,t_struct(ValidatorMetadata)40039_storage)": { + "encoding": "mapping", + "key": "t_bytes_memory_ptr", + "label": "mapping(bytes => struct IIPTokenStaking.ValidatorMetadata)", + "numberOfBytes": "32", + "value": "t_struct(ValidatorMetadata)40039_storage" + }, + "t_mapping(t_bytes_memory_ptr,t_uint256)": { + "encoding": "mapping", + "key": "t_bytes_memory_ptr", + "label": "mapping(bytes => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)589_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.AddressSet", + "numberOfBytes": "64", + "members": [ + { + "astId": 588, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "_inner", + "offset": 0, + "slot": "0", + "type": "t_struct(Set)274_storage" + } + ] + }, + "t_struct(Set)274_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.Set", + "numberOfBytes": "64", + "members": [ + { + "astId": 269, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "_values", + "offset": 0, + "slot": "0", + "type": "t_array(t_bytes32)dyn_storage" + }, + { + "astId": 273, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "_positions", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_uint256)" + } + ] + }, + "t_struct(ValidatorMetadata)40039_storage": { + "encoding": "inplace", + "label": "struct IIPTokenStaking.ValidatorMetadata", + "numberOfBytes": "128", + "members": [ + { + "astId": 40028, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "exists", + "offset": 0, + "slot": "0", + "type": "t_bool" + }, + { + "astId": 40030, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "moniker", + "offset": 0, + "slot": "1", + "type": "t_string_storage" + }, + { + "astId": 40032, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "totalStake", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 40034, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "commissionRate", + "offset": 0, + "slot": "3", + "type": "t_uint32" + }, + { + "astId": 40036, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "maxCommissionRate", + "offset": 4, + "slot": "3", + "type": "t_uint32" + }, + { + "astId": 40038, + "contract": "src/protocol/IPTokenStaking.sol:IPTokenStaking", + "label": "maxCommissionChangeRate", + "offset": 8, + "slot": "3", + "type": "t_uint32" + } + ] + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "encoding": "inplace", + "label": "uint32", + "numberOfBytes": "4" + } + } +} diff --git a/contracts/bindings/scripts/commenttypes.go b/contracts/bindings/scripts/commenttypes.go new file mode 100644 index 00000000..24fb3a27 --- /dev/null +++ b/contracts/bindings/scripts/commenttypes.go @@ -0,0 +1,130 @@ +// commenttypes comments out type definitions in a Go file +// Usage: go run commenttypes.go -- ... + +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/piplabs/story/lib/errors" +) + +func main() { + err := run() + + if err != nil { + fmt.Printf("commenttypes failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("commenttypes succeeded") +} + +func run() error { + if len(os.Args) < 3 { + return errors.New("usage: go run commenttypes.go -- ...") + } + + // os.Args[0] is the name of the command + // os.Args[1] is either or "--" separator + + firstArgIdx := 1 + if os.Args[1] == "--" { + firstArgIdx = 2 + } + + filepath := os.Args[firstArgIdx] + types := os.Args[firstArgIdx+1:] + + if len(types) == 0 { + return errors.New("no types") + } + + inputF, err := os.Open(filepath) + if err != nil { + return errors.Wrap(err, "open file") + } + defer inputF.Close() + + scanner := bufio.NewScanner(inputF) + var content string + for scanner.Scan() { + content += scanner.Text() + "\n" + } + + modified := commentTypes(content, types) + + err = os.WriteFile(filepath, []byte(modified), 0600) + if err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + +// commentTypes comments out type definitions in a Go file. +func commentTypes(content string, types []string) string { + var modifiedLines []string + lines := strings.Split(content, "\n") + + // tracks whether we're in a type definition + // + // type Name { + // ... + // } + inTypeDef := false + + for _, line := range lines { + commentline := func() { + modifiedLines = append(modifiedLines, "// "+line) + } + + if inTypeDef && isTypeDefEnd(line) { + inTypeDef = false + commentline() + + continue + } + + if inTypeDef { + commentline() + + continue + } + + if isTypeDefStart(line, types) { + inTypeDef = true + modifiedLines = append(modifiedLines, "// autocommented by commenttypes.go") + commentline() + + // single line type definition + if isTypeDefEnd(line) { + inTypeDef = false + } + + continue + } + + // no type definition, just copy the line + modifiedLines = append(modifiedLines, line) + } + + return strings.Join(modifiedLines, "\n") +} + +func isTypeDefStart(line string, types []string) bool { + for _, t := range types { + if strings.HasPrefix(strings.TrimSpace(line), "type "+t) { + return true + } + } + + return false +} + +func isTypeDefEnd(line string) bool { + return strings.HasPrefix(strings.TrimSpace(line), "}") +} diff --git a/contracts/bindings/scripts/gen.sh b/contracts/bindings/scripts/gen.sh new file mode 100755 index 00000000..d5ecc2f8 --- /dev/null +++ b/contracts/bindings/scripts/gen.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Generate bindings for solidity contracts + +DIR=${DIR:-./bindings} +PKG=${PKG:-bindings} + +# generate bindings for the given contract +# works on contract name of fully qualified path to the contract +# params: +# $1: contract name (ex. 'IPTokenStaking', 'src/protocol/IPTokenStaking.sol:IPTokenStaking') +gen_binding() { + contract=$1 + + # strip path prefix, if used + # ex src/protocol/IPTokenStaking.sol:IPTokenStaking => IPTokenStaking + name=$(echo ${contract} | cut -d ":" -f 2) + + # convert to lower case to respect golang package naming conventions + name_lower=$(echo ${name} | tr '[:upper:]' '[:lower:]') + + temp=$(mktemp -d) + forge inspect ${contract} abi > ${temp}/${name}.abi + forge inspect ${contract} bytecode > ${temp}/${name}.bin + + abigen \ + --abi ${temp}/${name}.abi \ + --bin ${temp}/${name}.bin \ + --pkg ${PKG} \ + --type ${name} \ + --out ${DIR}/${name_lower}.go +} + + +for contract in $@; do + gen_binding ${contract} +done diff --git a/contracts/bindings/scripts/genmore.sh b/contracts/bindings/scripts/genmore.sh new file mode 100755 index 00000000..46f3119c --- /dev/null +++ b/contracts/bindings/scripts/genmore.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Generate additional bindings for solidity contracts, not generated by abigen + +DIR=${DIR:-./bindings} +PKG=${PKG:-bindings} + +# generate bindings for the given contract +# works on contract name of fully qualified path to the contract +# params: +# $1: contract name (ex. 'IPTokenStaking', 'src/protocol/IPTokenStaking.sol:IPTokenStaking') +gen_binding() { + contract=$1 + + # strip path prefix, if used + # ex src/protocol/IPTokenStaking.sol:IPTokenStaking => IPTokenStaking + name=$(echo "${contract}" | cut -d ":" -f 2) + + # convert to lower case to respect golang package naming conventions + name_lower=$(echo "${name}" | tr '[:upper:]' '[:lower:]') + + + # gen storage layout json + storage_layout_file=${DIR}/${name_lower}_storage_layout.json + forge inspect "${contract}" storageLayout > "${storage_layout_file}" + + # abigen does not generate the deployedBytecode, so we add it manually to {name_lower}_more.go + + deployed_bytecode=$(forge inspect ${contract} deployedBytecode --libraries "src/libraries/Secp256k1.sol:Secp256k1:0x00000000000000000000000000000000000256f1") + file=${DIR}/${name_lower}_more.go + + cat < "${file}" +package ${PKG} + +import ( + _ "embed" +) + +const ( + ${name}DeployedBytecode = "${deployed_bytecode}" +) + +//go:embed ${name_lower}_storage_layout.json +var ${name_lower}StorageLayoutJSON []byte + +var ${name}StorageLayout = mustGetStorageLayout(${name_lower}StorageLayoutJSON) +EOF + + # gofmt expects 600 permissions + chmod 600 "${file}" +} + + +for contract in "$@"; do + gen_binding "${contract}" +done diff --git a/contracts/bindings/upgradeentrypoint.go b/contracts/bindings/upgradeentrypoint.go new file mode 100644 index 00000000..74132a8b --- /dev/null +++ b/contracts/bindings/upgradeentrypoint.go @@ -0,0 +1,791 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// UpgradeEntrypointMetaData contains all meta data concerning the UpgradeEntrypoint contract. +var UpgradeEntrypointMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"acceptOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"pendingOwner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"planUpgrade\",\"inputs\":[{\"name\":\"name\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"height\",\"type\":\"int64\",\"internalType\":\"int64\"},{\"name\":\"info\",\"type\":\"string\",\"internalType\":\"string\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"OwnershipTransferStarted\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SoftwareUpgrade\",\"inputs\":[{\"name\":\"name\",\"type\":\"string\",\"indexed\":false,\"internalType\":\"string\"},{\"name\":\"height\",\"type\":\"int64\",\"indexed\":false,\"internalType\":\"int64\"},{\"name\":\"info\",\"type\":\"string\",\"indexed\":false,\"internalType\":\"string\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + Bin: "0x608060405234801561001057600080fd5b5060405161053f38038061053f83398101604081905261002f916100da565b806001600160a01b03811661005e57604051631e4fbdf760e01b81526000600482015260240160405180910390fd5b6100678161006e565b505061010a565b600180546001600160a01b03191690556100878161008a565b50565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000602082840312156100ec57600080fd5b81516001600160a01b038116811461010357600080fd5b9392505050565b610426806101196000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063715018a61461006757806379ba5097146100715780638da5cb5b14610079578063e30c3978146100a2578063ef176e0e146100b3578063f2fde38b146100c6575b600080fd5b61006f6100d9565b005b61006f6100ed565b6000546001600160a01b03165b6040516001600160a01b03909116815260200160405180910390f35b6001546001600160a01b0316610086565b61006f6100c13660046102cf565b610136565b61006f6100d436600461035b565b610184565b6100e16101f5565b6100eb6000610222565b565b60015433906001600160a01b0316811461012a5760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61013381610222565b50565b61013e6101f5565b7f112749e79b2098b58eab36c21f123b2883c3ecbbb4f41623a744fa6d9b3e37c685858585856040516101759594939291906103b4565b60405180910390a15050505050565b61018c6101f5565b600180546001600160a01b0383166001600160a01b031990911681179091556101bd6000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b6000546001600160a01b031633146100eb5760405163118cdaa760e01b8152336004820152602401610121565b600180546001600160a01b031916905561013381600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b60008083601f84011261029857600080fd5b50813567ffffffffffffffff8111156102b057600080fd5b6020830191508360208285010111156102c857600080fd5b9250929050565b6000806000806000606086880312156102e757600080fd5b853567ffffffffffffffff808211156102ff57600080fd5b61030b89838a01610286565b909750955060208801359150600782900b821461032757600080fd5b9093506040870135908082111561033d57600080fd5b5061034a88828901610286565b969995985093965092949392505050565b60006020828403121561036d57600080fd5b81356001600160a01b038116811461038457600080fd5b9392505050565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b6060815260006103c860608301878961038b565b8560070b602084015282810360408401526103e481858761038b565b9897505050505050505056fea2646970667358221220f12e8408869738cf77e756c57f3dbd8115f9eea757a287364347b7f1ff36591964736f6c63430008180033", +} + +// UpgradeEntrypointABI is the input ABI used to generate the binding from. +// Deprecated: Use UpgradeEntrypointMetaData.ABI instead. +var UpgradeEntrypointABI = UpgradeEntrypointMetaData.ABI + +// UpgradeEntrypointBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use UpgradeEntrypointMetaData.Bin instead. +var UpgradeEntrypointBin = UpgradeEntrypointMetaData.Bin + +// DeployUpgradeEntrypoint deploys a new Ethereum contract, binding an instance of UpgradeEntrypoint to it. +func DeployUpgradeEntrypoint(auth *bind.TransactOpts, backend bind.ContractBackend, newOwner common.Address) (common.Address, *types.Transaction, *UpgradeEntrypoint, error) { + parsed, err := UpgradeEntrypointMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(UpgradeEntrypointBin), backend, newOwner) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &UpgradeEntrypoint{UpgradeEntrypointCaller: UpgradeEntrypointCaller{contract: contract}, UpgradeEntrypointTransactor: UpgradeEntrypointTransactor{contract: contract}, UpgradeEntrypointFilterer: UpgradeEntrypointFilterer{contract: contract}}, nil +} + +// UpgradeEntrypoint is an auto generated Go binding around an Ethereum contract. +type UpgradeEntrypoint struct { + UpgradeEntrypointCaller // Read-only binding to the contract + UpgradeEntrypointTransactor // Write-only binding to the contract + UpgradeEntrypointFilterer // Log filterer for contract events +} + +// UpgradeEntrypointCaller is an auto generated read-only Go binding around an Ethereum contract. +type UpgradeEntrypointCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// UpgradeEntrypointTransactor is an auto generated write-only Go binding around an Ethereum contract. +type UpgradeEntrypointTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// UpgradeEntrypointFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type UpgradeEntrypointFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// UpgradeEntrypointSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type UpgradeEntrypointSession struct { + Contract *UpgradeEntrypoint // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// UpgradeEntrypointCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type UpgradeEntrypointCallerSession struct { + Contract *UpgradeEntrypointCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// UpgradeEntrypointTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type UpgradeEntrypointTransactorSession struct { + Contract *UpgradeEntrypointTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// UpgradeEntrypointRaw is an auto generated low-level Go binding around an Ethereum contract. +type UpgradeEntrypointRaw struct { + Contract *UpgradeEntrypoint // Generic contract binding to access the raw methods on +} + +// UpgradeEntrypointCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type UpgradeEntrypointCallerRaw struct { + Contract *UpgradeEntrypointCaller // Generic read-only contract binding to access the raw methods on +} + +// UpgradeEntrypointTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type UpgradeEntrypointTransactorRaw struct { + Contract *UpgradeEntrypointTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewUpgradeEntrypoint creates a new instance of UpgradeEntrypoint, bound to a specific deployed contract. +func NewUpgradeEntrypoint(address common.Address, backend bind.ContractBackend) (*UpgradeEntrypoint, error) { + contract, err := bindUpgradeEntrypoint(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &UpgradeEntrypoint{UpgradeEntrypointCaller: UpgradeEntrypointCaller{contract: contract}, UpgradeEntrypointTransactor: UpgradeEntrypointTransactor{contract: contract}, UpgradeEntrypointFilterer: UpgradeEntrypointFilterer{contract: contract}}, nil +} + +// NewUpgradeEntrypointCaller creates a new read-only instance of UpgradeEntrypoint, bound to a specific deployed contract. +func NewUpgradeEntrypointCaller(address common.Address, caller bind.ContractCaller) (*UpgradeEntrypointCaller, error) { + contract, err := bindUpgradeEntrypoint(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &UpgradeEntrypointCaller{contract: contract}, nil +} + +// NewUpgradeEntrypointTransactor creates a new write-only instance of UpgradeEntrypoint, bound to a specific deployed contract. +func NewUpgradeEntrypointTransactor(address common.Address, transactor bind.ContractTransactor) (*UpgradeEntrypointTransactor, error) { + contract, err := bindUpgradeEntrypoint(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &UpgradeEntrypointTransactor{contract: contract}, nil +} + +// NewUpgradeEntrypointFilterer creates a new log filterer instance of UpgradeEntrypoint, bound to a specific deployed contract. +func NewUpgradeEntrypointFilterer(address common.Address, filterer bind.ContractFilterer) (*UpgradeEntrypointFilterer, error) { + contract, err := bindUpgradeEntrypoint(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &UpgradeEntrypointFilterer{contract: contract}, nil +} + +// bindUpgradeEntrypoint binds a generic wrapper to an already deployed contract. +func bindUpgradeEntrypoint(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := UpgradeEntrypointMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_UpgradeEntrypoint *UpgradeEntrypointRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _UpgradeEntrypoint.Contract.UpgradeEntrypointCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_UpgradeEntrypoint *UpgradeEntrypointRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.UpgradeEntrypointTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_UpgradeEntrypoint *UpgradeEntrypointRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.UpgradeEntrypointTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_UpgradeEntrypoint *UpgradeEntrypointCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _UpgradeEntrypoint.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_UpgradeEntrypoint *UpgradeEntrypointTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_UpgradeEntrypoint *UpgradeEntrypointTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.contract.Transact(opts, method, params...) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_UpgradeEntrypoint *UpgradeEntrypointCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _UpgradeEntrypoint.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_UpgradeEntrypoint *UpgradeEntrypointSession) Owner() (common.Address, error) { + return _UpgradeEntrypoint.Contract.Owner(&_UpgradeEntrypoint.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_UpgradeEntrypoint *UpgradeEntrypointCallerSession) Owner() (common.Address, error) { + return _UpgradeEntrypoint.Contract.Owner(&_UpgradeEntrypoint.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_UpgradeEntrypoint *UpgradeEntrypointCaller) PendingOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _UpgradeEntrypoint.contract.Call(opts, &out, "pendingOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_UpgradeEntrypoint *UpgradeEntrypointSession) PendingOwner() (common.Address, error) { + return _UpgradeEntrypoint.Contract.PendingOwner(&_UpgradeEntrypoint.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_UpgradeEntrypoint *UpgradeEntrypointCallerSession) PendingOwner() (common.Address, error) { + return _UpgradeEntrypoint.Contract.PendingOwner(&_UpgradeEntrypoint.CallOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactor) AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _UpgradeEntrypoint.contract.Transact(opts, "acceptOwnership") +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_UpgradeEntrypoint *UpgradeEntrypointSession) AcceptOwnership() (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.AcceptOwnership(&_UpgradeEntrypoint.TransactOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactorSession) AcceptOwnership() (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.AcceptOwnership(&_UpgradeEntrypoint.TransactOpts) +} + +// PlanUpgrade is a paid mutator transaction binding the contract method 0xef176e0e. +// +// Solidity: function planUpgrade(string name, int64 height, string info) returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactor) PlanUpgrade(opts *bind.TransactOpts, name string, height int64, info string) (*types.Transaction, error) { + return _UpgradeEntrypoint.contract.Transact(opts, "planUpgrade", name, height, info) +} + +// PlanUpgrade is a paid mutator transaction binding the contract method 0xef176e0e. +// +// Solidity: function planUpgrade(string name, int64 height, string info) returns() +func (_UpgradeEntrypoint *UpgradeEntrypointSession) PlanUpgrade(name string, height int64, info string) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.PlanUpgrade(&_UpgradeEntrypoint.TransactOpts, name, height, info) +} + +// PlanUpgrade is a paid mutator transaction binding the contract method 0xef176e0e. +// +// Solidity: function planUpgrade(string name, int64 height, string info) returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactorSession) PlanUpgrade(name string, height int64, info string) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.PlanUpgrade(&_UpgradeEntrypoint.TransactOpts, name, height, info) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _UpgradeEntrypoint.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_UpgradeEntrypoint *UpgradeEntrypointSession) RenounceOwnership() (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.RenounceOwnership(&_UpgradeEntrypoint.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.RenounceOwnership(&_UpgradeEntrypoint.TransactOpts) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _UpgradeEntrypoint.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_UpgradeEntrypoint *UpgradeEntrypointSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.TransferOwnership(&_UpgradeEntrypoint.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_UpgradeEntrypoint *UpgradeEntrypointTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _UpgradeEntrypoint.Contract.TransferOwnership(&_UpgradeEntrypoint.TransactOpts, newOwner) +} + +// UpgradeEntrypointOwnershipTransferStartedIterator is returned from FilterOwnershipTransferStarted and is used to iterate over the raw logs and unpacked data for OwnershipTransferStarted events raised by the UpgradeEntrypoint contract. +type UpgradeEntrypointOwnershipTransferStartedIterator struct { + Event *UpgradeEntrypointOwnershipTransferStarted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *UpgradeEntrypointOwnershipTransferStartedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(UpgradeEntrypointOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(UpgradeEntrypointOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *UpgradeEntrypointOwnershipTransferStartedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *UpgradeEntrypointOwnershipTransferStartedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// UpgradeEntrypointOwnershipTransferStarted represents a OwnershipTransferStarted event raised by the UpgradeEntrypoint contract. +type UpgradeEntrypointOwnershipTransferStarted struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferStarted is a free log retrieval operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) FilterOwnershipTransferStarted(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*UpgradeEntrypointOwnershipTransferStartedIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _UpgradeEntrypoint.contract.FilterLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &UpgradeEntrypointOwnershipTransferStartedIterator{contract: _UpgradeEntrypoint.contract, event: "OwnershipTransferStarted", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferStarted is a free log subscription operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) WatchOwnershipTransferStarted(opts *bind.WatchOpts, sink chan<- *UpgradeEntrypointOwnershipTransferStarted, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _UpgradeEntrypoint.contract.WatchLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(UpgradeEntrypointOwnershipTransferStarted) + if err := _UpgradeEntrypoint.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferStarted is a log parse operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) ParseOwnershipTransferStarted(log types.Log) (*UpgradeEntrypointOwnershipTransferStarted, error) { + event := new(UpgradeEntrypointOwnershipTransferStarted) + if err := _UpgradeEntrypoint.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// UpgradeEntrypointOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the UpgradeEntrypoint contract. +type UpgradeEntrypointOwnershipTransferredIterator struct { + Event *UpgradeEntrypointOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *UpgradeEntrypointOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(UpgradeEntrypointOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(UpgradeEntrypointOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *UpgradeEntrypointOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *UpgradeEntrypointOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// UpgradeEntrypointOwnershipTransferred represents a OwnershipTransferred event raised by the UpgradeEntrypoint contract. +type UpgradeEntrypointOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*UpgradeEntrypointOwnershipTransferredIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _UpgradeEntrypoint.contract.FilterLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &UpgradeEntrypointOwnershipTransferredIterator{contract: _UpgradeEntrypoint.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *UpgradeEntrypointOwnershipTransferred, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _UpgradeEntrypoint.contract.WatchLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(UpgradeEntrypointOwnershipTransferred) + if err := _UpgradeEntrypoint.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) ParseOwnershipTransferred(log types.Log) (*UpgradeEntrypointOwnershipTransferred, error) { + event := new(UpgradeEntrypointOwnershipTransferred) + if err := _UpgradeEntrypoint.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// UpgradeEntrypointSoftwareUpgradeIterator is returned from FilterSoftwareUpgrade and is used to iterate over the raw logs and unpacked data for SoftwareUpgrade events raised by the UpgradeEntrypoint contract. +type UpgradeEntrypointSoftwareUpgradeIterator struct { + Event *UpgradeEntrypointSoftwareUpgrade // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *UpgradeEntrypointSoftwareUpgradeIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(UpgradeEntrypointSoftwareUpgrade) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(UpgradeEntrypointSoftwareUpgrade) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *UpgradeEntrypointSoftwareUpgradeIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *UpgradeEntrypointSoftwareUpgradeIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// UpgradeEntrypointSoftwareUpgrade represents a SoftwareUpgrade event raised by the UpgradeEntrypoint contract. +type UpgradeEntrypointSoftwareUpgrade struct { + Name string + Height int64 + Info string + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSoftwareUpgrade is a free log retrieval operation binding the contract event 0x112749e79b2098b58eab36c21f123b2883c3ecbbb4f41623a744fa6d9b3e37c6. +// +// Solidity: event SoftwareUpgrade(string name, int64 height, string info) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) FilterSoftwareUpgrade(opts *bind.FilterOpts) (*UpgradeEntrypointSoftwareUpgradeIterator, error) { + + logs, sub, err := _UpgradeEntrypoint.contract.FilterLogs(opts, "SoftwareUpgrade") + if err != nil { + return nil, err + } + return &UpgradeEntrypointSoftwareUpgradeIterator{contract: _UpgradeEntrypoint.contract, event: "SoftwareUpgrade", logs: logs, sub: sub}, nil +} + +// WatchSoftwareUpgrade is a free log subscription operation binding the contract event 0x112749e79b2098b58eab36c21f123b2883c3ecbbb4f41623a744fa6d9b3e37c6. +// +// Solidity: event SoftwareUpgrade(string name, int64 height, string info) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) WatchSoftwareUpgrade(opts *bind.WatchOpts, sink chan<- *UpgradeEntrypointSoftwareUpgrade) (event.Subscription, error) { + + logs, sub, err := _UpgradeEntrypoint.contract.WatchLogs(opts, "SoftwareUpgrade") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(UpgradeEntrypointSoftwareUpgrade) + if err := _UpgradeEntrypoint.contract.UnpackLog(event, "SoftwareUpgrade", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSoftwareUpgrade is a log parse operation binding the contract event 0x112749e79b2098b58eab36c21f123b2883c3ecbbb4f41623a744fa6d9b3e37c6. +// +// Solidity: event SoftwareUpgrade(string name, int64 height, string info) +func (_UpgradeEntrypoint *UpgradeEntrypointFilterer) ParseSoftwareUpgrade(log types.Log) (*UpgradeEntrypointSoftwareUpgrade, error) { + event := new(UpgradeEntrypointSoftwareUpgrade) + if err := _UpgradeEntrypoint.contract.UnpackLog(event, "SoftwareUpgrade", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/contracts/bindings/upgradeentrypoint_more.go b/contracts/bindings/upgradeentrypoint_more.go new file mode 100644 index 00000000..0c33c269 --- /dev/null +++ b/contracts/bindings/upgradeentrypoint_more.go @@ -0,0 +1,14 @@ +package bindings + +import ( + _ "embed" +) + +const ( + UpgradeEntrypointDeployedBytecode = "0x608060405234801561001057600080fd5b50600436106100625760003560e01c8063715018a61461006757806379ba5097146100715780638da5cb5b14610079578063e30c3978146100a2578063ef176e0e146100b3578063f2fde38b146100c6575b600080fd5b61006f6100d9565b005b61006f6100ed565b6000546001600160a01b03165b6040516001600160a01b03909116815260200160405180910390f35b6001546001600160a01b0316610086565b61006f6100c13660046102cf565b610136565b61006f6100d436600461035b565b610184565b6100e16101f5565b6100eb6000610222565b565b60015433906001600160a01b0316811461012a5760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61013381610222565b50565b61013e6101f5565b7f112749e79b2098b58eab36c21f123b2883c3ecbbb4f41623a744fa6d9b3e37c685858585856040516101759594939291906103b4565b60405180910390a15050505050565b61018c6101f5565b600180546001600160a01b0383166001600160a01b031990911681179091556101bd6000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b6000546001600160a01b031633146100eb5760405163118cdaa760e01b8152336004820152602401610121565b600180546001600160a01b031916905561013381600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b60008083601f84011261029857600080fd5b50813567ffffffffffffffff8111156102b057600080fd5b6020830191508360208285010111156102c857600080fd5b9250929050565b6000806000806000606086880312156102e757600080fd5b853567ffffffffffffffff808211156102ff57600080fd5b61030b89838a01610286565b909750955060208801359150600782900b821461032757600080fd5b9093506040870135908082111561033d57600080fd5b5061034a88828901610286565b969995985093965092949392505050565b60006020828403121561036d57600080fd5b81356001600160a01b038116811461038457600080fd5b9392505050565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b6060815260006103c860608301878961038b565b8560070b602084015282810360408401526103e481858761038b565b9897505050505050505056fea2646970667358221220b5368e57893f3d7629f1e008c242a82e4675b883e77bab8dd19a2293fdfbae5464736f6c63430008180033" +) + +//go:embed upgradeentrypoint_storage_layout.json +var upgradeentrypointStorageLayoutJSON []byte + +var UpgradeEntrypointStorageLayout = mustGetStorageLayout(upgradeentrypointStorageLayoutJSON) diff --git a/contracts/bindings/upgradeentrypoint_storage_layout.json b/contracts/bindings/upgradeentrypoint_storage_layout.json new file mode 100644 index 00000000..1e729502 --- /dev/null +++ b/contracts/bindings/upgradeentrypoint_storage_layout.json @@ -0,0 +1,27 @@ +{ + "storage": [ + { + "astId": 8, + "contract": "src/protocol/UpgradeEntrypoint.sol:UpgradeEntrypoint", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 156, + "contract": "src/protocol/UpgradeEntrypoint.sol:UpgradeEntrypoint", + "label": "_pendingOwner", + "offset": 0, + "slot": "1", + "type": "t_address" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + } +} diff --git a/contracts/bindings/utils.go b/contracts/bindings/utils.go new file mode 100644 index 00000000..4d7455ee --- /dev/null +++ b/contracts/bindings/utils.go @@ -0,0 +1,16 @@ +package bindings + +import ( + "encoding/json" + + "github.com/piplabs/story/lib/solc" +) + +func mustGetStorageLayout(data []byte) solc.StorageLayout { + var layout solc.StorageLayout + if err := json.Unmarshal(data, &layout); err != nil { + panic(err) + } + + return layout +} diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 00000000..e7aa4dff --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,28 @@ +[profile.default] +src = "src" +out = "out" +libs = ["node_modules"] + +remappings = [ + "forge-std/=node_modules/forge-std/src", + "ds-test/=node_modules/ds-test/src", + "src/=src", + "test/=test", + "@openzeppelin/=node_modules/@openzeppelin/", + "@openzeppelin-upgrades/contracts/=node_modules/@openzeppelin/contracts-upgradeable" +] + +fs_permissions = [ + { access = "read-write", path = "./test" }, + { access = "read-write", path = "./script" }, + { access = "read", path = "./out" }, +] + +libraries = ["src/protocol/libraries/Secp256k1.sol:Secp256k1:0x00000000000000000000000000000000000256f1"] + +[fuzz] +seed = "0x6eed" + +[fmt] +bracket_spacing = true +number_underscore = "thousands" diff --git a/contracts/genbindings.go b/contracts/genbindings.go new file mode 100644 index 00000000..4add3b5f --- /dev/null +++ b/contracts/genbindings.go @@ -0,0 +1,4 @@ +//go:generate make bindings +//go:generate make examples-bindings + +package contracts diff --git a/contracts/mythril.config.json b/contracts/mythril.config.json new file mode 100644 index 00000000..66e077bf --- /dev/null +++ b/contracts/mythril.config.json @@ -0,0 +1,7 @@ +{ + "remappings": [ + "ds-test/=lib/ds-test/src/", + "forge-std/=lib/forge-std/src/", + "@openzeppelin/=node_modules/@openzeppelin/" + ] +} diff --git a/contracts/package.json b/contracts/package.json new file mode 100644 index 00000000..e4f88752 --- /dev/null +++ b/contracts/package.json @@ -0,0 +1,39 @@ +{ + "name": "@piplabs/story-contracts", + "version": "0.1.0-alpha.0", + "license": "GPL-3.0-only", + "repository": "https://github.com/piplabs/story/contracts", + "packageManager": "pnpm@9.1.0", + "files": [ + "src/**/*.sol", + "test/**/*.sol", + "test/**/*.ts", + "script/**/*.sol" + ], + "scripts": { + "test": "pnpm test:gen && forge test", + "lint-full": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", + "lint-fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", + "lint-check": "solhint '{contracts,test}/**/*.sol'" + }, + "devDependencies": { + "@openzeppelin/merkle-tree": "^1.0.5", + "@types/node": "^20.11.7", + "ds-test": "https://github.com/dapphub/ds-test", + "ethereum-cryptography": "^2.1.3", + "forge-std": "https://github.com/foundry-rs/forge-std", + "prettier": "^3.3.3", + "prettier-plugin-solidity": "^1.3.1", + "solhint": "^5.0.1", + "solhint-community": "^4.0.0", + "solhint-plugin-prettier": "^0.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3", + "viem": "^2.5.0" + }, + "dependencies": { + "@openzeppelin/contracts": "5.0.2", + "@openzeppelin/contracts-upgradeable": "5.0.2", + "solmate": "^6.2.0" + } +} diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml new file mode 100644 index 00000000..379d243e --- /dev/null +++ b/contracts/pnpm-lock.yaml @@ -0,0 +1,1510 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@openzeppelin/contracts': + specifier: 5.0.2 + version: 5.0.2 + '@openzeppelin/contracts-upgradeable': + specifier: 5.0.2 + version: 5.0.2(@openzeppelin/contracts@5.0.2) + solmate: + specifier: ^6.2.0 + version: 6.2.0 + devDependencies: + '@openzeppelin/merkle-tree': + specifier: ^1.0.5 + version: 1.0.7 + '@types/node': + specifier: ^20.11.7 + version: 20.14.10 + ds-test: + specifier: https://github.com/dapphub/ds-test + version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 + ethereum-cryptography: + specifier: ^2.1.3 + version: 2.2.1 + forge-std: + specifier: https://github.com/foundry-rs/forge-std + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/07263d193d621c4b2b0ce8b4d54af58f6957d97d + prettier: + specifier: ^3.3.3 + version: 3.3.3 + prettier-plugin-solidity: + specifier: ^1.3.1 + version: 1.3.1(prettier@3.3.3) + solhint: + specifier: ^5.0.1 + version: 5.0.1(typescript@5.5.3) + solhint-community: + specifier: ^4.0.0 + version: 4.0.0(typescript@5.5.3) + solhint-plugin-prettier: + specifier: ^0.1.0 + version: 0.1.0(prettier-plugin-solidity@1.3.1)(prettier@3.3.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.14.10)(typescript@5.5.3) + typescript: + specifier: ^5.3.3 + version: 5.5.3 + viem: + specifier: ^2.5.0 + version: 2.17.4(typescript@5.5.3) + +packages: + + '@adraffy/ens-normalize@1.10.0': + resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@ethersproject/abi@5.7.0': + resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==} + + '@ethersproject/abstract-provider@5.7.0': + resolution: {integrity: sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==} + + '@ethersproject/abstract-signer@5.7.0': + resolution: {integrity: sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==} + + '@ethersproject/address@5.7.0': + resolution: {integrity: sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==} + + '@ethersproject/base64@5.7.0': + resolution: {integrity: sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==} + + '@ethersproject/bignumber@5.7.0': + resolution: {integrity: sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==} + + '@ethersproject/bytes@5.7.0': + resolution: {integrity: sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==} + + '@ethersproject/constants@5.7.0': + resolution: {integrity: sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==} + + '@ethersproject/hash@5.7.0': + resolution: {integrity: sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==} + + '@ethersproject/keccak256@5.7.0': + resolution: {integrity: sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==} + + '@ethersproject/logger@5.7.0': + resolution: {integrity: sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==} + + '@ethersproject/networks@5.7.1': + resolution: {integrity: sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==} + + '@ethersproject/properties@5.7.0': + resolution: {integrity: sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==} + + '@ethersproject/rlp@5.7.0': + resolution: {integrity: sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==} + + '@ethersproject/signing-key@5.7.0': + resolution: {integrity: sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==} + + '@ethersproject/strings@5.7.0': + resolution: {integrity: sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==} + + '@ethersproject/transactions@5.7.0': + resolution: {integrity: sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==} + + '@ethersproject/web@5.7.1': + resolution: {integrity: sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@noble/curves@1.4.0': + resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@openzeppelin/contracts-upgradeable@5.0.2': + resolution: {integrity: sha512-0MmkHSHiW2NRFiT9/r5Lu4eJq5UJ4/tzlOgYXNAIj/ONkQTVnz22pLxDvp4C4uZ9he7ZFvGn3Driptn1/iU7tQ==} + peerDependencies: + '@openzeppelin/contracts': 5.0.2 + + '@openzeppelin/contracts@5.0.2': + resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} + + '@openzeppelin/merkle-tree@1.0.7': + resolution: {integrity: sha512-i93t0YYv6ZxTCYU3CdO5Q+DXK0JH10A4dCBOMlzYbX+ujTXm+k1lXiEyVqmf94t3sqmv8sm/XT5zTa0+efnPgQ==} + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.2.2': + resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} + engines: {node: '>=12'} + + '@prettier/sync@0.3.0': + resolution: {integrity: sha512-3dcmCyAxIcxy036h1I7MQU/uEEBq8oLwf1CE3xeze+MPlgkdlb/+w6rGR/1dhp6Hqi17fRS6nvwnOzkESxEkOw==} + peerDependencies: + prettier: ^3.0.0 + + '@scure/base@1.1.7': + resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@solidity-parser/parser@0.16.2': + resolution: {integrity: sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==} + + '@solidity-parser/parser@0.17.0': + resolution: {integrity: sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==} + + '@solidity-parser/parser@0.18.0': + resolution: {integrity: sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/node@20.14.10': + resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==} + + abitype@1.0.5: + resolution: {integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + acorn-walk@8.3.3: + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} + engines: {node: '>=0.4.0'} + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + antlr4@4.13.1: + resolution: {integrity: sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA==} + engines: {node: '>=16'} + + antlr4@4.13.1-patch-1: + resolution: {integrity: sha512-OjFLWWLzDMV9rdFhpvroCWR4ooktNg9/nvVYSA5z28wuVpU36QUNuioR1XLnQtcjVlf8npjyz593PxnU/f/Cow==} + engines: {node: '>=16'} + + antlr4ts@0.5.0-alpha.4: + resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + ast-parents@0.0.1: + resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + + bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + ds-test@https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0: + resolution: {tarball: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0} + name: ds-test + version: 1.0.0 + + elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.0.1: + resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} + + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/07263d193d621c4b2b0ce8b4d54af58f6957d97d: + resolution: {tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/07263d193d621c4b2b0ce8b4d54af58f6957d97d} + name: forge-std + version: 1.9.1 + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isows@1.0.4: + resolution: {integrity: sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==} + peerDependencies: + ws: '*' + + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + normalize-url@8.0.1: + resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + engines: {node: '>=14.16'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier-plugin-solidity@1.3.1: + resolution: {integrity: sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA==} + engines: {node: '>=16'} + peerDependencies: + prettier: '>=2.3.0' + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + registry-auth-token@5.0.2: + resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + solhint-community@4.0.0: + resolution: {integrity: sha512-BERw3qYzkJE64EwvYrp2+iiTN8yAZOJ74FCiL4bTBp7v0JFUvRYCEGZKAqfHcfi/koKkzM6qThsJUceKm9vvfg==} + hasBin: true + + solhint-plugin-prettier@0.1.0: + resolution: {integrity: sha512-SDOTSM6tZxZ6hamrzl3GUgzF77FM6jZplgL2plFBclj/OjKP8Z3eIPojKU73gRr0MvOS8ACZILn8a5g0VTz/Gw==} + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-solidity: ^1.0.0 + + solhint@5.0.1: + resolution: {integrity: sha512-QeQLS9HGCnIiibt+xiOa/+MuP7BWz9N7C5+Mj9pLHshdkNhuo3AzCpWmjfWVZBUuwIUO3YyCRVIcYLR3YOKGfg==} + hasBin: true + + solidity-comments-extractor@0.0.8: + resolution: {integrity: sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==} + + solmate@6.2.0: + resolution: {integrity: sha512-AM38ioQ2P8zRsA42zenb9or6OybRjOLXIu3lhIT8rhddUuduCt76pUEuLxOIg9GByGojGz+EbpFdCB6B+QZVVA==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + table@6.8.2: + resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==} + engines: {node: '>=10.0.0'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + viem@2.17.4: + resolution: {integrity: sha512-6gmBB85I7z1qt/+yPPS+i4L2jNPJqCs+SEb+26WnKVYez13svSzjYMsU9OYYlPFpQmpGSy9dV2bKW6VX68FTgg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@adraffy/ens-normalize@1.10.0': {} + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@ethersproject/abi@5.7.0': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@ethersproject/abstract-provider@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 + + '@ethersproject/abstract-signer@5.7.0': + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + + '@ethersproject/address@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/rlp': 5.7.0 + + '@ethersproject/base64@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + + '@ethersproject/bignumber@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + bn.js: 5.2.1 + + '@ethersproject/bytes@5.7.0': + dependencies: + '@ethersproject/logger': 5.7.0 + + '@ethersproject/constants@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + + '@ethersproject/hash@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@ethersproject/keccak256@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + js-sha3: 0.8.0 + + '@ethersproject/logger@5.7.0': {} + + '@ethersproject/networks@5.7.1': + dependencies: + '@ethersproject/logger': 5.7.0 + + '@ethersproject/properties@5.7.0': + dependencies: + '@ethersproject/logger': 5.7.0 + + '@ethersproject/rlp@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + + '@ethersproject/signing-key@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + bn.js: 5.2.1 + elliptic: 6.5.4 + hash.js: 1.1.7 + + '@ethersproject/strings@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + + '@ethersproject/transactions@5.7.0': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + + '@ethersproject/web@5.7.1': + dependencies: + '@ethersproject/base64': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@noble/curves@1.4.0': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/hashes@1.4.0': {} + + '@openzeppelin/contracts-upgradeable@5.0.2(@openzeppelin/contracts@5.0.2)': + dependencies: + '@openzeppelin/contracts': 5.0.2 + + '@openzeppelin/contracts@5.0.2': {} + + '@openzeppelin/merkle-tree@1.0.7': + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.2.2': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@prettier/sync@0.3.0(prettier@3.3.3)': + dependencies: + prettier: 3.3.3 + + '@scure/base@1.1.7': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + + '@sindresorhus/is@5.6.0': {} + + '@solidity-parser/parser@0.16.2': + dependencies: + antlr4ts: 0.5.0-alpha.4 + + '@solidity-parser/parser@0.17.0': {} + + '@solidity-parser/parser@0.18.0': {} + + '@szmarczak/http-timer@5.0.1': + dependencies: + defer-to-connect: 2.0.1 + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/node@20.14.10': + dependencies: + undici-types: 5.26.5 + + abitype@1.0.5(typescript@5.5.3): + dependencies: + typescript: 5.5.3 + + acorn-walk@8.3.3: + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.1 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + antlr4@4.13.1: {} + + antlr4@4.13.1-patch-1: {} + + antlr4ts@0.5.0-alpha.4: {} + + arg@4.1.3: {} + + argparse@2.0.1: {} + + ast-parents@0.0.1: {} + + astral-regex@2.0.0: {} + + balanced-match@1.0.2: {} + + bn.js@4.12.0: {} + + bn.js@5.2.1: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + brorand@1.1.0: {} + + cacheable-lookup@7.0.0: {} + + cacheable-request@10.2.14: + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.1 + responselike: 3.0.0 + + callsites@3.1.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + commander@10.0.1: {} + + commander@11.1.0: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + cosmiconfig@8.3.6(typescript@5.5.3): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.5.3 + + create-require@1.1.1: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + defer-to-connect@2.0.1: {} + + diff@4.0.2: {} + + ds-test@https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0: {} + + elliptic@6.5.4: + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emoji-regex@8.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + escape-string-regexp@1.0.5: {} + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.0.1: {} + + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/07263d193d621c4b2b0ce8b4d54af58f6957d97d: {} + + form-data-encoder@2.1.4: {} + + fs.realpath@1.0.0: {} + + get-stream@6.0.1: {} + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + got@12.6.1: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + + graceful-fs@4.2.10: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + http-cache-semantics@4.1.1: {} + + http2-wrapper@2.2.1: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + ignore@5.3.1: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + isows@1.0.4(ws@8.17.1): + dependencies: + ws: 8.17.1 + + js-sha3@0.8.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + latest-version@7.0.0: + dependencies: + package-json: 8.1.1 + + lines-and-columns@1.2.4: {} + + lodash.truncate@4.4.2: {} + + lodash@4.17.21: {} + + lowercase-keys@3.0.0: {} + + make-error@1.3.6: {} + + mimic-response@3.1.0: {} + + mimic-response@4.0.0: {} + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + normalize-url@8.0.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-cancelable@3.0.0: {} + + package-json@8.1.1: + dependencies: + got: 12.6.1 + registry-auth-token: 5.0.2 + registry-url: 6.0.1 + semver: 7.6.3 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-type@4.0.0: {} + + picocolors@1.0.1: {} + + pluralize@8.0.0: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier-plugin-solidity@1.3.1(prettier@3.3.3): + dependencies: + '@solidity-parser/parser': 0.17.0 + prettier: 3.3.3 + semver: 7.6.3 + solidity-comments-extractor: 0.0.8 + + prettier@2.8.8: + optional: true + + prettier@3.3.3: {} + + proto-list@1.2.4: {} + + punycode@2.3.1: {} + + quick-lru@5.1.1: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + registry-auth-token@5.0.2: + dependencies: + '@pnpm/npm-conf': 2.2.2 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + require-from-string@2.0.2: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + responselike@3.0.0: + dependencies: + lowercase-keys: 3.0.0 + + semver@6.3.1: {} + + semver@7.6.3: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + solhint-community@4.0.0(typescript@5.5.3): + dependencies: + '@solidity-parser/parser': 0.16.2 + ajv: 6.12.6 + antlr4: 4.13.1 + ast-parents: 0.0.1 + chalk: 4.1.2 + commander: 11.1.0 + cosmiconfig: 8.3.6(typescript@5.5.3) + fast-diff: 1.3.0 + glob: 8.1.0 + ignore: 5.3.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + pluralize: 8.0.0 + semver: 6.3.1 + strip-ansi: 6.0.1 + table: 6.8.2 + text-table: 0.2.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - typescript + + solhint-plugin-prettier@0.1.0(prettier-plugin-solidity@1.3.1)(prettier@3.3.3): + dependencies: + '@prettier/sync': 0.3.0(prettier@3.3.3) + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + prettier-plugin-solidity: 1.3.1(prettier@3.3.3) + + solhint@5.0.1(typescript@5.5.3): + dependencies: + '@solidity-parser/parser': 0.18.0 + ajv: 6.12.6 + antlr4: 4.13.1-patch-1 + ast-parents: 0.0.1 + chalk: 4.1.2 + commander: 10.0.1 + cosmiconfig: 8.3.6(typescript@5.5.3) + fast-diff: 1.3.0 + glob: 8.1.0 + ignore: 5.3.1 + js-yaml: 4.1.0 + latest-version: 7.0.0 + lodash: 4.17.21 + pluralize: 8.0.0 + semver: 7.6.3 + strip-ansi: 6.0.1 + table: 6.8.2 + text-table: 0.2.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - typescript + + solidity-comments-extractor@0.0.8: {} + + solmate@6.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + table@6.8.2: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + text-table@0.2.0: {} + + ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.14.10 + acorn: 8.12.1 + acorn-walk: 8.3.3 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + typescript@5.5.3: {} + + undici-types@5.26.5: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + v8-compile-cache-lib@3.0.1: {} + + viem@2.17.4(typescript@5.5.3): + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + abitype: 1.0.5(typescript@5.5.3) + isows: 1.0.4(ws@8.17.1) + typescript: 5.5.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + wrappy@1.0.2: {} + + ws@8.17.1: {} + + yn@3.1.1: {} diff --git a/contracts/script/DeployIPTokenStaking.sol b/contracts/script/DeployIPTokenStaking.sol new file mode 100644 index 00000000..370d310b --- /dev/null +++ b/contracts/script/DeployIPTokenStaking.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ + +import { Script } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; + +import { IPTokenStaking } from "../src/protocol/IPTokenStaking.sol"; + +/** + * @title DeployIPTokenStaking + * @dev A script + utilities to deploy the IPTokenStaking contract + */ +contract DeployIPTokenStaking is Script { + function run() public { + // TODO: read env + address protocolAccessManagerAddr = address(0x438d47Bc1e184b976fB9EB06Fa4EA27e1247674E); + + require(block.chainid == 1, "only mainnet deployment"); + require(protocolAccessManagerAddr != address(0), "address not set"); + + uint256 deployerKey = vm.envUint("IPTOKENSTAKING_DEPLOYER_KEY"); + + vm.startBroadcast(deployerKey); + IPTokenStaking ipTokenStaking = new IPTokenStaking( + protocolAccessManagerAddr, + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.stopBroadcast(); + + console2.log("IPTokenStaking deployed at:", address(ipTokenStaking)); + } +} diff --git a/contracts/slither.config.json b/contracts/slither.config.json new file mode 100644 index 00000000..24abc065 --- /dev/null +++ b/contracts/slither.config.json @@ -0,0 +1,3 @@ +{ + "filter_paths": "lib" +} diff --git a/contracts/src/deploy/Create3.sol b/contracts/src/deploy/Create3.sol new file mode 100644 index 00000000..c7129f2a --- /dev/null +++ b/contracts/src/deploy/Create3.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.23; + +import { CREATE3 } from "solmate/src/utils/CREATE3.sol"; + +/** + * @title Create3 + * @notice Factory for deploying contracts to deterministic addresses via CREATE3 Enables deploying + * contracts using CREATE3. Each deployer (msg.sender) has its own namespace for deployed + * addresses. + * @author zefram.eth + * @custom:attribution zefram.eth (https://github.com/ZeframLou/create3-factory/blob/main/src/CREATE3Factory.sol) + */ +contract Create3 { + /** + * @notice Deploys a contract using CREATE3 + * @dev The provided salt is hashed together with msg.sender to generate the final salt + * @param salt The deployer-specific salt for determining the deployed contract's address + * @param creationCode The creation code of the contract to deploy + * @return deployed The address of the deployed contract + */ + function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address deployed) { + // hash salt with the deployer address to give each deployer its own namespace + salt = keccak256(abi.encodePacked(msg.sender, salt)); + return CREATE3.deploy(salt, creationCode, msg.value); + } + + /** + * @notice Predicts the address of a deployed contract + * @dev The provided salt is hashed together with the deployer address to generate the final salt + * @param deployer The deployer account that will call deploy() + * @param salt The deployer-specific salt for determining the deployed contract's address + * @return deployed The address of the contract that will be deployed + */ + function getDeployed(address deployer, bytes32 salt) external view returns (address deployed) { + // hash salt with the deployer address to give each deployer its own namespace + salt = keccak256(abi.encodePacked(deployer, salt)); + return CREATE3.getDeployed(salt); + } +} diff --git a/contracts/src/interfaces/IIPTokenSlashing.sol b/contracts/src/interfaces/IIPTokenSlashing.sol new file mode 100644 index 00000000..01fa5972 --- /dev/null +++ b/contracts/src/interfaces/IIPTokenSlashing.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +interface IIPTokenSlashing { + /// @notice Emitted when a request to unjail a validator is made + /// @param sender The address that sent the unjail request + /// @param validatorCmpPubkey 33 bytes compressed secp256k1 public key. + event Unjail(address indexed sender, bytes validatorCmpPubkey); + + /// @notice Emitted when the unjail fee is updated + /// @param newUnjailFee The new unjail fee + event UnjailFeeSet(uint256 newUnjailFee); + + /// @notice Requests to unjail the validator. Must pay fee on the execution side to prevent spamming. + /// @param validatorUncmpPubkey The validator's 65-byte uncompressed Secp256k1 public key + function unjail(bytes calldata validatorUncmpPubkey) external payable; + + /// @notice Requests to unjail a validator on behalf. Must pay fee on the execution side to prevent spamming. + /// @param validatorUncmpPubkey The validator's 65-byte uncompressed Secp256k1 public key + function unjailOnBehalf(bytes calldata validatorUncmpPubkey) external payable; +} diff --git a/contracts/src/interfaces/IIPTokenStaking.sol b/contracts/src/interfaces/IIPTokenStaking.sol new file mode 100644 index 00000000..9b0cd8f9 --- /dev/null +++ b/contracts/src/interfaces/IIPTokenStaking.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +interface IIPTokenStaking { + /// @param exists Set to true once the validator is created. + /// @param moniker The moniker of the validator. + /// @param totalStake Total amount of stake for the validator. + /// @param commissionRate The commission rate of the validator. + /// @param maxCommissionRate The maximum commission rate of the validator. + /// @param maxCommissionChangeRate The maximum commission change rate of the validator. + struct ValidatorMetadata { + bool exists; + string moniker; + uint256 totalStake; + uint32 commissionRate; + uint32 maxCommissionRate; + uint32 maxCommissionChangeRate; + } + + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param validatorSrcPubkey Source validator's 33 bytes compressed secp256k1 public key. + /// @param validatorDstPubkey Destination validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token amount to redelegate. + struct RedelegateParams { + bytes delegatorUncmpPubkey; + bytes validatorSrcPubkey; + bytes validatorDstPubkey; + uint256 amount; + } + + /// @notice Emitted when a new validator is created. + /// @param validatorPubkey 33 bytes compressed secp256k1 public key. + /// @param moniker The moniker of the validator. + /// @param stakeAmount Token staked to the validator as self-delegation. + /// @param commissionRate The commission rate of the validator. + /// @param maxCommissionRate The maximum commission rate of the validator. + /// @param maxCommissionChangeRate The maximum commission change rate of the validator. + event CreateValidator( + bytes validatorPubkey, + string moniker, + uint256 stakeAmount, + uint32 commissionRate, + uint32 maxCommissionRate, + uint32 maxCommissionChangeRate + ); + + /// @notice Emitted when the withdrawal address is set/changed. + /// @param depositorPubkey Depositor's 33 bytes compressed secp256k1 public key. + /// @param executionAddress Left-padded 32 bytes of the EVM address to receive stake and reward withdrawals. + event SetWithdrawalAddress(bytes depositorPubkey, bytes32 executionAddress); + + /// @notice Emitted when a user deposits token into the contract. + /// @param depositorPubkey Depositor's 33 bytes compressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token deposited. + event Deposit(bytes depositorPubkey, bytes validatorPubkey, uint256 amount); + + /// @notice Emitted when a user triggers redelegation of token from source validator to destination validator. + /// @param depositorPubkey Depositor's 33 bytes compressed secp256k1 public key. + /// @param validatorSrcPubkey Source validator's 33 bytes compressed secp256k1 public key. + /// @param validatorDstPubkey Destination validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token redelegated. + event Redelegate(bytes depositorPubkey, bytes validatorSrcPubkey, bytes validatorDstPubkey, uint256 amount); + + /// @notice Emitted when a user withdraws her stake and starts the unbonding period. + /// @param depositorPubkey Depositor's 33 bytes compressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token deposited. + event Withdraw(bytes depositorPubkey, bytes validatorPubkey, uint256 amount); + + /// @notice Emitted when the minimum stake amount is set. + /// @param minStakeAmount The new minimum stake amount. + event MinStakeAmountSet(uint256 minStakeAmount); + + /// @notice Emitted when the minimum unstake amount is set. + /// @param minUnstakeAmount The new minimum unstake amount. + event MinUnstakeAmountSet(uint256 minUnstakeAmount); + + /// @notice Emitted when the minimum redelegation amount is set. + /// @param minRedelegateAmount The new minimum redelegation amount. + event MinRedelegateAmountSet(uint256 minRedelegateAmount); + + /// @notice Emitted when the unbonding period is set. + /// @param newInterval The new unbonding period. + event WithdrawalAddressChangeIntervalSet(uint256 newInterval); + + /// @notice Returns the rounded stake amount and the remainder. + /// @param rawAmount The raw stake amount. + /// @return amount The rounded stake amount. + /// @return remainder The remainder of the stake amount. + function roundedStakeAmount(uint256 rawAmount) external view returns (uint256 amount, uint256 remainder); + + /// @notice Returns the operators for the delegator. + /// @param pubkey 33 bytes compressed secp256k1 public key. + function getOperators(bytes calldata pubkey) external view returns (address[] memory); + + /// @notice Adds an operator for the delegator. + /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key. + /// @param operator The operator address to add. + function addOperator(bytes calldata uncmpPubkey, address operator) external; + + /// @notice Removes an operator for the delegator. + /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key. + /// @param operator The operator address to remove. + function removeOperator(bytes calldata uncmpPubkey, address operator) external; + + /// @notice Set/Update the withdrawal address that receives the stake and reward withdrawals. + /// @dev To prevent spam, only delegators with stake can call this function with cool-down time. + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param newWithdrawalAddress EVM address to receive the stake and reward withdrawals. + function setWithdrawalAddress(bytes calldata delegatorUncmpPubkey, address newWithdrawalAddress) external; + + /// @notice Entry point for creating a new validator with self delegation. + /// @dev The caller must provide the uncompressed public key that matches the expected EVM address. + /// @param validatorUncmpPubkey 65 bytes uncompressed secp256k1 public key. + /// @param moniker The moniker of the validator. + /// @param commissionRate The commission rate of the validator. + /// @param maxCommissionRate The maximum commission rate of the validator. + /// @param maxCommissionChangeRate The maximum commission change rate of the validator. + function createValidator( + bytes calldata validatorUncmpPubkey, + string calldata moniker, + uint32 commissionRate, + uint32 maxCommissionRate, + uint32 maxCommissionChangeRate + ) external payable; + + /// @notice Entry point for creating a new validator with self delegation on behalf of the validator. + /// @dev There's no minimum amount required to stake when creating a new validator. + /// @param validatorPubkey 33 bytes compressed secp256k1 public key. + function createValidatorOnBehalf(bytes calldata validatorPubkey) external payable; + + /// @notice Entry point for staking IP token to stake to the given validator. The consensus chain is notified of + /// the deposit and manages the stake accounting and validator onboarding. Payer must be the delegator. + /// @dev When staking, consider it as BURNING. Unstaking (withdrawal) will trigger native minting. + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + function stake(bytes calldata delegatorUncmpPubkey, bytes calldata validatorPubkey) external payable; + + /// @notice Entry point for staking IP token to stake to the given validator. The consensus chain is notified of + /// the stake and manages the stake accounting and validator onboarding. Payer can stake on behalf of another user, + /// who will be the beneficiary of the stake. + /// @dev When staking, consider it as BURNING. Unstaking (withdrawal) will trigger native minting. + /// @param delegatorPubkey Delegator's 33 bytes compressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + function stakeOnBehalf(bytes calldata delegatorPubkey, bytes calldata validatorPubkey) external payable; + + // TODO: Redelegation also requires unbonding period to be executed. Should we separate storage for this for el? + /// @notice Entry point for redelegating the staked token. + /// @dev Redelegateion redelegates staked token from src validator to dst validator (x/staking.MsgBeginRedelegate) + /// @param p See RedelegateParams + function redelegate(RedelegateParams calldata p) external; + + /// @notice Entry point for redelegating the staked token on behalf of the delegator. + /// @dev Redelegateion redelegates staked token from src validator to dst validator (x/staking.MsgBeginRedelegate) + /// @param p See RedelegateParams + function redelegateOnBehalf(RedelegateParams calldata p) external; + + /// @notice Entry point for unstaking the previously staked token. + /// @dev Unstake (withdrawal) will trigger native minting, so token in this contract is considered as burned. + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token amount to unstake. + function unstake(bytes calldata delegatorUncmpPubkey, bytes calldata validatorPubkey, uint256 amount) external; + + /// @notice Entry point for unstaking the previously staked token on behalf of the delegator. + /// @dev Must be an approved operator for the delegator. + /// @param delegatorCmpPubkey Delegator's 33 bytes compressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token amount to unstake. + function unstakeOnBehalf( + bytes calldata delegatorCmpPubkey, + bytes calldata validatorPubkey, + uint256 amount + ) external; +} diff --git a/contracts/src/interfaces/IUpgradeEntrypoint.sol b/contracts/src/interfaces/IUpgradeEntrypoint.sol new file mode 100644 index 00000000..ac8c4676 --- /dev/null +++ b/contracts/src/interfaces/IUpgradeEntrypoint.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +interface IUpgradeEntrypoint { + /// PROTO: https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/proto/cosmos/upgrade/v1beta1/upgrade.proto + /// @notice Emitted when an upgrade is submitted. + /// @param name Sets the name for the upgrade. This name will be used by the upgraded version of the software to + /// apply any special "on-upgrade" commands during the first BeginBlock method after the upgrade is applied. It is + /// also used to detect whether a software version can handle a given upgrade. If no upgrade handler with this name + /// has been set in the software, it will be assumed that the software is out-of-date when the upgrade Time or + /// Height is reached and the software will exit. + /// @param height The height at which the upgrade must be performed. + /// @param info Any application specific upgrade info to be included on-chain such as a git commit that validators + /// could automatically upgrade to. + event SoftwareUpgrade(string name, int64 height, string info); + + /// @notice Submits an upgrade plan. + /// @param name Sets the name for the upgrade. This name will be used by the upgraded version of the software to + /// apply any special "on-upgrade" commands during the first BeginBlock method after the upgrade is applied. It is + /// also used to detect whether a software version can handle a given upgrade. If no upgrade handler with this name + /// has been set in the software, it will be assumed that the software is out-of-date when the upgrade Time or + /// Height is reached and the software will exit. + /// @param height The height at which the upgrade must be performed. + /// @param info Any application specific upgrade info to be included on-chain such as a git commit that validators + /// could automatically upgrade to. + function planUpgrade(string calldata name, int64 height, string calldata info) external; +} diff --git a/contracts/src/libraries/Secp256k1.sol b/contracts/src/libraries/Secp256k1.sol new file mode 100644 index 00000000..9d0d8dfd --- /dev/null +++ b/contracts/src/libraries/Secp256k1.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +library Secp256k1 { + /// @notice Compress an uncompressed 65-byte Secp256k1 public key. + /// @dev Assumes that the input is a valid 65-byte uncompressed public key. + function compressPublicKey(bytes memory uncompressedKey) public pure returns (bytes memory) { + require(uncompressedKey.length == 65, "Invalid uncompressed public key length"); + + // Extract the X and Y coordinates + bytes32 x; + bytes32 y; + + assembly { + x := mload(add(uncompressedKey, 0x21)) + y := mload(add(uncompressedKey, 0x41)) + } + + // Determine the prefix (0x02 for even Y, 0x03 for odd Y) + bytes1 prefix = (uint8(y[31]) % 2 == 0) ? bytes1(0x02) : bytes1(0x03); + + // Concatenate the prefix and X coordinate + bytes memory compressedKey = new bytes(33); + compressedKey[0] = prefix; + + for (uint256 i = 0; i < 32; i++) { + compressedKey[i + 1] = x[i]; + } + + return compressedKey; + } +} diff --git a/contracts/src/protocol/IPTokenSlashing.sol b/contracts/src/protocol/IPTokenSlashing.sol new file mode 100644 index 00000000..d4b22ef3 --- /dev/null +++ b/contracts/src/protocol/IPTokenSlashing.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; + +import { IIPTokenSlashing } from "../interfaces/IIPTokenSlashing.sol"; +import { IPTokenStaking } from "./IPTokenStaking.sol"; +import { Secp256k1 } from "../libraries/Secp256k1.sol"; + +/** + * @title IPTokenSlashing + * @notice The EVM interface to the consensus chain's x/slashing module. Calls are proxied to the consensus chain, but + * not executed synchronously; execution is left to the consensus chain, which may fail. + */ +contract IPTokenSlashing is IIPTokenSlashing, Ownable2Step { + /// @notice IPTokenStaking contract address. + IPTokenStaking public immutable IP_TOKEN_STAKING; + + /// @notice The fee paid to unjail a validator. + uint256 public unjailFee; + + constructor(address newOwner, address ipTokenStaking, uint256 newUnjailFee) Ownable(newOwner) { + require(newUnjailFee > 0, "IPTokenSlashing: Invalid unjail fee"); + require(ipTokenStaking != address(0), "IPTokenSlashing: Invalid IPTokenStaking address"); + IP_TOKEN_STAKING = IPTokenStaking(ipTokenStaking); + unjailFee = newUnjailFee; + } + + /// @notice Verifies that the given 65 byte uncompressed secp256k1 public key (with 0x04 prefix) is valid and + /// matches the expected EVM address. + modifier verifyUncmpPubkeyWithExpectedAddress(bytes calldata uncmpPubkey, address expectedAddress) { + require(uncmpPubkey.length == 65, "IPTokenSlashing: Invalid pubkey length"); + require(uncmpPubkey[0] == 0x04, "IPTokenSlashing: Invalid pubkey prefix"); + require( + _uncmpPubkeyToAddress(uncmpPubkey) == expectedAddress, + "IPTokenSlashing: Invalid pubkey derived address" + ); + _; + } + + /// @notice Sets the unjail fee. + /// @param newUnjailFee The new unjail fee. + function setUnjailFee(uint256 newUnjailFee) external onlyOwner { + unjailFee = newUnjailFee; + emit UnjailFeeSet(newUnjailFee); + } + + /// @notice Converts the given public key to an EVM address. + /// @dev Assume all calls to this function passes in the uncompressed public key. + /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key, with prefix 04. + /// @return address The EVM address derived from the public key. + function _uncmpPubkeyToAddress(bytes calldata uncmpPubkey) internal pure returns (address) { + return address(uint160(uint256(keccak256(uncmpPubkey[1:])))); + } + + /// @notice Requests to unjail the validator. Must pay fee on the execution side to prevent spamming. + function unjail( + bytes calldata validatorUncmpPubkey + ) external payable verifyUncmpPubkeyWithExpectedAddress(validatorUncmpPubkey, msg.sender) { + bytes memory validatorCmpPubkey = Secp256k1.compressPublicKey(validatorUncmpPubkey); + _verifyExistingValidator(validatorCmpPubkey); + _unjail(validatorCmpPubkey); + } + + /// @notice Requests to unjail a validator on behalf. Must pay fee on the execution side to prevent spamming. + /// @param validatorCmpPubkey The validator's 33-byte compressed Secp256k1 public key + function unjailOnBehalf(bytes calldata validatorCmpPubkey) external payable { + _verifyExistingValidator(validatorCmpPubkey); + _unjail(validatorCmpPubkey); + } + + /// @dev Emits the Unjail event after burning the fee. + function _unjail(bytes memory validatorCmpPubkey) internal { + require(msg.value == unjailFee, "IPTokenSlashing: Insufficient fee"); + payable(address(0x0)).transfer(msg.value); + emit Unjail(msg.sender, validatorCmpPubkey); + } + + /// @notice Verifies that the validator with the given pubkey exists. + /// @param validatorCmpPubkey The validator's 33-byte compressed Secp256k1 public key + function _verifyExistingValidator(bytes memory validatorCmpPubkey) internal view { + require(validatorCmpPubkey.length == 33, "IPTokenSlashing: Invalid pubkey length"); + require( + validatorCmpPubkey[0] == 0x02 || validatorCmpPubkey[0] == 0x03, + "IPTokenSlashing: Invalid pubkey prefix" + ); + (bool validatorExists, , , , , ) = IP_TOKEN_STAKING.validatorMetadata(validatorCmpPubkey); + require(validatorExists, "IPTokenSlashing: Validator does not exist"); + } +} diff --git a/contracts/src/protocol/IPTokenStaking.sol b/contracts/src/protocol/IPTokenStaking.sol new file mode 100644 index 00000000..fb2a6390 --- /dev/null +++ b/contracts/src/protocol/IPTokenStaking.sol @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import { IIPTokenStaking } from "../interfaces/IIPTokenStaking.sol"; +import { Secp256k1 } from "../libraries/Secp256k1.sol"; + +/** + * @title IPTokenStaking + * @notice The deposit contract for IP token staked validators. + */ +contract IPTokenStaking is IIPTokenStaking, Ownable2Step, ReentrancyGuard { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Default commission rate for a validator. Out of 100%, or 10_000. + uint32 public immutable DEFAULT_COMMISSION_RATE; + + /// @notice Default maximum commission rate for a validator. Out of 100%, or 10_000. + uint32 public immutable DEFAULT_MAX_COMMISSION_RATE; + + /// @notice Default maximum commission change rate for a validator. Out of 100%, or 10_000. + uint32 public immutable DEFAULT_MAX_COMMISSION_CHANGE_RATE; + + /// @notice Stake amount increments, 1 ether => e.g. 1 ether, 2 ether, 5 ether etc. + uint256 public immutable STAKE_ROUNDING; + + /// @notice Minimum amount required to stake. + uint256 public minStakeAmount; + + /// @notice Minimum amount required to unstake. + uint256 public minUnstakeAmount; + + /// @notice Minimum amount required to redelegate. + uint256 public minRedelegateAmount; + + /// @notice The interval between changing the withdrawal address for each delegator + uint256 public withdrawalAddressChangeInterval; + + /// @notice Validator's metadata. + /// @dev Validator public key is a 33-byte compressed Secp256k1 public key. + mapping(bytes validatorCmpPubkey => ValidatorMetadata metadata) public validatorMetadata; + + /// @notice Delegator's total staked amount. + /// @dev Delegator public key is a 33-byte compressed Secp256k1 public key. + mapping(bytes delegatorCmpPubkey => uint256 stakedAmount) public delegatorTotalStakes; + + /// @notice Delegator's staked amount for the given validator. + /// @dev Delegator and validator public keys are 33-byte compressed Secp256k1 public keys. + mapping(bytes delegatorCmpPubkey => mapping(bytes validatorCmpPubkey => uint256 stakedAmount)) + public delegatorValidatorStakes; + + /// @notice Approved operators for delegators. + /// @dev Delegator public key is a 33-byte compressed Secp256k1 public key. + mapping(bytes delegatorCmpPubkey => EnumerableSet.AddressSet operators) private delegatorOperators; + + /// @notice The timestamp of last withdrawal address changes for each delegator. + /// @dev Delegator public key is a 33-byte compressed Secp256k1 public key. + mapping(bytes delegatorCmpPubkey => uint256 lastChange) public withdrawalAddressChange; + + constructor( + address newOwner, + uint256 _minStakeAmount, + uint256 _minUnstakeAmount, + uint256 _minRedelegateAmount, + uint256 stakingRounding, + uint256 _withdrawalAddressChangeInterval, + uint32 defaultCommissionRate, + uint32 defaultMaxCommissionRate, + uint32 defaultMaxCommissionChangeRate + ) Ownable(newOwner) { + STAKE_ROUNDING = stakingRounding; // Recommended: 1 gwei (10^9) + + _setMinStakeAmount(_minStakeAmount); + _setMinUnstakeAmount(_minUnstakeAmount); + _setMinRedelegateAmount(_minRedelegateAmount); + _setWithdrawalAddressChangeInterval(_withdrawalAddressChangeInterval); + + require(defaultCommissionRate <= 10_000, "IPTokenStaking: Invalid default commission rate"); + DEFAULT_COMMISSION_RATE = defaultCommissionRate; // Recommended: 10%, or 1_000 / 10_000 + + require( + defaultMaxCommissionRate >= DEFAULT_COMMISSION_RATE && defaultMaxCommissionRate <= 10_000, + "IPTokenStaking: Invalid default max commission rate" + ); + DEFAULT_MAX_COMMISSION_RATE = defaultMaxCommissionRate; // Recommended: 50%, or 5_000 / 10_000 + + require(defaultMaxCommissionChangeRate <= 10_000, "IPTokenStaking: Invalid default max commission change rate"); + DEFAULT_MAX_COMMISSION_CHANGE_RATE = defaultMaxCommissionChangeRate; // Recommended: 5%, or 500 / 10_000 + } + + /// @notice Verifies that the syntax of the given public key is a 33 byte compressed secp256k1 public key. + modifier verifyPubkey(bytes calldata pubkey) { + require(pubkey.length == 33, "IPTokenStaking: Invalid pubkey length"); + require(pubkey[0] == 0x02 || pubkey[0] == 0x03, "IPTokenStaking: Invalid pubkey prefix"); + _; + } + + /// @notice Verifies that the syntax of the given public key is a 65 byte uncompressed secp256k1 public key. + modifier verifyUncmpPubkey(bytes calldata uncmpPubkey) { + require(uncmpPubkey.length == 65, "IPTokenStaking: Invalid pubkey length"); + require(uncmpPubkey[0] == 0x04, "IPTokenStaking: Invalid pubkey prefix"); + _; + } + + /// @notice Verifies that the given 65 byte uncompressed secp256k1 public key (with 0x04 prefix) is valid and + /// matches the expected EVM address. + modifier verifyUncmpPubkeyWithExpectedAddress(bytes calldata uncmpPubkey, address expectedAddress) { + require(uncmpPubkey.length == 65, "IPTokenStaking: Invalid pubkey length"); + require(uncmpPubkey[0] == 0x04, "IPTokenStaking: Invalid pubkey prefix"); + require( + _uncmpPubkeyToAddress(uncmpPubkey) == expectedAddress, + "IPTokenStaking: Invalid pubkey derived address" + ); + _; + } + + /// @notice Verifies that the validator with the given pubkey exists. + modifier verifyExistingValidator(bytes calldata validatorPubkey) { + require(validatorPubkey.length == 33, "IPTokenStaking: Invalid pubkey length"); + require(validatorPubkey[0] == 0x02 || validatorPubkey[0] == 0x03, "IPTokenStaking: Invalid pubkey prefix"); + require(validatorMetadata[validatorPubkey].exists, "IPTokenStaking: Validator does not exist"); + _; + } + + /*////////////////////////////////////////////////////////////////////////// + // Setters/Getters // + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Sets the minimum amount required to stake. + /// @param newMinStakeAmount The minimum amount required to stake. + function setMinStakeAmount(uint256 newMinStakeAmount) external onlyOwner { + _setMinStakeAmount(newMinStakeAmount); + } + + /// @dev Sets the minimum amount required to withdraw. + /// @param newMinUnstakeAmount The minimum amount required to stake. + function setMinUnstakeAmount(uint256 newMinUnstakeAmount) external onlyOwner { + _setMinUnstakeAmount(newMinUnstakeAmount); + } + + /// @dev Sets the minimum amount required to redelegate. + /// @param newMinRedelegateAmount The minimum amount required to redelegate. + function setMinRedelegateAmount(uint256 newMinRedelegateAmount) external onlyOwner { + _setMinRedelegateAmount(newMinRedelegateAmount); + } + + /// @dev Sets the interval for updating the withdrawal address. + /// @param newWithdrawalAddressChangeInterval The interval between updating the withdrawal address. + function setWithdrawalAddressChangeInterval(uint256 newWithdrawalAddressChangeInterval) external onlyOwner { + _setWithdrawalAddressChangeInterval(newWithdrawalAddressChangeInterval); + } + + function _setMinStakeAmount(uint256 newMinStakeAmount) private { + require(newMinStakeAmount > 0, "IPTokenStaking: minStakeAmount cannot be 0"); + minStakeAmount = newMinStakeAmount - (newMinStakeAmount % STAKE_ROUNDING); + emit MinStakeAmountSet(minStakeAmount); + } + + /// @dev Sets the minimum amount required to withdraw. + /// @param newMinUnstakeAmount The minimum amount required to stake. + function _setMinUnstakeAmount(uint256 newMinUnstakeAmount) private { + require(newMinUnstakeAmount > 0, "IPTokenStaking: minUnstakeAmount cannot be 0"); + minUnstakeAmount = newMinUnstakeAmount - (newMinUnstakeAmount % STAKE_ROUNDING); + emit MinUnstakeAmountSet(minUnstakeAmount); + } + + /// @dev Sets the minimum amount required to redelegate. + /// @param newMinRedelegateAmount The minimum amount required to redelegate. + function _setMinRedelegateAmount(uint256 newMinRedelegateAmount) private { + require(newMinRedelegateAmount > 0, "IPTokenStaking: minRedelegateAmount cannot be 0"); + minRedelegateAmount = newMinRedelegateAmount - (newMinRedelegateAmount % STAKE_ROUNDING); + emit MinRedelegateAmountSet(minRedelegateAmount); + } + + /// @dev Sets the interval for updating the withdrawal address. + /// @param newWithdrawalAddressChangeInterval The interval between updating the withdrawal address. + function _setWithdrawalAddressChangeInterval(uint256 newWithdrawalAddressChangeInterval) private { + require( + newWithdrawalAddressChangeInterval > 0, + "IPTokenStaking: newWithdrawalAddressChangeInterval cannot be 0" + ); + withdrawalAddressChangeInterval = newWithdrawalAddressChangeInterval; + emit WithdrawalAddressChangeIntervalSet(newWithdrawalAddressChangeInterval); + } + + /// @notice Returns the rounded stake amount and the remainder. + /// @param rawAmount The raw stake amount. + /// @return amount The rounded stake amount. + /// @return remainder The remainder of the stake amount. + function roundedStakeAmount(uint256 rawAmount) public view returns (uint256 amount, uint256 remainder) { + remainder = rawAmount % STAKE_ROUNDING; + amount = rawAmount - remainder; + } + + /// @notice Converts the given public key to an EVM address. + /// @dev Assume all calls to this function passes in the uncompressed public key. + /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key, with prefix 04. + /// @return address The EVM address derived from the public key. + function _uncmpPubkeyToAddress(bytes calldata uncmpPubkey) internal pure returns (address) { + return address(uint160(uint256(keccak256(uncmpPubkey[1:])))); + } + + /*////////////////////////////////////////////////////////////////////////// + // Operator functions // + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Returns the operators for the delegator. + /// @param pubkey 33 bytes compressed secp256k1 public key. + function getOperators(bytes calldata pubkey) external view returns (address[] memory) { + return delegatorOperators[pubkey].values(); + } + + /// @notice Adds an operator for the delegator. + /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key. + /// @param operator The operator address to add. + function addOperator( + bytes calldata uncmpPubkey, + address operator + ) external verifyUncmpPubkeyWithExpectedAddress(uncmpPubkey, msg.sender) { + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(uncmpPubkey); + require(delegatorOperators[delegatorCmpPubkey].add(operator), "IPTokenStaking: Operator already exists"); + } + + /// @notice Removes an operator for the delegator. + /// @param uncmpPubkey 65 bytes uncompressed secp256k1 public key. + /// @param operator The operator address to remove. + function removeOperator( + bytes calldata uncmpPubkey, + address operator + ) external verifyUncmpPubkeyWithExpectedAddress(uncmpPubkey, msg.sender) { + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(uncmpPubkey); + require(delegatorOperators[delegatorCmpPubkey].remove(operator), "IPTokenStaking: Operator not found"); + } + + /// @dev Verifies that the caller is an operator for the delegator. This check will revert if the caller is the + /// delegator, which is intentional as this function should only be called in `onBehalf` functions. + function _verifyCallerIsOperator(bytes memory delegatorCmpPubkey, address caller) internal view { + require(delegatorOperators[delegatorCmpPubkey].contains(caller), "IPTokenStaking: Caller is not an operator"); + } + + /*////////////////////////////////////////////////////////////////////////// + // Staking Configuration functions // + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Set/Update the withdrawal address that receives the stake and reward withdrawals. + /// @dev To prevent spam, only delegators with stake can call this function with cool-down time. + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param newWithdrawalAddress EVM address to receive the stake and reward withdrawals. + function setWithdrawalAddress( + bytes calldata delegatorUncmpPubkey, + address newWithdrawalAddress + ) external verifyUncmpPubkeyWithExpectedAddress(delegatorUncmpPubkey, msg.sender) { + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(delegatorUncmpPubkey); + require(delegatorTotalStakes[delegatorCmpPubkey] > 0, "IPTokenStaking: Delegator must have stake"); + + require( + withdrawalAddressChange[delegatorCmpPubkey] + withdrawalAddressChangeInterval < block.timestamp, + "IPTokenStaking: Withdrawal address change cool-down" + ); + withdrawalAddressChange[delegatorCmpPubkey] = block.timestamp; + + emit SetWithdrawalAddress({ + depositorPubkey: delegatorCmpPubkey, + executionAddress: bytes32(uint256(uint160(newWithdrawalAddress))) // left-padded bytes32 of the address + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Token Staking functions // + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Entry point for creating a new validator with self delegation. + /// @dev The caller must provide the uncompressed public key that matches the expected EVM address. + /// @param validatorUncmpPubkey 65 bytes uncompressed secp256k1 public key. + /// @param moniker The moniker of the validator. + /// @param commissionRate The commission rate of the validator. + /// @param maxCommissionRate The maximum commission rate of the validator. + /// @param maxCommissionChangeRate The maximum commission change rate of the validator. + function createValidator( + bytes calldata validatorUncmpPubkey, + string calldata moniker, + uint32 commissionRate, + uint32 maxCommissionRate, + uint32 maxCommissionChangeRate + ) external payable verifyUncmpPubkeyWithExpectedAddress(validatorUncmpPubkey, msg.sender) nonReentrant { + bytes memory validatorCmpPubkey = Secp256k1.compressPublicKey(validatorUncmpPubkey); + _createValidator(validatorCmpPubkey, moniker, commissionRate, maxCommissionRate, maxCommissionChangeRate); + } + + /// @notice Entry point for creating a new validator with self delegation on behalf of the validator. + /// @dev There's no minimum amount required to stake when creating a new validator. + /// @param validatorPubkey 33 bytes compressed secp256k1 public key. + function createValidatorOnBehalf( + bytes calldata validatorPubkey + ) external payable verifyPubkey(validatorPubkey) nonReentrant { + _createValidator( + validatorPubkey, + "validator", + DEFAULT_COMMISSION_RATE, + DEFAULT_MAX_COMMISSION_RATE, + DEFAULT_MAX_COMMISSION_CHANGE_RATE + ); + } + + /// @dev Validator is the delegator when creating a new validator (self-delegation). + /// @param validatorPubkey 33 bytes compressed secp256k1 public key. + /// @param moniker The moniker of the validator. + /// @param commissionRate The commission rate of the validator. + /// @param maxCommissionRate The maximum commission rate of the validator. + /// @param maxCommissionChangeRate The maximum commission change rate of the validator. + function _createValidator( + bytes memory validatorPubkey, + string memory moniker, + uint32 commissionRate, + uint32 maxCommissionRate, + uint32 maxCommissionChangeRate + ) internal { + // NOTE: We intentionally disable this check to circumvent the unsycned validator state between CL and EL. + // In particular, when a validator gets its stake withdrawn to below the minimum power-reduction, it will get + // removed from the validator set. If a user attempts to stake to that validator after it's removed from the + // set, the EL contract will allow for staking but CL will fail because the validator info doesn't exist, thus + // the user would lose the fund. Hence, by removing this check, user can and must first call createValidator + // again for a removed validator before staking to it. + // require(!validatorMetadata[validatorPubkey].exists, "IPTokenStaking: Validator already exists"); + + (uint256 stakeAmount, uint256 remainder) = roundedStakeAmount(msg.value); + require(stakeAmount > 0, "IPTokenStaking: Stake amount too low"); + + // Since users can call createValidator multiple times, we need to reuse the existing metadata if it exists. + // The only data that monotonically increases is the totalStake. All others are selected based on whether the + // validator data already exists or not. + ValidatorMetadata storage vm = validatorMetadata[validatorPubkey]; + bool exists = vm.exists; // get before updating + vm.exists = true; + vm.moniker = exists ? vm.moniker : moniker; + vm.totalStake = vm.totalStake + stakeAmount; + vm.commissionRate = exists ? vm.commissionRate : commissionRate; + vm.maxCommissionRate = exists ? vm.maxCommissionRate : maxCommissionRate; + vm.maxCommissionChangeRate = exists ? vm.maxCommissionChangeRate : maxCommissionChangeRate; + + delegatorTotalStakes[validatorPubkey] += stakeAmount; + delegatorValidatorStakes[validatorPubkey][validatorPubkey] += stakeAmount; + + _refundRemainder(remainder); + + emit CreateValidator({ + validatorPubkey: validatorPubkey, + moniker: vm.moniker, + stakeAmount: stakeAmount, + commissionRate: vm.commissionRate, + maxCommissionRate: vm.maxCommissionRate, + maxCommissionChangeRate: vm.maxCommissionChangeRate + }); + } + + /// @notice Entry point for staking IP token to stake to the given validator. The consensus chain is notified of + /// the deposit and manages the stake accounting and validator onboarding. Payer must be the delegator. + /// @dev When staking, consider it as BURNING. Unstaking (withdrawal) will trigger native minting. + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + function stake( + bytes calldata delegatorUncmpPubkey, + bytes calldata validatorPubkey + ) + external + payable + verifyUncmpPubkeyWithExpectedAddress(delegatorUncmpPubkey, msg.sender) + verifyExistingValidator(validatorPubkey) + nonReentrant + { + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(delegatorUncmpPubkey); + _stake(delegatorCmpPubkey, validatorPubkey); + } + + /// @notice Entry point for staking IP token to stake to the given validator. The consensus chain is notified of + /// the stake and manages the stake accounting and validator onboarding. Payer can stake on behalf of another user, + /// who will be the beneficiary of the stake. + /// @dev When staking, consider it as BURNING. Unstaking (withdrawal) will trigger native minting. + /// @param delegatorPubkey Delegator's 33 bytes compressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + function stakeOnBehalf( + bytes calldata delegatorPubkey, + bytes calldata validatorPubkey + ) external payable verifyPubkey(delegatorPubkey) verifyExistingValidator(validatorPubkey) nonReentrant { + _stake(delegatorPubkey, validatorPubkey); + } + + /// @dev Creates a validator (x/staking.MsgCreateValidator) if it does not exist. Then delegates the stake to the + /// validator (x/staking.MsgDelegate). + /// @param delegatorPubkey Delegator's 33 byte compressed secp256k1 public key (no 0x04 prefix). + /// @param validatorPubkey 33 byte compressed secp256k1 public key (no 0x04 prefix). + function _stake(bytes memory delegatorPubkey, bytes calldata validatorPubkey) internal { + (uint256 stakeAmount, uint256 remainder) = roundedStakeAmount(msg.value); + require(stakeAmount >= minStakeAmount, "IPTokenStaking: Stake amount too low"); + + unchecked { + validatorMetadata[validatorPubkey].totalStake += stakeAmount; + delegatorTotalStakes[delegatorPubkey] += stakeAmount; + delegatorValidatorStakes[delegatorPubkey][validatorPubkey] += stakeAmount; + } + + _refundRemainder(remainder); + + emit Deposit(delegatorPubkey, validatorPubkey, stakeAmount); + } + + // TODO: Redelegation also requires unbonding period to be executed. Should we separate storage for this for el? + /// @notice Entry point for redelegating the staked token. + /// @dev Redelegateion redelegates staked token from src validator to dst validator (x/staking.MsgBeginRedelegate) + /// @param p See RedelegateParams + function redelegate( + RedelegateParams calldata p + ) + external + verifyUncmpPubkeyWithExpectedAddress(p.delegatorUncmpPubkey, msg.sender) + verifyExistingValidator(p.validatorSrcPubkey) + verifyExistingValidator(p.validatorDstPubkey) + { + (uint256 stakeAmount, ) = roundedStakeAmount(p.amount); + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(p.delegatorUncmpPubkey); + + _redelegate(delegatorCmpPubkey, p.validatorSrcPubkey, p.validatorDstPubkey, stakeAmount); + } + + /// @notice Entry point for redelegating the staked token on behalf of the delegator. + /// @dev Redelegateion redelegates staked token from src validator to dst validator (x/staking.MsgBeginRedelegate) + /// @param p See RedelegateParams + function redelegateOnBehalf( + RedelegateParams calldata p + ) + external + verifyUncmpPubkey(p.delegatorUncmpPubkey) + verifyExistingValidator(p.validatorSrcPubkey) + verifyExistingValidator(p.validatorDstPubkey) + { + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(p.delegatorUncmpPubkey); + + (uint256 stakeAmount, ) = roundedStakeAmount(p.amount); + + _verifyCallerIsOperator(delegatorCmpPubkey, msg.sender); + _redelegate(delegatorCmpPubkey, p.validatorSrcPubkey, p.validatorDstPubkey, stakeAmount); + } + + /// @dev Redelegates the given amount from the source validator to the destination validator. + /// @param delegatorPubkey Delegator's 33 bytes compressed secp256k1 public key. + /// @param validatorSrcPubkey Source validator's 33 bytes compressed secp256k1 public key. + /// @param validatorDstPubkey Destination validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token amount to redelegate. + function _redelegate( + bytes memory delegatorPubkey, + bytes calldata validatorSrcPubkey, + bytes calldata validatorDstPubkey, + uint256 amount + ) internal { + require( + delegatorValidatorStakes[delegatorPubkey][validatorSrcPubkey] >= amount, + "IPTokenStaking: Insufficient staked amount" + ); + + validatorMetadata[validatorSrcPubkey].totalStake -= amount; + validatorMetadata[validatorDstPubkey].totalStake += amount; + + delegatorValidatorStakes[delegatorPubkey][validatorSrcPubkey] -= amount; + delegatorValidatorStakes[delegatorPubkey][validatorDstPubkey] += amount; + + emit Redelegate(delegatorPubkey, validatorSrcPubkey, validatorDstPubkey, amount); + } + + /// @notice Entry point for unstaking the previously staked token. + /// @dev Unstake (withdrawal) will trigger native minting, so token in this contract is considered as burned. + /// @param delegatorUncmpPubkey Delegator's 65 bytes uncompressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token amount to unstake. + function unstake( + bytes calldata delegatorUncmpPubkey, + bytes calldata validatorPubkey, + uint256 amount + ) + external + verifyUncmpPubkeyWithExpectedAddress(delegatorUncmpPubkey, msg.sender) + verifyExistingValidator(validatorPubkey) + { + bytes memory delegatorCmpPubkey = Secp256k1.compressPublicKey(delegatorUncmpPubkey); + _unstake(delegatorCmpPubkey, validatorPubkey, amount); + } + + /// @notice Entry point for unstaking the previously staked token on behalf of the delegator. + /// @dev Must be an approved operator for the delegator. + /// @param delegatorCmpPubkey Delegator's 33 bytes compressed secp256k1 public key. + /// @param validatorPubkey Validator's 33 bytes compressed secp256k1 public key. + /// @param amount Token amount to unstake. + function unstakeOnBehalf( + bytes calldata delegatorCmpPubkey, + bytes calldata validatorPubkey, + uint256 amount + ) external verifyPubkey(delegatorCmpPubkey) verifyExistingValidator(validatorPubkey) { + _verifyCallerIsOperator(delegatorCmpPubkey, msg.sender); + _unstake(delegatorCmpPubkey, validatorPubkey, amount); + } + + /// @dev Unstakes the given amount from the validator for the delegator, where the amount is deposited to the + /// execution address. + function _unstake(bytes memory delegatorPubkey, bytes calldata validatorPubkey, uint256 amount) internal { + require( + delegatorValidatorStakes[delegatorPubkey][validatorPubkey] >= amount, + "IPTokenStaking: Insufficient staked amount" + ); + + validatorMetadata[validatorPubkey].totalStake -= amount; + delegatorTotalStakes[delegatorPubkey] -= amount; + delegatorValidatorStakes[delegatorPubkey][validatorPubkey] -= amount; + + // If validator gets slashed and the total staked in CL is less than the total staked in EL, then the validator + // might not exist in CL while still existing in EL. + if (validatorMetadata[validatorPubkey].totalStake == 0) { + delete validatorMetadata[validatorPubkey]; + } + + emit Withdraw(delegatorPubkey, validatorPubkey, amount); + } + + /// @dev Refunds the remainder of the stake amount to the msg sender. + /// @param remainder The remainder of the stake amount. + function _refundRemainder(uint256 remainder) internal { + (bool success, ) = msg.sender.call{ value: remainder }(""); + require(success, "IPTokenStaking: Failed to refund remainder"); + } +} diff --git a/contracts/src/protocol/UpgradeEntrypoint.sol b/contracts/src/protocol/UpgradeEntrypoint.sol new file mode 100644 index 00000000..1e72a64e --- /dev/null +++ b/contracts/src/protocol/UpgradeEntrypoint.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; + +import { IUpgradeEntrypoint } from "../interfaces/IUpgradeEntrypoint.sol"; + +/** + * @title UpgradeEntrypoint + * @notice Entrypoint contract for submitting x/upgrade module actions. + */ +contract UpgradeEntrypoint is IUpgradeEntrypoint, Ownable2Step { + constructor(address newOwner) Ownable(newOwner) {} + + /// @notice Submits an upgrade plan. + /// @param name Sets the name for the upgrade. This name will be used by the upgraded version of the software to + /// apply any special "on-upgrade" commands during the first BeginBlock method after the upgrade is applied. It is + /// also used to detect whether a software version can handle a given upgrade. If no upgrade handler with this name + /// has been set in the software, it will be assumed that the software is out-of-date when the upgrade Time or + /// Height is reached and the software will exit. + /// @param height The height at which the upgrade must be performed. + /// @param info Any application specific upgrade info to be included on-chain such as a git commit that validators + /// could automatically upgrade to. + function planUpgrade(string calldata name, int64 height, string calldata info) external onlyOwner { + emit SoftwareUpgrade({ name: name, height: height, info: info }); + } +} diff --git a/contracts/test/deploy/Create3.t.sol b/contracts/test/deploy/Create3.t.sol new file mode 100644 index 00000000..0cff576f --- /dev/null +++ b/contracts/test/deploy/Create3.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ +/// NOTE: pragma allowlist-secret must be inline (same line as the pubkey hex string) to avoid false positive +/// flag "Hex High Entropy String" in CI run detect-secrets + +import { Test } from "forge-std/Test.sol"; + +import { Create3 } from "../../src/deploy/Create3.sol"; + +contract Create3Test is Test { + Create3 private create3; + + function setUp() public { + create3 = new Create3(); + } + + function testCreate3_deploy() public { + // deploy and getDeployed should return same address when deployed by the same deployer and with same salt. + bytes32 salt = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + bytes memory creationCode = type(Create3).creationCode; + address deployed = create3.deploy(salt, creationCode); + address expected = create3.getDeployed(address(this), salt); + assertEq(deployed, expected); + + // Network shall generate the same address for the same deployer and salt. + vm.expectRevert("DEPLOYMENT_FAILED"); + deployed = create3.deploy(salt, creationCode); + + // Network shall generate different addresses for different deployers. + address otherAddr = address(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab); + vm.prank(otherAddr); + deployed = create3.deploy(salt, creationCode); + expected = create3.getDeployed(otherAddr, salt); + assertEq(deployed, expected); + + // Network shall generate different addresses for different salts. + bytes32 otherSalt = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fedcba; + deployed = create3.deploy(otherSalt, creationCode); + expected = create3.getDeployed(address(this), otherSalt); + assertEq(deployed, expected); + } +} diff --git a/contracts/test/libraries/Secp256k1.t.sol b/contracts/test/libraries/Secp256k1.t.sol new file mode 100644 index 00000000..16ac05dd --- /dev/null +++ b/contracts/test/libraries/Secp256k1.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ +/* solhint-disable function-state-mutability */ +/// NOTE: pragma allowlist-secret must be inline (same line as the pubkey hex string) to avoid false positive +/// flag "Hex High Entropy String" in CI run detect-secrets + +import { Test } from "forge-std/Test.sol"; + +import { Secp256k1 } from "../../src/libraries/Secp256k1.sol"; + +contract IPTokenStakingTest is Test { + function setUp() public {} + + function testCompressPublicKey_validKey() public pure { + // prefix: 04 + bytes + memory uncmpPubkey = hex"04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe47b"; // pragma: allowlist-secret + // prefix: 03 + bytes memory cmpPubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced1"; // pragma: allowlist-secret + + bytes + memory anotherUncmpPubkey = hex"04e38d111122223333ce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe47b"; // pragma: allowlist-secret + + vm.assertEq(Secp256k1.compressPublicKey(uncmpPubkey), cmpPubkey); + vm.assertNotEq(Secp256k1.compressPublicKey(uncmpPubkey), Secp256k1.compressPublicKey(anotherUncmpPubkey)); + } + + function testCompressPublicKey_deriveAddress() public pure { + // prefix 04 sliced from `04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe47b` // pragma: allowlist-secret + bytes + memory uncmpPubkeySliced = hex"e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe47b"; // pragma: allowlist-secret + + address expectedAddr = 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab; + address derivedAddr = address(uint160(uint256(keccak256(uncmpPubkeySliced)))); + + vm.assertEq(uncmpPubkeySliced.length, 64); + vm.assertEq(derivedAddr, expectedAddr); + } +} diff --git a/contracts/test/script/DeployIPTokenStaking.t.sol b/contracts/test/script/DeployIPTokenStaking.t.sol new file mode 100644 index 00000000..3375c799 --- /dev/null +++ b/contracts/test/script/DeployIPTokenStaking.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ +/// NOTE: pragma allowlist-secret must be inline (same line as the pubkey hex string) to avoid false positive +/// flag "Hex High Entropy String" in CI run detect-secrets + +import { Test } from "forge-std/Test.sol"; + +import { DeployIPTokenStaking } from "../../script/DeployIPTokenStaking.sol"; + +contract DeployIPTokenStakingTest is Test { + DeployIPTokenStaking private deployIPTokenStaking; + + function setUp() public { + deployIPTokenStaking = new DeployIPTokenStaking(); + } + + function testDeployIPTokenStaking_run() public { + // Network shall not deploy the IPTokenStaking contract if not mainnet. + vm.expectRevert("only mainnet deployment"); + deployIPTokenStaking.run(); + + // Network shall not deploy the IPTokenStaking contract if IPTOKENSTAKING_DEPLOYER_KEY not set. + vm.chainId(1); + // solhint-disable + vm.expectRevert("vm.envUint: environment variable \"IPTOKENSTAKING_DEPLOYER_KEY\" not found"); + deployIPTokenStaking.run(); + + // Network shall deploy the IPTokenStaking contract. + vm.setEnv("IPTOKENSTAKING_DEPLOYER_KEY", "0x123456789abcdef"); + deployIPTokenStaking.run(); + } +} diff --git a/contracts/test/stake/IPTokenSlashing.t.sol b/contracts/test/stake/IPTokenSlashing.t.sol new file mode 100644 index 00000000..903df5a7 --- /dev/null +++ b/contracts/test/stake/IPTokenSlashing.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ +/// NOTE: pragma allowlist-secret must be inline (same line as the pubkey hex string) to avoid false positive +/// flag "Hex High Entropy String" in CI run detect-secrets + +import { Test } from "forge-std/Test.sol"; + +import { IPTokenSlashing, IIPTokenSlashing } from "../../src/protocol/IPTokenSlashing.sol"; +import { IPTokenStaking } from "../../src/protocol/IPTokenStaking.sol"; + +contract IPTokenSlashingTest is Test { + IPTokenSlashing private ipTokenSlashing; + IPTokenStaking private ipTokenStaking; + + bytes private delegatorUncmpPubkey = + hex"04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe47b"; // pragma: allowlist-secret + // Address matching delegatorUncmpPubkey + bytes private delegatorCmpPubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced1"; // pragma: allowlist-secret + // Address matching delegatorCmpPubkey + address private delegatorAddr = address(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab); + + event Received(address, uint256); + + // For some tests, we need to receive the native token to this contract + receive() external payable { + emit Received(msg.sender, msg.value); + } + + function setUp() public { + address protocolAccessManagerAddr = address(this); + + ipTokenStaking = new IPTokenStaking( + protocolAccessManagerAddr, + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + + ipTokenSlashing = new IPTokenSlashing( + protocolAccessManagerAddr, + address(ipTokenStaking), + 1 ether // unjailFee + ); + } + + function testIPTokenSlashing_Parameters() public view { + assertEq(ipTokenSlashing.unjailFee(), 1 ether); + } + + function createDefaultValidator() private { + vm.deal(delegatorAddr, 1 ether); + vm.prank(delegatorAddr); + ipTokenStaking.createValidator{ value: 1 ether }({ + validatorUncmpPubkey: delegatorUncmpPubkey, + moniker: "delegator's validator", + commissionRate: 1000, + maxCommissionRate: 5000, + maxCommissionChangeRate: 100 + }); + } + + function testIPTokenSlashing_Unjail() public { + // Network shall not allow anyone to unjail an non-existing validator. + uint256 feeAmount = 1 ether; + vm.deal(delegatorAddr, feeAmount); + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenSlashing: Validator does not exist"); + ipTokenSlashing.unjail{ value: feeAmount }(delegatorUncmpPubkey); + + // Network shall not allow anyone to unjail a validator if it is not the validator itself. + createDefaultValidator(); + address otherAddress = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Invalid pubkey derived address"); + ipTokenSlashing.unjail(delegatorUncmpPubkey); + + // Network shall not allow anyone to unjail a validator if the fee is not paid. + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenSlashing: Insufficient fee"); + ipTokenSlashing.unjail(delegatorUncmpPubkey); + + // Network shall not allow anyone to unjail a validator if the fee is not sufficient. + feeAmount = 0.9 ether; + vm.deal(delegatorAddr, feeAmount); + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenSlashing: Insufficient fee"); + ipTokenSlashing.unjail{ value: feeAmount }(delegatorUncmpPubkey); + + // Network shall allow anyone to unjail a validator if the fee is paid. + feeAmount = 1 ether; + vm.deal(delegatorAddr, feeAmount); + vm.prank(delegatorAddr); + vm.expectEmit(address(ipTokenSlashing)); + emit IIPTokenSlashing.Unjail(delegatorAddr, delegatorCmpPubkey); + ipTokenSlashing.unjail{ value: feeAmount }(delegatorUncmpPubkey); + + // Network shall not allow anyone to unjail a validator if the fee is over. + feeAmount = 1.1 ether; + vm.deal(delegatorAddr, feeAmount); + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenSlashing: Insufficient fee"); + ipTokenSlashing.unjail{ value: feeAmount }(delegatorUncmpPubkey); + } + + function testIPTokenSlashing_UnjailOnBehalf() public { + address otherAddress = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + + // Network shall not allow anyone to unjail an non-existing validator. + uint256 feeAmount = 1 ether; + vm.deal(otherAddress, feeAmount); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Validator does not exist"); + ipTokenSlashing.unjailOnBehalf{ value: feeAmount }(delegatorCmpPubkey); + + // Network shall not allow anyone to unjail with compressed pubkey of incorrect length. + bytes memory delegatorCmpPubkeyShortLen = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ce"; // pragma: allowlist secret + feeAmount = 1 ether; + vm.deal(otherAddress, feeAmount); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Invalid pubkey length"); + ipTokenSlashing.unjailOnBehalf{ value: feeAmount }(delegatorCmpPubkeyShortLen); + + // Network shall not allow anyone to unjail with compressed pubkey of incorrect prefix. + bytes + memory delegatorCmpPubkeyWrongPrefix = hex"05e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced1"; // pragma: allowlist secret + feeAmount = 1 ether; + vm.deal(otherAddress, feeAmount); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Invalid pubkey prefix"); + ipTokenSlashing.unjailOnBehalf{ value: feeAmount }(delegatorCmpPubkeyWrongPrefix); + + // Network shall not allow anyone to unjail a validator if the fee is not paid. + createDefaultValidator(); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Insufficient fee"); + ipTokenSlashing.unjailOnBehalf(delegatorCmpPubkey); + + // Network shall not allow anyone to unjail a validator if the fee is not sufficient. + feeAmount = 0.9 ether; + vm.deal(otherAddress, feeAmount); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Insufficient fee"); + ipTokenSlashing.unjailOnBehalf{ value: feeAmount }(delegatorCmpPubkey); + + // Network shall allow anyone to unjail a validator on behalf if the fee is paid. + feeAmount = 1 ether; + vm.deal(otherAddress, feeAmount); + vm.prank(otherAddress); + vm.expectEmit(address(ipTokenSlashing)); + emit IIPTokenSlashing.Unjail(otherAddress, delegatorCmpPubkey); + ipTokenSlashing.unjailOnBehalf{ value: feeAmount }(delegatorCmpPubkey); + + // Network shall not allow anyone to unjail a validator if the fee is over. + feeAmount = 1.1 ether; + vm.deal(otherAddress, feeAmount); + vm.prank(otherAddress); + vm.expectRevert("IPTokenSlashing: Insufficient fee"); + ipTokenSlashing.unjailOnBehalf{ value: feeAmount }(delegatorCmpPubkey); + } + + function testIPTokenSlashing_SetUnjailFee() public { + // Network shall allow the owner to set the unjail fee. + uint256 newUnjailFee = 2 ether; + vm.expectEmit(address(ipTokenSlashing)); + emit IIPTokenSlashing.UnjailFeeSet(newUnjailFee); + vm.prank(address(this)); + ipTokenSlashing.setUnjailFee(newUnjailFee); + assertEq(ipTokenSlashing.unjailFee(), newUnjailFee); + + // Network shall not allow non-owner to set the unjail fee. + vm.prank(address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA)); + vm.expectRevert(); + ipTokenSlashing.setUnjailFee(1 ether); + assertEq(ipTokenSlashing.unjailFee(), newUnjailFee); + } +} diff --git a/contracts/test/stake/IPTokenStaking.t.sol b/contracts/test/stake/IPTokenStaking.t.sol new file mode 100644 index 00000000..15fb86c0 --- /dev/null +++ b/contracts/test/stake/IPTokenStaking.t.sol @@ -0,0 +1,931 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ +/// NOTE: pragma allowlist-secret must be inline (same line as the pubkey hex string) to avoid false positive +/// flag "Hex High Entropy String" in CI run detect-secrets + +import { Test } from "forge-std/Test.sol"; + +import { IPTokenStaking, IIPTokenStaking } from "../../src/protocol/IPTokenStaking.sol"; +import { Secp256k1 } from "../../src/libraries/Secp256k1.sol"; + +contract IPTokenStakingTest is Test { + IPTokenStaking private ipTokenStaking; + + bytes private delegatorUncmpPubkey = + hex"04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe47b"; // pragma: allowlist-secret + // Address matching delegatorUncmpPubkey + bytes private delegatorCmpPubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced1"; // pragma: allowlist-secret + // Address matching delegatorCmpPubkey + address private delegatorAddr = address(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab); + + event Received(address, uint256); + + // For some tests, we need to receive the native token to this contract + receive() external payable { + emit Received(msg.sender, msg.value); + } + + function setUp() public { + address protocolAccessManagerAddr = address(this); + + ipTokenStaking = new IPTokenStaking( + protocolAccessManagerAddr, + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + + vm.assertEq(delegatorCmpPubkey, Secp256k1.compressPublicKey(delegatorUncmpPubkey)); + } + + function testIPTokenStaking_Constructor() public { + vm.expectRevert("IPTokenStaking: minStakeAmount cannot be 0"); + new IPTokenStaking( + address(this), + 0, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: minUnstakeAmount cannot be 0"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 0, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: minRedelegateAmount cannot be 0"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 0, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert(); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 0, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: newWithdrawalAddressChangeInterval cannot be 0"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 0, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: Invalid default commission rate"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 10_001, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: Invalid default max commission rate"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 10_001, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: Invalid default max commission rate"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 1, // defaultMaxCommissionRate, 50% + 500 // defaultMaxCommissionChangeRate, 5% + ); + vm.expectRevert("IPTokenStaking: Invalid default max commission change rate"); + new IPTokenStaking( + address(this), + 1 ether, // minStakeAmount + 1 ether, // minUnstakeAmount + 1 ether, // minRedelegateAmount + 1 gwei, // stakingRounding + 7 days, // withdrawalAddressChangeInterval + 1000, // defaultCommissionRate, 10% + 5000, // defaultMaxCommissionRate, 50% + 10_001 // defaultMaxCommissionChangeRate, 5% + ); + } + + function testIPTokenStaking_Parameters() public view { + assertEq(ipTokenStaking.minStakeAmount(), 1 ether); + assertEq(ipTokenStaking.minUnstakeAmount(), 1 ether); + assertEq(ipTokenStaking.minRedelegateAmount(), 1 ether); + assertEq(ipTokenStaking.STAKE_ROUNDING(), 1 gwei); + assertEq(ipTokenStaking.withdrawalAddressChangeInterval(), 7 days); + assertEq(ipTokenStaking.DEFAULT_COMMISSION_RATE(), 1000); + assertEq(ipTokenStaking.DEFAULT_MAX_COMMISSION_RATE(), 5000); + assertEq(ipTokenStaking.DEFAULT_MAX_COMMISSION_CHANGE_RATE(), 500); + } + + function testIPTokenStaking_CreateValidator() public { + // Network shall not allow anyone to create a new validator if the validator accountā€™s balance is 0. + // Note that this restriction doesnā€™t apply to validator creation on behalf. + uint256 stakeAmount = 0 ether; + bytes memory validatorUncmpPubkey = delegatorUncmpPubkey; + bytes memory validatorCmpPubkey = delegatorCmpPubkey; + vm.deal(delegatorAddr, stakeAmount); + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Stake amount too low"); + ipTokenStaking.createValidator{ value: stakeAmount }({ + validatorUncmpPubkey: validatorUncmpPubkey, + moniker: "delegator's validator", + commissionRate: 1000, + maxCommissionRate: 5000, + maxCommissionChangeRate: 100 + }); + // Check that no stakes are put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(validatorCmpPubkey), 0 ether); + assertEq(ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorCmpPubkey), 0 ether); + + // Network shall not allow anyone to create a new validator on behalf if the sender accountā€™s balance is 0. + bytes memory validator1Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9111111"; // pragma: allowlist-secret + stakeAmount = 0 ether; + vm.deal(delegatorAddr, stakeAmount); + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Stake amount too low"); + ipTokenStaking.createValidatorOnBehalf{ value: stakeAmount }({ validatorPubkey: validator1Pubkey }); + // Check that no stakes are put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(validator1Pubkey), 0 ether); + assertEq(ipTokenStaking.delegatorValidatorStakes(validator1Pubkey, validator1Pubkey), 0 ether); + + // Network shall allow anyone to create a new validator by staking validatorā€™s own tokens (self-delegation) + stakeAmount = 1 ether; + vm.deal(delegatorAddr, stakeAmount); + vm.prank(delegatorAddr); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.CreateValidator(delegatorCmpPubkey, "delegator's validator", stakeAmount, 1000, 5000, 100); + ipTokenStaking.createValidator{ value: stakeAmount }({ + validatorUncmpPubkey: delegatorUncmpPubkey, + moniker: "delegator's validator", + commissionRate: 1000, + maxCommissionRate: 5000, + maxCommissionChangeRate: 100 + }); + // Check that stakes are correctly put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), 1 ether); + assertEq(ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, delegatorCmpPubkey), 1 ether); + + // NOTE: We have removed the validator existence check in createValidator, thus this test is not valid anymore. + // // Adding a validator twice should not be allowed + // vm.deal(delegatorAddr, stakeAmount); + // vm.prank(delegatorAddr); + // vm.expectRevert("IPTokenStaking: Validator already exists"); + // ipTokenStaking.createValidator{ value: stakeAmount }({ + // validatorUncmpPubkey: delegatorUncmpPubkey, + // moniker: "delegator's validator", + // commissionRate: 1000, + // maxCommissionRate: 5000, + // maxCommissionChangeRate: 100 + // }); + + // Network shall allow anyone to create a new validator on behalf of a validator. + // Note that the operation stakes senderā€™s tokens to the validator, and the delegator will still be the validator itself. + bytes memory validator2Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9222222"; // pragma: allowlist-secret + stakeAmount = 1000 ether; + vm.deal(delegatorAddr, stakeAmount); + vm.prank(delegatorAddr); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.CreateValidator(validator2Pubkey, "validator", stakeAmount, 1000, 5000, 500); + ipTokenStaking.createValidatorOnBehalf{ value: stakeAmount }({ validatorPubkey: validator2Pubkey }); + // Check that stakes are correctly put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(validator2Pubkey), 1000 ether); + // Check that the delegator is the validator itself + assertEq(ipTokenStaking.delegatorValidatorStakes(validator2Pubkey, validator2Pubkey), 1000 ether); + assertEq(ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validator2Pubkey), 0 ether); + + // NOTE: We have removed the validator existence check in createValidator, thus this test is not valid anymore. + // // Network shall not allow anyone to create a new validator with existing validatorsā€™ public keys. + // bytes memory validator3Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9222222"; // pragma: allowlist-secret + // stakeAmount = 1 ether; + // vm.deal(delegatorAddr, stakeAmount); + // vm.prank(delegatorAddr); + // vm.expectRevert("IPTokenStaking: Validator already exists"); + // ipTokenStaking.createValidatorOnBehalf{ value: stakeAmount }({ validatorPubkey: validator3Pubkey }); + // // Check that stakes are changing for the existing validator + // assertEq(ipTokenStaking.delegatorTotalStakes(validator3Pubkey), 1000 ether); + // assertEq(ipTokenStaking.delegatorValidatorStakes(validator3Pubkey, validator3Pubkey), 1000 ether); + + // Network shall not allow anyone to create a new validator if the provided public key doesnā€™t match senderā€™s address. + bytes + memory delegatorUncmpPubkeyChanged = hex"04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1efe222"; // pragma: allowlist-secret + stakeAmount = 1 ether; + vm.deal(delegatorAddr, stakeAmount); + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Invalid pubkey derived address"); + ipTokenStaking.createValidator{ value: stakeAmount }({ + validatorUncmpPubkey: delegatorUncmpPubkeyChanged, + moniker: "delegator's validator", + commissionRate: 1000, + maxCommissionRate: 5000, + maxCommissionChangeRate: 100 + }); + // Check that no stakes are put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorUncmpPubkeyChanged), 0 ether); + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorUncmpPubkeyChanged, delegatorUncmpPubkeyChanged), + 0 ether + ); + } + + function testIPTokenStaking_CreateValidator_MultipleTimes() public { + // When creating an existing validator (second time), it should emit CreateValidator event with existing values but updated stake amount. + uint256 stakeAmount = ipTokenStaking.minStakeAmount(); + bytes memory validatorUncmpPubkey = delegatorUncmpPubkey; + bytes memory validatorCmpPubkey = delegatorCmpPubkey; + vm.deal(delegatorAddr, stakeAmount * 2); + + uint256 beforeDelegatorTotalStakes = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + uint256 beforeDelegatorValidatorStakes = ipTokenStaking.delegatorValidatorStakes( + delegatorCmpPubkey, + validatorCmpPubkey + ); + + // Create initially + vm.prank(delegatorAddr); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.CreateValidator(validatorCmpPubkey, "delegator's validator", stakeAmount, 1000, 5000, 100); + ipTokenStaking.createValidator{ value: stakeAmount }({ + validatorUncmpPubkey: validatorUncmpPubkey, + moniker: "delegator's validator", + commissionRate: 1000, + maxCommissionRate: 5000, + maxCommissionChangeRate: 100 + }); + + // Check that more stakes are put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), beforeDelegatorTotalStakes + stakeAmount); + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorCmpPubkey), + beforeDelegatorValidatorStakes + stakeAmount + ); + + // Create again + vm.prank(delegatorAddr); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.CreateValidator(validatorCmpPubkey, "delegator's validator", stakeAmount, 1000, 5000, 100); + ipTokenStaking.createValidator{ value: stakeAmount }({ + validatorUncmpPubkey: validatorUncmpPubkey, + moniker: "bad name validator", + commissionRate: 100, + maxCommissionRate: 100, + maxCommissionChangeRate: 100 + }); + + // Check that more stakes are put on the validator + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), beforeDelegatorTotalStakes + stakeAmount * 2); + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorCmpPubkey), + beforeDelegatorValidatorStakes + stakeAmount * 2 + ); + } + + modifier withDefaultValidator() { + vm.deal(delegatorAddr, 1 ether); + vm.prank(delegatorAddr); + ipTokenStaking.createValidator{ value: 1 ether }({ + validatorUncmpPubkey: delegatorUncmpPubkey, + moniker: "delegator's validator", + commissionRate: 1000, + maxCommissionRate: 5000, + maxCommissionChangeRate: 100 + }); + _; + } + + function testIPTokenStaking_Stake() public withDefaultValidator { + // Network shall allow anyone to deposit stake ā‰„ minimum stake amount into an existing validator for a delegator pubkey. + bytes memory validatorPubkey = delegatorCmpPubkey; + uint256 stakeAmount = 1 ether; + vm.deal(delegatorAddr, stakeAmount); + + uint256 delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey); + uint256 delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + ( + bool isActive, + string memory moniker, + uint256 totalStake, + uint32 commissionRate, + uint32 maxCommissionRate, + uint32 maxCommissionChangeRate + ) = ipTokenStaking.validatorMetadata(validatorPubkey); + uint256 validatorTotalBefore = totalStake; + + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: stakeAmount }(delegatorUncmpPubkey, validatorPubkey); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey), + delegatorValidatorBefore + stakeAmount + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), delegatorTotalBefore + stakeAmount); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore + stakeAmount); + + // (TODO) Network shall refund money to the staker in EL if execution in EL succeeds but CL fails + + // Network shall allow anyone to stake on behalf of another delegator. + validatorPubkey = delegatorCmpPubkey; + bytes memory delegator1Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9111111"; // pragma: allowlist secret + stakeAmount = 1000 ether; + vm.deal(delegatorAddr, stakeAmount); + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegator1Pubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.prank(delegatorAddr); + ipTokenStaking.stakeOnBehalf{ value: stakeAmount }(delegator1Pubkey, validatorPubkey); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey), + delegatorValidatorBefore + stakeAmount + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegator1Pubkey), delegatorTotalBefore + stakeAmount); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore + stakeAmount); + + // (TODO) Network shall prevent depositing stake into a validator pubkey that has not been created (stake). + + // Network shall prevent depositing stake into a validator pubkey that has not been created (stakeOnBehalf). + validatorPubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9777777"; // pragma: allowlist secret + delegator1Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9111111"; // pragma: allowlist secret + stakeAmount = 1000 ether; + vm.deal(delegatorAddr, stakeAmount); + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegator1Pubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Validator does not exist"); + ipTokenStaking.stakeOnBehalf{ value: stakeAmount }(delegator1Pubkey, validatorPubkey); + + assertEq(ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey), delegatorValidatorBefore); + assertEq(ipTokenStaking.delegatorTotalStakes(delegator1Pubkey), delegatorTotalBefore); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore); + + // Network shall not allow anyone to deposit stake < minimum stake amount + validatorPubkey = delegatorCmpPubkey; + delegator1Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9111111"; // pragma: allowlist secret + stakeAmount = 100 gwei; + vm.deal(delegatorAddr, stakeAmount); + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegator1Pubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Stake amount too low"); + ipTokenStaking.stakeOnBehalf{ value: stakeAmount }(delegator1Pubkey, validatorPubkey); + + assertEq(ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey), delegatorValidatorBefore); + assertEq(ipTokenStaking.delegatorTotalStakes(delegator1Pubkey), delegatorTotalBefore); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore); + + // Network shall round the input stake amount by 1 gwei and send the remainder back to the sender. + validatorPubkey = delegatorCmpPubkey; + delegator1Pubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9111111"; // pragma: allowlist secret + stakeAmount = 1_000_000_000_000_000_001 wei; + vm.deal(delegatorAddr, stakeAmount); + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegator1Pubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.prank(delegatorAddr); + ipTokenStaking.stakeOnBehalf{ value: stakeAmount }(delegator1Pubkey, validatorPubkey); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegator1Pubkey, validatorPubkey), + delegatorValidatorBefore + stakeAmount - 1 wei + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegator1Pubkey), delegatorTotalBefore + stakeAmount - 1 wei); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore + stakeAmount - 1 wei); + // (TODO) Check that sender got 1 wei back. + } + + function testIPTokenStaking_Redelegate() public withDefaultValidator { + // Network shall allow the delegators to move their staked token from source validator to destination validator. + bytes memory validatorSrcPubkey = delegatorCmpPubkey; + + uint256 stakeAmount = 5 ether; + + vm.deal(delegatorAddr, stakeAmount + 1 gwei); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: stakeAmount }(delegatorUncmpPubkey, validatorSrcPubkey); + + // last character modified from e => f + bytes + memory validatorDstUncmpPubkey = hex"03eb8e065336169de70e591e397b76600a71b356c9c3c629a8d0987e2169588e5b64d5f0c60f03ec8f5b13ba133b0a8e0f03bbaa8e678c0d03bb9dab42626be04f"; // pragma: allowlist-secret + bytes memory validatorDstCmpPubkey = Secp256k1.compressPublicKey(validatorDstUncmpPubkey); + + // Create the new validator + ipTokenStaking.createValidatorOnBehalf{ value: 1 gwei }(validatorDstCmpPubkey); + + uint256 srcValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey); + uint256 dstValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey); + + vm.prank(delegatorAddr); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.Redelegate(delegatorCmpPubkey, validatorSrcPubkey, validatorDstCmpPubkey, stakeAmount); + ipTokenStaking.redelegate( + IIPTokenStaking.RedelegateParams({ + delegatorUncmpPubkey: delegatorUncmpPubkey, + validatorSrcPubkey: validatorSrcPubkey, + validatorDstPubkey: validatorDstCmpPubkey, + amount: stakeAmount + }) + ); + + // Check the amount for the source and destination validator + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey), + srcValidatorBefore - stakeAmount + ); + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey), + dstValidatorBefore + stakeAmount + ); + + // Network shall not allow non-operators of a stake owner to redelegate from the stake ownerā€™s public key + address operator = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + validatorSrcPubkey = delegatorCmpPubkey; + stakeAmount = 5 ether; + vm.deal(delegatorAddr, stakeAmount + 1 gwei); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: stakeAmount }(delegatorUncmpPubkey, validatorSrcPubkey); + + validatorDstUncmpPubkey = hex"03eb8e065336169de70e591e397b76600a71b356c9c3c629a8d0987e2169588e5b64d5f0c60f03ec8f5b13ba133b0a8e0f03bbaa8e678c0d03bb9dab42626be04f"; // pragma: allowlist-secret + validatorDstCmpPubkey = Secp256k1.compressPublicKey(validatorDstUncmpPubkey); + + srcValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey); + dstValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey); + + vm.prank(operator); + vm.expectRevert("IPTokenStaking: Caller is not an operator"); + ipTokenStaking.redelegateOnBehalf( + IIPTokenStaking.RedelegateParams({ + delegatorUncmpPubkey: delegatorUncmpPubkey, + validatorSrcPubkey: validatorSrcPubkey, + validatorDstPubkey: validatorDstCmpPubkey, + amount: stakeAmount + }) + ); + + // Network shall allow operators of a stake owner to redelegate from the stake ownerā€™s public key + vm.prank(delegatorAddr); + ipTokenStaking.addOperator(delegatorUncmpPubkey, operator); + validatorSrcPubkey = delegatorCmpPubkey; + stakeAmount = 5 ether; + vm.deal(delegatorAddr, stakeAmount + 1 gwei); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: stakeAmount }(delegatorUncmpPubkey, validatorSrcPubkey); + + validatorDstUncmpPubkey = hex"03eb8e065336169de70e591e397b76600a71b356c9c3c629a8d0987e2169588e5b64d5f0c60f03ec8f5b13ba133b0a8e0f03bbaa8e678c0d03bb9dab42626be04f"; // pragma: allowlist-secret + validatorDstCmpPubkey = Secp256k1.compressPublicKey(validatorDstUncmpPubkey); + + srcValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey); + dstValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey); + + vm.prank(operator); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.Redelegate(delegatorCmpPubkey, validatorSrcPubkey, validatorDstCmpPubkey, stakeAmount); + ipTokenStaking.redelegateOnBehalf( + IIPTokenStaking.RedelegateParams({ + delegatorUncmpPubkey: delegatorUncmpPubkey, + validatorSrcPubkey: validatorSrcPubkey, + validatorDstPubkey: validatorDstCmpPubkey, + amount: stakeAmount + }) + ); + + // Check the amount for the source and destination validator + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey), + srcValidatorBefore - stakeAmount + ); + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey), + dstValidatorBefore + stakeAmount + ); + + // Network shall not allow anyone to redelegate from non-existing-validator + validatorSrcPubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9888888"; // pragma: allowlist secret + stakeAmount = 5 ether; + + validatorDstUncmpPubkey = hex"03eb8e065336169de70e591e397b76600a71b356c9c3c629a8d0987e2169588e5b64d5f0c60f03ec8f5b13ba133b0a8e0f03bbaa8e678c0d03bb9dab42626be04f"; // pragma: allowlist-secret + validatorDstCmpPubkey = Secp256k1.compressPublicKey(validatorDstUncmpPubkey); + + srcValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey); + dstValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey); + + vm.prank(operator); + vm.expectRevert("IPTokenStaking: Validator does not exist"); + ipTokenStaking.redelegateOnBehalf( + IIPTokenStaking.RedelegateParams({ + delegatorUncmpPubkey: delegatorUncmpPubkey, + validatorSrcPubkey: validatorSrcPubkey, + validatorDstPubkey: validatorDstCmpPubkey, + amount: stakeAmount + }) + ); + + // Network shall not allow anyone to redelegate to non-existing-validator + validatorSrcPubkey = delegatorCmpPubkey; + stakeAmount = 5 ether; + vm.deal(delegatorAddr, stakeAmount + 1 gwei); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: stakeAmount }(delegatorUncmpPubkey, validatorSrcPubkey); + + validatorDstUncmpPubkey = hex"03eb8e065336169de70e591e397b76600a71b356c9c3c629a8d0987e2169588e5b64d5f0c60f03ec8f5b13ba133b0a8e0f03bbaa8e678c0d03bb9dab4262000000"; // pragma: allowlist-secret + validatorDstCmpPubkey = Secp256k1.compressPublicKey(validatorDstUncmpPubkey); + + vm.prank(operator); + vm.expectRevert("IPTokenStaking: Validator does not exist"); + ipTokenStaking.redelegateOnBehalf( + IIPTokenStaking.RedelegateParams({ + delegatorUncmpPubkey: delegatorUncmpPubkey, + validatorSrcPubkey: validatorSrcPubkey, + validatorDstPubkey: validatorDstCmpPubkey, + amount: stakeAmount + }) + ); + + // Network shall not allow operators or stake owners to redelegate more than the delegator staked on the source validator + validatorSrcPubkey = delegatorCmpPubkey; + stakeAmount = 5 ether; + vm.deal(delegatorAddr, stakeAmount + 1 gwei); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: stakeAmount }(delegatorUncmpPubkey, validatorSrcPubkey); + + validatorDstUncmpPubkey = hex"03eb8e065336169de70e591e397b76600a71b356c9c3c629a8d0987e2169588e5b64d5f0c60f03ec8f5b13ba133b0a8e0f03bbaa8e678c0d03bb9dab42626be04f"; // pragma: allowlist-secret + validatorDstCmpPubkey = Secp256k1.compressPublicKey(validatorDstUncmpPubkey); + + srcValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorSrcPubkey); + dstValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorDstCmpPubkey); + + vm.prank(operator); + vm.expectRevert("IPTokenStaking: Insufficient staked amount"); + ipTokenStaking.redelegateOnBehalf( + IIPTokenStaking.RedelegateParams({ + delegatorUncmpPubkey: delegatorUncmpPubkey, + validatorSrcPubkey: validatorSrcPubkey, + validatorDstPubkey: validatorDstCmpPubkey, + amount: stakeAmount + 100 ether + }) + ); + } + + function testIPTokenStaking_Unstake() public withDefaultValidator { + bytes memory validatorPubkey = delegatorCmpPubkey; + + vm.deal(delegatorAddr, 100 ether); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: 50 ether }(delegatorUncmpPubkey, validatorPubkey); + + // Network shall only allow the stake owner to withdraw from their stake pubkey + uint256 stakeAmount = 1 ether; + + uint256 delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey); + uint256 delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + ( + bool isActive, + string memory moniker, + uint256 totalStake, + uint32 commissionRate, + uint32 maxCommissionRate, + uint32 maxCommissionChangeRate + ) = ipTokenStaking.validatorMetadata(validatorPubkey); + uint256 validatorTotalBefore = totalStake; + + vm.warp(vm.getBlockTimestamp() + ipTokenStaking.withdrawalAddressChangeInterval() + 1); + + vm.startPrank(delegatorAddr); + ipTokenStaking.setWithdrawalAddress(delegatorUncmpPubkey, address(0xb0b)); + ipTokenStaking.unstake(delegatorUncmpPubkey, validatorPubkey, stakeAmount); + vm.stopPrank(); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey), + delegatorValidatorBefore - stakeAmount + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), delegatorTotalBefore - stakeAmount); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore - stakeAmount); + + // Network shall not allow non-operators of a stake owner to withdraw from the stake ownerā€™s public key + address operator = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + stakeAmount = 1 ether; + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.startPrank(operator); + vm.expectRevert("IPTokenStaking: Caller is not an operator"); + ipTokenStaking.unstakeOnBehalf(delegatorCmpPubkey, validatorPubkey, stakeAmount); + vm.stopPrank(); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey), + delegatorValidatorBefore + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), delegatorTotalBefore); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore); + + // Network shall allow operators of a stake owner to withdraw from the stake ownerā€™s public key + vm.prank(delegatorAddr); + ipTokenStaking.addOperator(delegatorUncmpPubkey, operator); + stakeAmount = 1 ether; + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.startPrank(operator); + ipTokenStaking.unstakeOnBehalf(delegatorCmpPubkey, validatorPubkey, stakeAmount); + vm.stopPrank(); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey), + delegatorValidatorBefore - stakeAmount + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), delegatorTotalBefore - stakeAmount); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore - stakeAmount); + + // Network shall not allow operators or stake owners to withdraw more than the delegator staked on the validator + stakeAmount = 100 ether; + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.startPrank(operator); + vm.expectRevert("IPTokenStaking: Insufficient staked amount"); + ipTokenStaking.unstakeOnBehalf(delegatorCmpPubkey, validatorPubkey, stakeAmount); + vm.stopPrank(); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey), + delegatorValidatorBefore + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), delegatorTotalBefore); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore); + + // Network shall not allow anyone to withdraw from stake on non-validatorsā€™ public keys + validatorPubkey = hex"03e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa9888888"; // pragma: allowlist secret + stakeAmount = 1 ether; + + delegatorValidatorBefore = ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey); + delegatorTotalBefore = ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + validatorTotalBefore = totalStake; + + vm.startPrank(operator); + vm.expectRevert("IPTokenStaking: Validator does not exist"); + ipTokenStaking.unstakeOnBehalf(delegatorCmpPubkey, validatorPubkey, stakeAmount); + vm.stopPrank(); + + assertEq( + ipTokenStaking.delegatorValidatorStakes(delegatorCmpPubkey, validatorPubkey), + delegatorValidatorBefore + ); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), delegatorTotalBefore); + (isActive, moniker, totalStake, commissionRate, maxCommissionRate, maxCommissionChangeRate) = ipTokenStaking + .validatorMetadata(validatorPubkey); + assertEq(totalStake, validatorTotalBefore); + } + + function testIPTokenStaking_SetWithdrawalAddress() public withDefaultValidator { + bytes memory validatorPubkey = delegatorCmpPubkey; + + vm.deal(delegatorAddr, 50 ether); + vm.prank(delegatorAddr); + ipTokenStaking.stake{ value: 50 ether }(delegatorUncmpPubkey, validatorPubkey); + + // Network shall allow the delegators to set their withdrawal address + vm.warp(vm.getBlockTimestamp() + ipTokenStaking.withdrawalAddressChangeInterval() + 1); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.SetWithdrawalAddress( + delegatorCmpPubkey, + 0x0000000000000000000000000000000000000000000000000000000000000b0b + ); + vm.prank(delegatorAddr); + ipTokenStaking.setWithdrawalAddress(delegatorUncmpPubkey, address(0xb0b)); + assertEq(ipTokenStaking.withdrawalAddressChange(delegatorCmpPubkey), vm.getBlockTimestamp()); + + // Network shall not allow anyone to set withdrawal address for other delegators + bytes + memory delegatorUncmpPubkey1 = hex"04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1000000"; // pragma: allowlist secret + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Invalid pubkey derived address"); + ipTokenStaking.setWithdrawalAddress(delegatorUncmpPubkey1, address(0xb0b)); + + // Network shall not allow anyone to set withdrawal address if cooldown period has not passed + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Withdrawal address change cool-down"); + ipTokenStaking.setWithdrawalAddress(delegatorUncmpPubkey, address(0xb0b)); + + // Network shall not allow anyone to set withdrawal address for 0-stake delegators + vm.prank(delegatorAddr); + ipTokenStaking.unstake(delegatorUncmpPubkey, validatorPubkey, 51 ether); + assertEq(ipTokenStaking.delegatorTotalStakes(delegatorCmpPubkey), 0 ether); + + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Delegator must have stake"); + ipTokenStaking.setWithdrawalAddress(delegatorUncmpPubkey, address(0xb0b)); + } + + function testIPTokenStaking_addOperator() public { + // Network shall not allow others to add operators for a delegator + address operator = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + bytes + memory otherDelegatorUncmpPubkey = hex"04e38d15ae6cc5d41cce27a2307903cb12a406cbf463fe5fef215bdf8aa988ced195e9327ac89cd362eaa0397f8d7f007c02b2a75642f174e455d339e4a1000000"; // pragma: allowlist secret + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Invalid pubkey derived address"); + ipTokenStaking.addOperator(otherDelegatorUncmpPubkey, operator); + } + + function isInArray(address[] memory array, address element) internal pure returns (bool) { + for (uint256 i = 0; i < array.length; i++) { + if (array[i] == element) { + return true; + } + } + return false; + } + + function testIPTokenStaking_removeOperator() public { + address operator = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + vm.prank(delegatorAddr); + ipTokenStaking.addOperator(delegatorUncmpPubkey, operator); + assert(isInArray(ipTokenStaking.getOperators(delegatorCmpPubkey), operator)); + + // Network shall not allow others to remove operators for a delegator + address otherAddress = address(0xf398c12A45BC409b6C652e25bb0A3e702492A4AA); + vm.prank(otherAddress); + vm.expectRevert("IPTokenStaking: Invalid pubkey derived address"); + ipTokenStaking.removeOperator(delegatorUncmpPubkey, operator); + assert(isInArray(ipTokenStaking.getOperators(delegatorCmpPubkey), operator)); + + // Network shall allow delegators to remove their operators + vm.prank(delegatorAddr); + ipTokenStaking.removeOperator(delegatorUncmpPubkey, operator); + assert(!isInArray(ipTokenStaking.getOperators(delegatorCmpPubkey), operator)); + + // Removing an operator that does not exist reverts + vm.prank(delegatorAddr); + vm.expectRevert("IPTokenStaking: Operator not found"); + ipTokenStaking.removeOperator(delegatorUncmpPubkey, operator); + } + + function testIPTokenStaking_setMinStakeAmount() public { + // Set amount that will be rounded down to 0 + vm.prank(address(this)); + ipTokenStaking.setMinStakeAmount(5 wei); + assertEq(ipTokenStaking.minStakeAmount(), 0); + + // Set amount that will not be rounded + vm.prank(address(this)); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.MinStakeAmountSet(1 ether); + ipTokenStaking.setMinStakeAmount(1 ether); + assertEq(ipTokenStaking.minStakeAmount(), 1 ether); + + // Set 0 + vm.prank(address(this)); + vm.expectRevert("IPTokenStaking: minStakeAmount cannot be 0"); + ipTokenStaking.setMinStakeAmount(0 ether); + + // Set using a non-owner address + vm.prank(delegatorAddr); + vm.expectRevert(); + ipTokenStaking.setMinStakeAmount(1 ether); + } + + function testIPTokenStaking_setMinUnstakeAmount() public { + // Set amount that will be rounded down to 0 + vm.prank(address(this)); + ipTokenStaking.setMinUnstakeAmount(5 wei); + assertEq(ipTokenStaking.minUnstakeAmount(), 0); + + // Set amount that will not be rounded + vm.prank(address(this)); + vm.expectEmit(address(ipTokenStaking)); + emit IIPTokenStaking.MinUnstakeAmountSet(1 ether); + ipTokenStaking.setMinUnstakeAmount(1 ether); + assertEq(ipTokenStaking.minUnstakeAmount(), 1 ether); + + // Set 0 + vm.prank(address(this)); + vm.expectRevert("IPTokenStaking: minUnstakeAmount cannot be 0"); + ipTokenStaking.setMinUnstakeAmount(0 ether); + + // Set using a non-owner address + vm.prank(delegatorAddr); + vm.expectRevert(); + ipTokenStaking.setMinUnstakeAmount(1 ether); + } + + function testIPTokenStaking_setMinRedelegateAmount() public { + // Set amount that will be rounded down to 0 + vm.prank(address(this)); + ipTokenStaking.setMinRedelegateAmount(5 wei); + assertEq(ipTokenStaking.minRedelegateAmount(), 0); + + // Set amount that will not be rounded + vm.prank(address(this)); + ipTokenStaking.setMinRedelegateAmount(1 ether); + assertEq(ipTokenStaking.minRedelegateAmount(), 1 ether); + + // Set 0 + vm.prank(address(this)); + vm.expectRevert("IPTokenStaking: minRedelegateAmount cannot be 0"); + ipTokenStaking.setMinRedelegateAmount(0 ether); + + // Set using a non-owner address + vm.prank(delegatorAddr); + vm.expectRevert(); + ipTokenStaking.setMinRedelegateAmount(1 ether); + } +} diff --git a/contracts/test/upgrade/UpgradeEntryPoint.t.sol b/contracts/test/upgrade/UpgradeEntryPoint.t.sol new file mode 100644 index 00000000..93c76879 --- /dev/null +++ b/contracts/test/upgrade/UpgradeEntryPoint.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +/* solhint-disable no-console */ +/* solhint-disable max-line-length */ +/// NOTE: pragma allowlist-secret must be inline (same line as the pubkey hex string) to avoid false positive +/// flag "Hex High Entropy String" in CI run detect-secrets + +import { Test } from "forge-std/Test.sol"; + +import { UpgradeEntrypoint, IUpgradeEntrypoint } from "../../src/protocol/UpgradeEntrypoint.sol"; + +contract UpgradeEntrypointTest is Test { + UpgradeEntrypoint private upgradeEntrypoint; + + function setUp() public { + address protocolAccessManagerAddr = address(this); + + upgradeEntrypoint = new UpgradeEntrypoint(protocolAccessManagerAddr); + } + + function testUpgradeEntrypoint_planUpgrade() public { + // Network shall allow the protocol owner to submit an upgrade plan. + string memory name = "upgrade"; + int64 height = 1; + string memory info = "info"; + + vm.prank(address(this)); + vm.expectEmit(address(upgradeEntrypoint)); + emit IUpgradeEntrypoint.SoftwareUpgrade(name, height, info); + upgradeEntrypoint.planUpgrade(name, height, info); + + // Network shall not allow non-protocol owner to submit an upgrade plan. + address otherAddr = address(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab); + vm.prank(otherAddr); + vm.expectRevert(); + upgradeEntrypoint.planUpgrade(name, height, info); + } +} diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json new file mode 100644 index 00000000..492cfbe6 --- /dev/null +++ b/contracts/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + }, +} diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 00000000..577774da --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,2 @@ +runs +failed-logs* diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..8a1782b0 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,50 @@ +# End-to-End Tests + +Spins up and tests Iliad devnets in Docker Compose based on a testnet manifest. To run the CI testnet: + +```sh +# In repo root +# Install the e2e app +go install github.com/piplabs/story/e2e + +# Build docker image of the code to test. +make build-docker + +# Run one of the "manifests" in networks/ directory: e2e -f +e2e -f e2e/networks/single.toml +``` + +This creates and runs a testnet named `single` under `e2e/runs/single/`. + +## Conceptual Overview + +Please refer to the [cometBFT E2E test framework](https://github.com/cometbft/cometbft/tree/main/test/e2e) for more details. + +In order to perform any action on a network (deploy/test/show logs), the following process is followed to create a network `Definition`: +1. A network is initially declared in a `manifest` file, see [manifests/](./manifests) folder. It defines the desired network topology. See the `e2e/types#Manifest` type for details. +2. Then the infrastructure provider (only `docker compose` supported at the moment) subsequently generates the `e2e/types#InfrastructureData` from the manifest. This defines the instance IPs and ports of everything we will deploy. +3. Subsequently, we generate a `Testnet` struct which is basically contains all the configuration/keys/peers/images/files/folders required to deploy a network. See `e2e/types#Testnet` for details. +4. We then instantiate a `netman.Manager` which is responsible for deploying portals. It takes a `Testnet` struct as input. +5. Finally, we instantiate new `InfrastructureProvider` which can deploy the network. It takes a `Testnet` struct and `InfrastructureData` as input. + +These objects are then wrapped in a `e2e/app#Definition` that can be used to perform any action on a network. + +## Test Stages + +The e2e test has the following stages, which can also be executed explicitly by running `e2e -f `: + +* `setup`: generates configuration files. + +* `start`: starts Docker containers. + +* `wait`: waits for a few blocks to be produced, and for all nodes to catch up to it. + +* `stop`: stops Docker containers. + +* `cleanup`: removes configuration files and Docker containers/networks. + +Auxiliary commands: + +* `logs`: outputs all node logs. + +* `tail`: tails (follows) node logs until canceled. diff --git a/e2e/app/agent/prometheus.go b/e2e/app/agent/prometheus.go new file mode 100644 index 00000000..fdbc8146 --- /dev/null +++ b/e2e/app/agent/prometheus.go @@ -0,0 +1,157 @@ +package agent + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" + + _ "embed" +) + +type Secrets struct { + URL string + User string + Pass string +} + +const promPort = 26660 // Default metrics port for all iliad apps (from cometBFT) +const gethPromPort = 6060 + +//go:embed prometheus.yml.tmpl +var promConfigTmpl []byte + +func WriteConfig(ctx context.Context, testnet types.Testnet, secrets Secrets) error { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + bz, err := genPromConfig(ctx, testnet, secrets, hostname) + if err != nil { + return errors.Wrap(err, "generating prometheus config") + } + + promFile := filepath.Join(testnet.Dir, "prometheus", "prometheus.yml") + if err := os.MkdirAll(filepath.Dir(promFile), 0755); err != nil { + return errors.Wrap(err, "creating prometheus dir") + } + + if err := os.WriteFile(promFile, bz, 0644); err != nil { + return errors.Wrap(err, "writing prometheus config") + } + + return nil +} + +func genPromConfig(ctx context.Context, testnet types.Testnet, secrets Secrets, hostname string) ([]byte, error) { + var nodeTargets []string + for _, node := range testnet.Nodes { + // Prometheus is always inside the same docker-compose, so use service names. + nodeTargets = append(nodeTargets, fmt.Sprintf("%s:%d", node.Name, promPort)) + } + + var evmTargets []string + for _, iliadEVM := range testnet.IliadEVMs { + evmTargets = append(evmTargets, fmt.Sprintf("%s:%d", iliadEVM.InstanceName, gethPromPort)) + } + + network := string(testnet.Network) + if testnet.Network == netconf.Devnet { + network = fmt.Sprintf("%s-%s", testnet.Name, hostname) + } + + if secrets.URL == "" { + log.Warn(ctx, "Prometheus remote URL not set, metrics not being pushed to Grafana cloud", nil) + } else { + log.Info(ctx, "Prometheus metrics pushed to Grafana cloud", "network", network) + } + + data := promTmplData{ + Network: network, + Host: hostname, + RemoteURL: secrets.URL, + RemoteUsername: secrets.User, + RemotePassword: secrets.Pass, + ScrapeConfigs: []promScrapConfig{ + { + JobName: "iliad", + MetricsPath: "/metrics", + targets: nodeTargets, + }, + { + JobName: "geth", + MetricsPath: "/debug/metrics/prometheus", + targets: evmTargets, + }, + }, + } + + t, err := template.New("").Parse(string(promConfigTmpl)) + if err != nil { + return nil, errors.Wrap(err, "parsing template") + } + + var bz bytes.Buffer + if err := t.Execute(&bz, data); err != nil { + return nil, errors.Wrap(err, "executing template") + } + + return bz.Bytes(), nil +} + +type promTmplData struct { + Network string // Used a "network" label to all metrics + Host string // Hostname of the docker host machine + RemoteURL string // URL to the Grafana cloud server + RemoteUsername string // Username to the Grafana cloud server + RemotePassword string // Password to the Grafana cloud server + ScrapeConfigs []promScrapConfig // List of scrape configs +} + +type promScrapConfig struct { + JobName string + MetricsPath string + targets []string +} + +func (c promScrapConfig) Targets() string { + return strings.Join(c.targets, ",") +} + +// ConfigForHost returns a new prometheus agent config with the given host and iliad targets. +// +// It replaces the iliad targets with provided. +// It replaces the geth targets with provided. +// It replaces the host label. +func ConfigForHost(bz []byte, newHost string, iliads []string, geths []string) []byte { + var iliadTargets []string + for _, iliad := range iliads { + iliadTargets = append(iliadTargets, fmt.Sprintf(`"%s:%d"`, iliad, promPort)) + } + replace := fmt.Sprintf(`[%s] # iliad targets`, strings.Join(iliadTargets, ",")) + bz = regexp.MustCompile(`(?m)\[.*\] # iliad targets$`). + ReplaceAll(bz, []byte(replace)) + + var gethTargets []string + for _, geth := range geths { + gethTargets = append(gethTargets, fmt.Sprintf(`"%s:%d"`, geth, gethPromPort)) + } + replace = fmt.Sprintf(`[%s] # geth targets`, strings.Join(gethTargets, ",")) + bz = regexp.MustCompile(`(?m)\[.*\] # geth targets$`). + ReplaceAll(bz, []byte(replace)) + + bz = regexp.MustCompile(`(?m)host: '.*'$`). + ReplaceAll(bz, []byte(fmt.Sprintf(`host: '%s'`, newHost))) + + return bz +} diff --git a/e2e/app/agent/prometheus.yml.tmpl b/e2e/app/agent/prometheus.yml.tmpl new file mode 100644 index 00000000..d545d7af --- /dev/null +++ b/e2e/app/agent/prometheus.yml.tmpl @@ -0,0 +1,28 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. + +{{- if .RemoteURL }} +remote_write: + - url: {{ .RemoteURL }} + basic_auth: + username: {{ .RemoteUsername }} + password: {{ .RemotePassword }} + write_relabel_configs: + # Add 'container' label using 'instance without port' + - source_labels: [instance] + regex: '(.+):(\d+)' + target_label: container + replacement: '${1}' +{{ end }} + +scrape_configs: +{{- range .ScrapeConfigs }} + - job_name: "{{ .JobName }}" + metrics_path: "{{ .MetricsPath }}" + static_configs: + - targets: [{{ .Targets }}] # {{ .JobName }} targets + labels: + network: '{{ $.Network }}' + host: '{{ $.Host }}' +{{ end }} diff --git a/e2e/app/agent/prometheus_internal_test.go b/e2e/app/agent/prometheus_internal_test.go new file mode 100644 index 00000000..19220ea6 --- /dev/null +++ b/e2e/app/agent/prometheus_internal_test.go @@ -0,0 +1,115 @@ +package agent + +import ( + "context" + "testing" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test . -golden -clean + +func TestPromGen(t *testing.T) { + t.Parallel() + tests := []struct { + name string + network netconf.ID + nodes []string + newNodes []string + geths []string + newGeths []string + hostname string + agentSecrets bool + }{ + { + name: "manifest1", + network: netconf.Devnet, + nodes: []string{"validator01", "validator02"}, + hostname: "localhost", + newNodes: []string{"validator01"}, + geths: []string{"iliad_evm"}, + newGeths: []string{"iliad_evm"}, + agentSecrets: false, + }, + { + name: "manifest2", + network: netconf.Staging, + nodes: []string{"validator01", "validator02", "fullnode03"}, + hostname: "vm", + newNodes: []string{"fullnode04"}, + geths: []string{"validator01_evm", "validator02_evm", "validator03_evm"}, + newGeths: []string{"fullnode04_evm"}, + agentSecrets: true, + }, + { + name: "manifest3", + network: netconf.Devnet, + nodes: []string{"validator01", "validator02"}, + hostname: "localhost", + newNodes: []string{"validator01"}, + agentSecrets: false, + }, + { + name: "manifest4", + network: netconf.Staging, + nodes: []string{"validator01", "validator02", "fullnode03"}, + hostname: "vm", + newNodes: []string{"fullnode04"}, + agentSecrets: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + var nodes []*e2e.Node + for _, name := range test.nodes { + nodes = append(nodes, &e2e.Node{Name: name}) + } + + var geths []types.IliadEVM + for _, name := range test.geths { + geths = append(geths, types.IliadEVM{InstanceName: name}) + } + + testnet := types.Testnet{ + Network: test.network, + Testnet: &e2e.Testnet{ + Name: test.name, + Nodes: nodes, + }, + IliadEVMs: geths, + } + + var agentSecrets Secrets + if test.agentSecrets { + agentSecrets = Secrets{ + URL: "https://grafana.com", + User: "admin", + Pass: "password", + } + } + + cfg1, err := genPromConfig(ctx, testnet, agentSecrets, test.hostname) + require.NoError(t, err) + + cfg2 := ConfigForHost(cfg1, test.hostname+"-2", test.newNodes, test.newGeths) + + t.Run("gen", func(t *testing.T) { + t.Parallel() + tutil.RequireGoldenBytes(t, cfg1) + }) + + t.Run("update", func(t *testing.T) { + t.Parallel() + tutil.RequireGoldenBytes(t, cfg2) + }) + }) + } +} diff --git a/e2e/app/agent/testdata/TestPromGen_manifest1_gen.golden b/e2e/app/agent/testdata/TestPromGen_manifest1_gen.golden new file mode 100644 index 00000000..19bd91c2 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest1_gen.golden @@ -0,0 +1,21 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: [validator01:26660,validator02:26660] # iliad targets + labels: + network: 'manifest1-localhost' + host: 'localhost' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: [iliad_evm:6060] # geth targets + labels: + network: 'manifest1-localhost' + host: 'localhost' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest1_update.golden b/e2e/app/agent/testdata/TestPromGen_manifest1_update.golden new file mode 100644 index 00000000..1dba1d91 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest1_update.golden @@ -0,0 +1,21 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: ["validator01:26660"] # iliad targets + labels: + network: 'manifest1-localhost' + host: 'localhost-2' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: ["iliad_evm:6060"] # geth targets + labels: + network: 'manifest1-localhost' + host: 'localhost-2' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest2_gen.golden b/e2e/app/agent/testdata/TestPromGen_manifest2_gen.golden new file mode 100644 index 00000000..5b3357c6 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest2_gen.golden @@ -0,0 +1,33 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. +remote_write: + - url: https://grafana.com + basic_auth: + username: admin + password: password + write_relabel_configs: + # Add 'container' label using 'instance without port' + - source_labels: [instance] + regex: '(.+):(\d+)' + target_label: container + replacement: '${1}' + + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: [validator01:26660,validator02:26660,fullnode03:26660] # iliad targets + labels: + network: 'staging' + host: 'vm' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: [validator01_evm:6060,validator02_evm:6060,validator03_evm:6060] # geth targets + labels: + network: 'staging' + host: 'vm' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest2_update.golden b/e2e/app/agent/testdata/TestPromGen_manifest2_update.golden new file mode 100644 index 00000000..ce7471a4 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest2_update.golden @@ -0,0 +1,33 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. +remote_write: + - url: https://grafana.com + basic_auth: + username: admin + password: password + write_relabel_configs: + # Add 'container' label using 'instance without port' + - source_labels: [instance] + regex: '(.+):(\d+)' + target_label: container + replacement: '${1}' + + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: ["fullnode04:26660"] # iliad targets + labels: + network: 'staging' + host: 'vm-2' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: ["fullnode04_evm:6060"] # geth targets + labels: + network: 'staging' + host: 'vm-2' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest3_gen.golden b/e2e/app/agent/testdata/TestPromGen_manifest3_gen.golden new file mode 100644 index 00000000..0087189c --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest3_gen.golden @@ -0,0 +1,21 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: [validator01:26660,validator02:26660] # iliad targets + labels: + network: 'manifest3-localhost' + host: 'localhost' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: [] # geth targets + labels: + network: 'manifest3-localhost' + host: 'localhost' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest3_update.golden b/e2e/app/agent/testdata/TestPromGen_manifest3_update.golden new file mode 100644 index 00000000..1eed30b9 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest3_update.golden @@ -0,0 +1,21 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: ["validator01:26660"] # iliad targets + labels: + network: 'manifest3-localhost' + host: 'localhost-2' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: [] # geth targets + labels: + network: 'manifest3-localhost' + host: 'localhost-2' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest4_gen.golden b/e2e/app/agent/testdata/TestPromGen_manifest4_gen.golden new file mode 100644 index 00000000..bd5024d2 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest4_gen.golden @@ -0,0 +1,33 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. +remote_write: + - url: https://grafana.com + basic_auth: + username: admin + password: password + write_relabel_configs: + # Add 'container' label using 'instance without port' + - source_labels: [instance] + regex: '(.+):(\d+)' + target_label: container + replacement: '${1}' + + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: [validator01:26660,validator02:26660,fullnode03:26660] # iliad targets + labels: + network: 'staging' + host: 'vm' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: [] # geth targets + labels: + network: 'staging' + host: 'vm' + diff --git a/e2e/app/agent/testdata/TestPromGen_manifest4_update.golden b/e2e/app/agent/testdata/TestPromGen_manifest4_update.golden new file mode 100644 index 00000000..f9e31b85 --- /dev/null +++ b/e2e/app/agent/testdata/TestPromGen_manifest4_update.golden @@ -0,0 +1,33 @@ +global: + scrape_interval: 30s # Set the scrape interval to every 30 seconds. + evaluation_interval: 30s # Evaluate rules every 30 seconds. +remote_write: + - url: https://grafana.com + basic_auth: + username: admin + password: password + write_relabel_configs: + # Add 'container' label using 'instance without port' + - source_labels: [instance] + regex: '(.+):(\d+)' + target_label: container + replacement: '${1}' + + +scrape_configs: + - job_name: "iliad" + metrics_path: "/metrics" + static_configs: + - targets: ["fullnode04:26660"] # iliad targets + labels: + network: 'staging' + host: 'vm-2' + + - job_name: "geth" + metrics_path: "/debug/metrics/prometheus" + static_configs: + - targets: [] # geth targets + labels: + network: 'staging' + host: 'vm-2' + diff --git a/e2e/app/cleanup.go b/e2e/app/cleanup.go new file mode 100644 index 00000000..50305370 --- /dev/null +++ b/e2e/app/cleanup.go @@ -0,0 +1,65 @@ +package app + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/cometbft/cometbft/test/e2e/pkg/infra/docker" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// CleanInfra stops and removes the infra containers. +func CleanInfra(ctx context.Context, def Definition) error { + if err := def.Infra.Clean(ctx); err != nil { + return errors.Wrap(err, "cleaning infrastructure") + } + + return nil +} + +// CleanupDir cleans up a testnet directory. +func CleanupDir(ctx context.Context, dir string) error { + if dir == "" { + return errors.New("no directory set") + } + + _, err := os.Stat(dir) + if os.IsNotExist(err) { + return nil + } else if err != nil { + return errors.Wrap(err, "stat") + } + + log.Info(ctx, "Cleanup dir", "dir", dir) + + // On Linux, some local files in the volume will be owned by root since CometBFT + // runs as root inside the container, so we need to clean them up from within a + // container running as root too. + if runtime.GOOS == "linux" { + absDir, err := filepath.Abs(dir) + if err != nil { + return errors.Wrap(err, "abs dir") + } + err = docker.Exec(ctx, "run", + "--rm", // Remove the container after it exits + "--entrypoint", "", // Clear the entrypoint so we can run a shell command + "-v", fmt.Sprintf("%v:/mount", absDir), // Mount the testnet dir into the container + "ethereum/client-go:latest", // Use the latest geth image (which runs as root) + "sh", "-c", "rm -rf /mount/*/") // Remove all files in the mounted testnet dir + if err != nil { + return errors.Wrap(err, "exec rm dir") + } + } + + err = os.RemoveAll(dir) + if err != nil { + return errors.Wrap(err, "remove dir") + } + + return nil +} diff --git a/e2e/app/create3.go b/e2e/app/create3.go new file mode 100644 index 00000000..cb4c5b99 --- /dev/null +++ b/e2e/app/create3.go @@ -0,0 +1,81 @@ +package app + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/lib/contracts/create3" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +type Create3DeployConfig struct { + ChainID uint64 // chain id of the chain to deploy to +} + +// Validate validates the Create3DeployConfig. +func (cfg Create3DeployConfig) Validate() error { + if cfg.ChainID == 0 { + return errors.New("chain id is zero") + } + + return nil +} + +// Create3Deploy deploys the Iliad Create3 contracts. +func Create3Deploy(ctx context.Context, def Definition, cfg Create3DeployConfig) error { + if err := cfg.Validate(); err != nil { + return errors.Wrap(err, "validate") + } + + addr, receipt, err := deployCreate3(ctx, def, cfg.ChainID) + if err != nil { + return errors.Wrap(err, "deploy create3") + } + + log.Info(ctx, "Create3 factory deployed", "chain", cfg.ChainID, "addr", addr.Hex(), "block", receipt.BlockNumber) + + return nil +} + +func deployPrivateCreate3(ctx context.Context, def Definition) error { + for _, c := range def.Testnet.AnvilChains { + _, _, err := deployCreate3(ctx, def, c.Chain.ChainID) + if err != nil { + return errors.Wrap(err, "deploy create3") + } + } + + // only deploy to iliad evm once + if len(def.Testnet.IliadEVMs) > 0 { + c := def.Testnet.IliadEVMs[0] + _, _, err := deployCreate3(ctx, def, c.Chain.ChainID) + if err != nil { + return errors.Wrap(err, "deploy create3") + } + } + + return nil +} + +func deployPublicCreate3(ctx context.Context, def Definition) error { + for _, c := range def.Testnet.PublicChains { + _, _, err := deployCreate3(ctx, def, c.Chain().ChainID) + if err != nil { + return errors.Wrap(err, "deploy create3") + } + } + + return nil +} + +func deployCreate3(ctx context.Context, def Definition, chainID uint64) (common.Address, *ethtypes.Receipt, error) { + backend, err := def.Backends().Backend(chainID) + if err != nil { + return common.Address{}, nil, err + } + + return create3.DeployIfNeeded(ctx, def.Testnet.Network, backend) +} diff --git a/e2e/app/definition.go b/e2e/app/definition.go new file mode 100644 index 00000000..5205785f --- /dev/null +++ b/e2e/app/definition.go @@ -0,0 +1,561 @@ +package app + +import ( + "context" + "fmt" + "net" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/BurntSushi/toml" + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/cometbft/cometbft/test/e2e/pkg/exec" + "github.com/ethereum/go-ethereum/p2p/enode" + + "github.com/piplabs/story/e2e/app/agent" + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/e2e/docker" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/e2e/vmcompose" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient/ethbackend" + "github.com/piplabs/story/lib/fireblocks" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" +) + +const iliadConsensus = "iliad_consensus" + +// DefinitionConfig is the configuration required to create a full Definition. +type DefinitionConfig struct { + AgentSecrets agent.Secrets + + ManifestFile string + InfraProvider string + + // Secrets (not required for devnet) + DeployKeyFile string + FireAPIKey string + FireKeyPath string + RPCOverrides map[string]string // map[chainName]rpcURL1,rpcURL2,... + + InfraDataFile string // Not required for docker provider + IliadImgTag string // IliadImgTag is the docker image tag used for iliad. + + TracingEndpoint string + TracingHeaders string +} + +// DefaultDefinitionConfig returns a default configuration for a Definition. +func DefaultDefinitionConfig(ctx context.Context) DefinitionConfig { + defaultTag := "main" + if out, err := exec.CommandOutput(ctx, "git", "rev-parse", "--short=7", "HEAD"); err == nil { + defaultTag = strings.TrimSpace(string(out)) + } + + return DefinitionConfig{ + AgentSecrets: agent.Secrets{}, // empty agent.Secrets by default + InfraProvider: docker.ProviderName, + IliadImgTag: defaultTag, + } +} + +// Definition defines a e2e network. All (sub)commands of the e2e cli requires a definition operate. +// Armed with a definition, a e2e network can be deployed, started, tested, stopped, etc. +type Definition struct { + Manifest types.Manifest + Testnet types.Testnet // Note that testnet is the cometBFT term. + Infra types.InfraProvider + Cfg DefinitionConfig // Original config used to construct the Definition. + lazyNetwork *lazyNetwork // lazyNetwork does lazy setup of backends (only if required). +} + +// InitLazyNetwork initializes the lazy network, which is the backends. +func (d Definition) InitLazyNetwork() error { + return d.lazyNetwork.Init() +} + +// Backends returns the backends. +func (d Definition) Backends() ethbackend.Backends { + return d.lazyNetwork.MustBackends() +} + +func MakeDefinition(ctx context.Context, cfg DefinitionConfig, commandName string) (Definition, error) { + if strings.TrimSpace(cfg.ManifestFile) == "" { + return Definition{}, errors.New("manifest not specified, use --manifest-file or -f") + } + + manifest, err := LoadManifest(cfg.ManifestFile) + if err != nil { + return Definition{}, errors.Wrap(err, "loading manifest") + } + + var infd types.InfrastructureData + switch cfg.InfraProvider { + case docker.ProviderName: + infd, err = docker.NewInfraData(manifest) + case vmcompose.ProviderName: + infd, err = vmcompose.LoadData(cfg.InfraDataFile) + default: + return Definition{}, errors.New("unknown infra provider", "provider", cfg.InfraProvider) + } + if err != nil { + return Definition{}, errors.Wrap(err, "loading infrastructure data") + } + + testnet, err := TestnetFromManifest(ctx, manifest, infd, cfg) + if err != nil { + return Definition{}, errors.Wrap(err, "loading testnet") + } + + // Setup lazy network, this is only executed by command that require networking. + lazy := func() (ethbackend.Backends, error) { + backends, err := newBackends(ctx, cfg, testnet, commandName) + if err != nil { + return ethbackend.Backends{}, errors.Wrap(err, "new backends") + } + + return backends, nil + } + + var infp types.InfraProvider + switch cfg.InfraProvider { + case docker.ProviderName: + infp = docker.NewProvider(testnet, infd, cfg.IliadImgTag) + case vmcompose.ProviderName: + infp = vmcompose.NewProvider(testnet, infd, cfg.IliadImgTag) + default: + return Definition{}, errors.New("unknown infra provider", "provider", cfg.InfraProvider) + } + + return Definition{ + Manifest: manifest, + Testnet: testnet, + Infra: infp, + lazyNetwork: &lazyNetwork{initFunc: lazy}, + Cfg: cfg, + }, nil +} + +func newBackends(ctx context.Context, cfg DefinitionConfig, testnet types.Testnet, commandName string) (ethbackend.Backends, error) { + // If no fireblocks API key, use in-memory keys. + if cfg.FireAPIKey == "" { + return ethbackend.NewBackends(testnet, cfg.DeployKeyFile) + } + + key, err := fireblocks.LoadKey(cfg.FireKeyPath) + if err != nil { + return ethbackend.Backends{}, errors.Wrap(err, "load fireblocks key") + } + + fireCl, err := fireblocks.New(testnet.Network, cfg.FireAPIKey, key, + fireblocks.WithSignNote(fmt.Sprintf("iliad e2e %s %s", commandName, testnet.Network)), + ) + if err != nil { + return ethbackend.Backends{}, errors.Wrap(err, "new fireblocks") + } + + // TODO: Fireblocks keys need to be funded on private/internal chains we deploy. + + return ethbackend.NewFireBackends(ctx, testnet, fireCl) +} + +// adaptCometTestnet adapts the default comet testnet for iliad specific changes and custom config. +func adaptCometTestnet(ctx context.Context, manifest types.Manifest, testnet *e2e.Testnet, imgTag string) (*e2e.Testnet, error) { + testnet.Dir = runsDir(testnet.File) + testnet.VoteExtensionsEnableHeight = 1 + testnet.UpgradeVersion = "iliadops/iliad:" + imgTag + + for i := range testnet.Nodes { + var err error + testnet.Nodes[i], err = adaptNode(ctx, manifest, testnet, testnet.Nodes[i], imgTag) + if err != nil { + return nil, err + } + } + + return testnet, nil +} + +// adaptNode adapts the default comet node for iliad specific changes and custom config. +func adaptNode(ctx context.Context, manifest types.Manifest, testnet *e2e.Testnet, node *e2e.Node, tag string) (*e2e.Node, error) { + valKey, err := getOrGenKey(ctx, manifest, node.Name, key.Validator) + if err != nil { + return nil, err + } + nodeKey, err := getOrGenKey(ctx, manifest, node.Name, key.P2PConsensus) + if err != nil { + return nil, err + } + + node.Version = "iliadops/iliad:" + tag + node.PrivvalKey = valKey.PrivKey + node.NodeKey = nodeKey.PrivKey + + // Add seeds (cometBFT only adds seeds defined explicitly per node, we auto-add all seeds). + seeds := manifest.Seeds() + for seed := range seeds { + if seed == node.Name { + continue // Skip self + } + node.Seeds = append(node.Seeds, testnet.LookupNode(seed)) + } + // Remove seeds from persisted peers (cometBFT adds all nodes as peers by default). + var persisted []*e2e.Node + for _, peer := range node.PersistentPeers { + if seeds[peer.Name] { + continue + } + persisted = append(persisted, peer) + } + node.PersistentPeers = persisted + + return node, nil +} + +// runsDir returns the runs directory for a given manifest file. +// E.g. /path/to/manifests/manifest.toml > /path/to/runs/manifest. +func runsDir(manifestFile string) string { + resp := strings.TrimSuffix(manifestFile, filepath.Ext(manifestFile)) + return strings.Replace(resp, "manifests", "runs", 1) +} + +// LoadManifest loads a manifest from disk. +func LoadManifest(path string) (types.Manifest, error) { + manifest := types.Manifest{} + _, err := toml.DecodeFile(path, &manifest) //nolint:musttag // toml tags annotated in struct + if err != nil { + return manifest, errors.Wrap(err, "decode manifest") + } + + return manifest, nil +} + +func NoNodesTestnet(manifest types.Manifest, infd types.InfrastructureData, cfg DefinitionConfig) (types.Testnet, error) { + publics, err := publicChains(manifest, cfg) + if err != nil { + return types.Testnet{}, err + } + + cmtTestnet, err := noNodesTestnet(manifest.Manifest, cfg.ManifestFile, infd.InfrastructureData) + if err != nil { + return types.Testnet{}, errors.Wrap(err, "testnet from manifest") + } + + return types.Testnet{ + Network: manifest.Network, + Testnet: cmtTestnet, + PublicChains: publics, + }, nil +} + +// noNodesTestnet returns a bare minimum instance of *e2e.Testnet. It doesn't have any nodes or chain details setup. +func noNodesTestnet(manifest e2e.Manifest, file string, ifd e2e.InfrastructureData) (*e2e.Testnet, error) { + dir := strings.TrimSuffix(file, filepath.Ext(file)) + + _, ipNet, err := net.ParseCIDR(ifd.Network) + if err != nil { + return nil, errors.Wrap(err, "parse network ip", "network", ifd.Network) + } + + testnet := &e2e.Testnet{ + Name: filepath.Base(dir), + File: file, + Dir: runsDir(file), + IP: ipNet, + InitialState: manifest.InitialState, + Prometheus: manifest.Prometheus, + } + + return testnet, nil +} + +//nolint:nosprintfhostport // Not an issue for non-critical e2e test code. +func TestnetFromManifest(ctx context.Context, manifest types.Manifest, infd types.InfrastructureData, cfg DefinitionConfig) (types.Testnet, error) { + cmtTestnet, err := e2e.NewTestnetFromManifest(manifest.Manifest, cfg.ManifestFile, infd.InfrastructureData) + if err != nil { + return types.Testnet{}, errors.Wrap(err, "testnet from manifest") + } + cmtTestnet, err = adaptCometTestnet(ctx, manifest, cmtTestnet, cfg.IliadImgTag) + if err != nil { + return types.Testnet{}, errors.Wrap(err, "adapt comet testnet") + } + + var iliadEVMS []types.IliadEVM + for name, isArchive := range manifest.IliadEVMs() { + inst, ok := infd.Instances[name] + if !ok { + return types.Testnet{}, errors.New("iliad evm instance not found in infrastructure data") + } + + pk, err := getOrGenKey(ctx, manifest, name, key.P2PExecution) + if err != nil { + return types.Testnet{}, errors.Wrap(err, "execution node key") + } + nodeKey, err := pk.ECDSA() + if err != nil { + return types.Testnet{}, err + } + + en := enode.NewV4(&nodeKey.PublicKey, inst.IPAddress, 30303, 30303) + + internalIP := inst.IPAddress.String() + advertisedIP := inst.ExtIPAddress // EVM P2P NAT advertised address. + if infd.Provider == docker.ProviderName { + internalIP = name // For docker, we use container names + advertisedIP = inst.IPAddress // For docker, we use container IPs for evm p2p networking, not localhost. + } + + iliadEVMS = append(iliadEVMS, types.IliadEVM{ + Chain: types.IliadEVMByNetwork(manifest.Network), + InstanceName: name, + AdvertisedIP: advertisedIP, + ProxyPort: inst.Port, + InternalRPC: fmt.Sprintf("http://%s:8545", internalIP), + ExternalRPC: fmt.Sprintf("http://%s:%d", inst.ExtIPAddress.String(), inst.Port), + NodeKey: nodeKey, + Enode: en, + IsArchive: isArchive, + JWTSecret: tutil.RandomHash().Hex(), + }) + } + + // Second pass to mesh the bootnodes + for i := range iliadEVMS { + var bootnodes []*enode.Node + for j, bootEVM := range iliadEVMS { + if i == j { + continue // Skip self + } + bootnodes = append(bootnodes, bootEVM.Enode) + } + iliadEVMS[i].Peers = bootnodes + } + + anvilEVMs, err := types.AnvilChainsByNames(manifest.AnvilChains) + if err != nil { + return types.Testnet{}, err + } + + var anvils []types.AnvilChain + for _, chain := range anvilEVMs { + inst, ok := infd.Instances[chain.Name] + if !ok { + return types.Testnet{}, errors.New("anvil chain instance not found in infrastructure data") + } + + internalIP := inst.IPAddress.String() + if infd.Provider == docker.ProviderName { + internalIP = chain.Name // For docker, we use container names + } + + anvils = append(anvils, types.AnvilChain{ + Chain: chain, + InternalIP: inst.IPAddress, + ProxyPort: inst.Port, + LoadState: "./anvil/state.json", + InternalRPC: fmt.Sprintf("http://%s:8545", internalIP), + ExternalRPC: fmt.Sprintf("http://%s:%d", inst.ExtIPAddress.String(), inst.Port), + }) + } + + publics, err := publicChains(manifest, cfg) + if err != nil { + return types.Testnet{}, err + } + + return types.Testnet{ + Network: manifest.Network, + Testnet: cmtTestnet, + IliadEVMs: iliadEVMS, + AnvilChains: anvils, + PublicChains: publics, + Perturb: manifest.Perturb, + }, nil +} + +// getOrGenKey gets (based on manifest) or creates a private key for the given node and type. +func getOrGenKey(ctx context.Context, manifest types.Manifest, nodeName string, typ key.Type) (key.Key, error) { + addr, ok := manifest.Keys[nodeName][typ] + if !ok { // No key in manifest + // Generate an insecure deterministic key for devnet + if manifest.Network == netconf.Devnet { + return key.GenerateInsecureDeterministic(manifest.Network, typ, nodeName), nil + } + + // Otherwise generate a proper key + return key.Generate(typ), nil + } + + // Address configured in manifest, download from GCP + return key.Download(ctx, manifest.Network, nodeName, typ, addr) +} + +func publicChains(manifest types.Manifest, cfg DefinitionConfig) ([]types.PublicChain, error) { + var publics []types.PublicChain + for _, name := range manifest.PublicChains { + chain, err := types.PublicChainByName(name) + if err != nil { + return nil, errors.Wrap(err, "get public chain") + } + + addr, ok := cfg.RPCOverrides[name] + if !ok { + addr = types.PublicRPCByName(name) + } + + publics = append(publics, types.NewPublicChain(chain, strings.Split(addr, ","))) + } + + return publics, nil +} + +// externalEndpoints returns the evm rpc endpoints for access from outside the +// docker network. +func externalEndpoints(def Definition) (endpoints map[string]string) { + endpoints = make(map[string]string) + + // Add all public chains + for _, public := range def.Testnet.PublicChains { + endpoints[public.Chain().Name] = public.NextRPCAddress() + } + + // Connect to a proper iliad_evm that isn't unavailable + iliadEVM := def.Testnet.BroadcastIliadEVM() + endpoints[iliadEVM.Chain.Name] = iliadEVM.ExternalRPC + + // Add story consensus chain + endpoints[def.Testnet.Network.Static().IliadConsensusChain().Name] = def.Testnet.BroadcastNode().AddressRPC() + + // Add all anvil chains + for _, anvil := range def.Testnet.AnvilChains { + endpoints[anvil.Chain.Name] = anvil.ExternalRPC + } + + return endpoints +} + +// networkFromDef returns the network configuration from the definition. +func networkFromDef(def Definition) netconf.Network { + var chains []netconf.Chain + + // Add all public chains + for _, public := range def.Testnet.PublicChains { + chains = append(chains, netconf.Chain{ + ID: public.Chain().ChainID, + Name: public.Chain().Name, + BlockPeriod: public.Chain().BlockPeriod, + }) + } + + // Connect to a proper iliad_evm that isn't unavailable + iliadEVM := def.Testnet.BroadcastIliadEVM() + chains = append(chains, netconf.Chain{ + ID: iliadEVM.Chain.ChainID, + Name: iliadEVM.Chain.Name, + BlockPeriod: iliadEVM.Chain.BlockPeriod, + }) + + // Add iliad consensus chain + chains = append(chains, netconf.Chain{ + ID: def.Testnet.Network.Static().IliadConsensusChainIDUint64(), + Name: iliadConsensus, + // No RPC URLs, since we are going to remove it from netconf in any case. + BlockPeriod: iliadEVM.Chain.BlockPeriod, // Same block period as iliadEVM + }) + + // Add all anvil chains + for _, anvil := range def.Testnet.AnvilChains { + chains = append(chains, netconf.Chain{ + ID: anvil.Chain.ChainID, + Name: anvil.Chain.Name, + BlockPeriod: anvil.Chain.BlockPeriod, + }) + } + + for _, chain := range chains { + if netconf.IsIliadConsensus(def.Testnet.Network, chain.ID) { + continue + } + } + + return netconf.Network{ + ID: def.Testnet.Network, + Chains: chains, + } +} + +// iliadEVMByPrefix returns a iliadEVM from the testnet with the given prefix. +// Or a random iliadEVM if prefix is empty. +// Or the only iliadEVM if there is only one. +func iliadEVMByPrefix(testnet types.Testnet, prefix string) types.IliadEVM { + if prefix == "" { + return random(testnet.IliadEVMs) + } else if len(testnet.IliadEVMs) == 1 { + return testnet.IliadEVMs[0] + } + + for _, evm := range testnet.IliadEVMs { + if strings.HasPrefix(evm.InstanceName, prefix) { + return evm + } + } + + panic("evm not found") +} + +// nodeByPrefix returns a iliad node from the testnet with the given prefix. +// Or a random node if prefix is empty. +// Or the only node if there is only one. +// +//nolint:unused // Disable unused lint temporarily. +func nodeByPrefix(testnet types.Testnet, prefix string) *e2e.Node { + if prefix == "" { + return random(testnet.Nodes) + } else if len(testnet.Nodes) == 1 { + return testnet.Nodes[0] + } + + for _, node := range testnet.Nodes { + if strings.HasPrefix(node.Name, prefix) { + return node + } + } + + panic("node not found") +} + +// random returns a random item from a slice. +func random[T any](items []T) T { + return items[int(time.Now().UnixNano())%len(items)] +} + +// lazyNetwork is a lazy network setup that initializes the backends only if required. +// Some e2e commands do not require networking, so this mitigates the need for special networking flags in that case. +type lazyNetwork struct { + once sync.Once + initFunc func() (ethbackend.Backends, error) + backends ethbackend.Backends +} + +func (l *lazyNetwork) Init() error { + var err error + l.once.Do(func() { + l.backends, err = l.initFunc() + }) + + return err +} + +func (l *lazyNetwork) mustInit() { + if err := l.Init(); err != nil { + panic(err) + } +} + +func (l *lazyNetwork) MustBackends() ethbackend.Backends { + l.mustInit() + return l.backends +} diff --git a/e2e/app/eoa/eoa.go b/e2e/app/eoa/eoa.go new file mode 100644 index 00000000..ea804ac1 --- /dev/null +++ b/e2e/app/eoa/eoa.go @@ -0,0 +1,147 @@ +// Package eoa defines well-known (non-fireblocks) eoa private keys used in an iliad network. +package eoa + +import ( + "context" + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/netconf" +) + +type Role string + +const ( + // RoleCreate3Deployer is used to deploy our create3 factories on all chains. This MUST only be done once with nonce 0. + RoleCreate3Deployer Role = "create3-deployer" + // RoleDeployer is used to deploy official iliad contracts on all chains. + RoleDeployer Role = "deployer" + // RoleAdmin is used to manage the iliad contracts on all chains. It has admin privileges on official iliad contracts. + RoleAdmin Role = "admin" + // RoleTester is used for general tasks and testing in non-mainnet networks. + RoleTester Role = "tester" +) + +func AllRoles() []Role { + return []Role{ + RoleCreate3Deployer, + RoleDeployer, + RoleAdmin, + RoleTester, + } +} + +func (r Role) Verify() error { + for _, role := range AllRoles() { + if r == role { + return nil + } + } + + return errors.New("invalid role", "role", r) +} + +type Type string + +const ( + TypeRemote Type = "remote" // stored in (fireblocks) accessible via API to sign + TypeSecret Type = "secret" // stored in GCP can be downloaded to disk + TypeWellKnown Type = "well-known" // well-known eoa private keys in the repo +) + +// Account defines a EOA account used within the iliad network. +type Account struct { + Type Type + Role Role + Address common.Address + privateKey *ecdsa.PrivateKey // only for devnet (well-known type) +} + +// privKey returns the private key for the account. +func (a Account) privKey() *ecdsa.PrivateKey { + return a.privateKey +} + +// MustAddress returns the address for the EOA identified by the network and role. +func MustAddress(network netconf.ID, role Role) common.Address { + resp, ok := Address(network, role) + if !ok { + panic(errors.New("eoa address not defined", "network", network, "role", role)) + } + + return resp +} + +// Address returns the address for the EOA identified by the network and role. +func Address(network netconf.ID, role Role) (common.Address, bool) { + accounts, ok := statics[network] + if !ok { + return common.Address{}, false + } + + for _, account := range accounts { + if account.Role == role { + return account.Address, true + } + } + + return common.Address{}, false +} + +// PrivateKey returns the private key for the EOA identified by the network and role. +func PrivateKey(ctx context.Context, network netconf.ID, role Role) (*ecdsa.PrivateKey, error) { + acc, ok := AccountForRole(network, role) + if !ok { + return nil, errors.New("eoa key not defined", "network", network, "role", role) + } + if acc.Type == TypeWellKnown { + return acc.privKey(), nil + } else if acc.Type == TypeRemote { + return nil, errors.New("private key not available for remote keys", "network", network, "role", role) + } + + k, err := key.Download(ctx, network, string(role), key.EOA, acc.Address.Hex()) + if err != nil { + return nil, errors.Wrap(err, "download key") + } + + return k.ECDSA() +} + +// AccountForRole returns the account for the network and role. +func AccountForRole(network netconf.ID, role Role) (Account, bool) { + accounts, ok := statics[network] + if !ok { + return Account{}, false + } + for _, account := range accounts { + if account.Role == role { + return account, true + } + } + + return Account{}, false +} + +// MustAddresses returns the addresses for the network and roles. +func MustAddresses(network netconf.ID, roles ...Role) []common.Address { + accounts := statics[network] + var addresses []common.Address + for _, role := range roles { + for _, account := range accounts { + if account.Role == role { + addresses = append(addresses, account.Address) + } + } + } + + return addresses +} + +// AllAccounts returns all accounts for the network. +func AllAccounts(network netconf.ID) []Account { + return statics[network] +} diff --git a/e2e/app/eoa/fund.go b/e2e/app/eoa/fund.go new file mode 100644 index 00000000..181bd9fb --- /dev/null +++ b/e2e/app/eoa/fund.go @@ -0,0 +1,87 @@ +//nolint:mnd,unused // suppress linter for this file +package eoa + +import ( + "math/big" + + "cosmossdk.io/math" + + "github.com/ethereum/go-ethereum/params" + + "github.com/piplabs/story/lib/netconf" +) + +var ( + // thresholdTiny is used for EOAs which are rarely used, mostly to deploy a handful of contracts per network. + thresholdTiny = FundThresholds{ + minEther: 0.001, + targetEther: 0.01, + } + + // thresholdSmall is used by EOAs that deploy contracts or perform actions a couple times per week/month. + //nolint:unused // Might be used in the future. + thresholdSmall = FundThresholds{ + minEther: 0.1, + targetEther: 1.0, + } + + // thresholdMedium is used by EOAs that regularly perform actions and need enough balance + // to last a weekend without topping up even if fees are spiking. + thresholdMedium = FundThresholds{ + minEther: 0.5, + targetEther: 5, + } + + // thresholdLarge is used by EOAs that constantly perform actions and need enough balance + // to last a weekend without topping up even if fees are spiking. + thresholdLarge = FundThresholds{ + minEther: 5, + targetEther: 20, // TODO: Increase along with e2e/app#saneMaxEther + } + + defaultThresholdsByRole = map[Role]FundThresholds{ + RoleCreate3Deployer: thresholdTiny, // Only 1 contract per chain + RoleAdmin: thresholdTiny, // Rarely used + RoleDeployer: thresholdTiny, // Protected chains are only deployed once + RoleTester: thresholdLarge, // Tester funds pingpongs, validator updates, etc. + } + + ephemeralOverrides = map[Role]FundThresholds{ + RoleDeployer: thresholdMedium, // Ephemeral chains are deployed often and fees can spike by a lot + } +) + +func GetFundThresholds(network netconf.ID, role Role) (FundThresholds, bool) { + if network.IsEphemeral() { + if resp, ok := ephemeralOverrides[role]; ok { + return resp, true + } + } + + resp, ok := defaultThresholdsByRole[role] + + return resp, ok +} + +type FundThresholds struct { + minEther float64 + targetEther float64 +} + +func (t FundThresholds) MinBalance() *big.Int { + gwei := t.minEther * params.GWei + if gwei < 1 { + panic("ether float64 must be greater than 1 Gwei") + } + + return math.NewInt(params.GWei).MulRaw(int64(gwei)).BigInt() +} + +func (t FundThresholds) TargetBalance() *big.Int { + gwei := t.targetEther * params.GWei + if gwei < 1 { + panic("ether float64 must be greater than 1 Gwei") + } + + return math.NewInt(params.GWei).MulRaw(int64(gwei)).BigInt() +} diff --git a/e2e/app/eoa/helpers.go b/e2e/app/eoa/helpers.go new file mode 100644 index 00000000..acdf534d --- /dev/null +++ b/e2e/app/eoa/helpers.go @@ -0,0 +1,76 @@ +package eoa + +import ( + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" +) + +// Funder returns the address of the funder account. +func Funder() common.Address { + return common.HexToAddress(fbFunder) +} + +func dummy(roles ...Role) []Account { + var resp []Account + for _, role := range roles { + resp = append(resp, Account{ + Type: TypeWellKnown, + Role: role, + Address: common.HexToAddress(ZeroXDead), + }) + } + + return resp +} + +func remote(hex string, roles ...Role) []Account { + var resp []Account + for _, role := range roles { + resp = append(resp, Account{ + Type: TypeRemote, + Role: role, + Address: common.HexToAddress(hex), + }) + } + + return resp +} + +func wellKnown(pk *ecdsa.PrivateKey, roles ...Role) []Account { + var resp []Account + for _, role := range roles { + resp = append(resp, Account{ + Type: TypeWellKnown, + Role: role, + Address: ethcrypto.PubkeyToAddress(pk.PublicKey), + privateKey: pk, + }) + } + + return resp +} + +//nolint:unused // Will be used later +func secret(hex string, roles ...Role) []Account { + var resp []Account + for _, role := range roles { + resp = append(resp, Account{ + Type: TypeSecret, + Role: role, + Address: common.HexToAddress(hex), + }) + } + + return resp +} + +func flatten[T any](slices ...[]T) []T { + var resp []T + for _, slice := range slices { + resp = append(resp, slice...) + } + + return resp +} diff --git a/e2e/app/eoa/static.go b/e2e/app/eoa/static.go new file mode 100644 index 00000000..9440fe2b --- /dev/null +++ b/e2e/app/eoa/static.go @@ -0,0 +1,31 @@ +package eoa + +import ( + "github.com/piplabs/story/lib/anvil" + "github.com/piplabs/story/lib/netconf" +) + +const ( + // fbFunder is the address of the fireblocks "funder" account. + fbFunder = "0xf63316AA39fEc9D2109AB0D9c7B1eE3a6F60AEA4" + fbDev = "0x7a6cF389082dc698285474976d7C75CAdE08ab7e" + ZeroXDead = "0x000000000000000000000000000000000000dead" +) + +//nolint:gochecknoglobals // Static mappings. +var statics = map[netconf.ID][]Account{ + netconf.Devnet: flatten( + wellKnown(anvil.DevPrivateKey0(), RoleTester, RoleCreate3Deployer, RoleDeployer), + ), + netconf.Staging: flatten( + remote("0xC8103859Ac7CB547d70307EdeF1A2319FC305fdC", RoleCreate3Deployer), + remote("0x274c4B3e5d27A65196d63964532366872F81D261", RoleDeployer), + ), + netconf.Testnet: flatten( + remote("0xeC5134556da0797A5C5cD51DD622b689Cac97Fe9", RoleCreate3Deployer), + remote("0x0CdCc644158b7D03f40197f55454dc7a11Bd92c1", RoleDeployer), + ), + netconf.Mainnet: flatten( + dummy(RoleAdmin, RoleCreate3Deployer, RoleDeployer, RoleTester), + ), +} diff --git a/e2e/app/fund.go b/e2e/app/fund.go new file mode 100644 index 00000000..a236c1b8 --- /dev/null +++ b/e2e/app/fund.go @@ -0,0 +1,186 @@ +package app + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + + "github.com/piplabs/story/e2e/app/eoa" + "github.com/piplabs/story/lib/anvil" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/txmgr" +) + +const saneMaxEther = 20 // Maximum amount to fund in ether. // TODO(corver): Increase this. + +// noAnvilDev returns a list of accounts that are not dev anvil accounts. +func noAnvilDev(accounts []common.Address) []common.Address { + var nonDevAccounts []common.Address + for _, account := range accounts { + if !anvil.IsDevAccount(account) { + nonDevAccounts = append(nonDevAccounts, account) + } + } + + return nonDevAccounts +} + +// accountsToFund returns a list of accounts to fund on anvil chains, based on the network. +func accountsToFund(network netconf.ID) []common.Address { + switch network { + case netconf.Staging: + return eoa.MustAddresses(netconf.Staging, eoa.AllRoles()...) + case netconf.Devnet: + return eoa.MustAddresses(netconf.Devnet, eoa.AllRoles()...) + default: + return []common.Address{} + } +} + +// fundAccounts funds the EOAs that need funding (just on anvil chains, for now). +func fundAccounts(ctx context.Context, def Definition) error { + accounts := accountsToFund(def.Testnet.Network) + eth100 := new(big.Int).Mul(big.NewInt(params.Ether), big.NewInt(100)) + for _, chain := range def.Testnet.AnvilChains { + if err := anvil.FundAccounts(ctx, chain.ExternalRPC, eth100, noAnvilDev(accounts)...); err != nil { + return errors.Wrap(err, "fund anvil account") + } + } + + return nil +} + +// FundEOAAccounts funds the EOAs that need funding to their target balance. +func FundEOAAccounts(ctx context.Context, def Definition, dryRun bool) error { + if def.Testnet.Network == netconf.Mainnet { + return errors.New("mainnet funding not supported yet") + } + + network := networkFromDef(def) + accounts := eoa.AllAccounts(network.ID) + + log.Info(ctx, "Checking accounts to fund", "network", network.ID, "count", len(accounts)) + + for _, chain := range network.EVMChains() { + backend, err := def.Backends().Backend(chain.ID) + if err != nil { + return errors.Wrap(err, "backend") + } + + funder := eoa.Funder() + funderBal, err := backend.BalanceAt(ctx, funder, nil) + if err != nil { + return err + } + + log.Info(ctx, "Funder balance", + "chain", chain.Name, + "funder", funder, + "balance", etherStr(funderBal), + ) + + for _, account := range accounts { + accCtx := log.WithCtx(ctx, + "chain", chain.Name, + "role", account.Role, + "address", account.Address, + "type", account.Type, + ) + + if account.Address == common.HexToAddress(eoa.ZeroXDead) { + log.Info(accCtx, "Skipping 0xdead account") + continue + } else if account.Type == eoa.TypeWellKnown { + log.Info(accCtx, "Skipping well-known anvil account") + continue + } + + thresholds, ok := eoa.GetFundThresholds(network.ID, account.Role) + if !ok { + log.Warn(accCtx, "Skipping account without fund thresholds", nil) + continue + } + + balance, err := backend.BalanceAt(accCtx, account.Address, nil) + if err != nil { + log.Warn(accCtx, "Failed fetching balance, skipping", err) + continue + } else if thresholds.MinBalance().Cmp(balance) < 0 { + log.Info(accCtx, + "Not funding account, balance sufficient", + "balance", etherStr(balance), + "min_balance", etherStr(thresholds.MinBalance()), + ) + + continue + } + + saneMax := new(big.Int).Mul(big.NewInt(saneMaxEther), big.NewInt(params.Ether)) + + amount := new(big.Int).Sub(thresholds.TargetBalance(), balance) + if amount.Cmp(big.NewInt(0)) <= 0 { + return errors.New("unexpected negative amount [BUG]") // Target balance below minimum balance + } else if amount.Cmp(saneMax) > 0 { + log.Warn(accCtx, "Funding amount exceeds sane max, skipping", nil, + "amount", etherStr(amount), + "max", etherStr(saneMax), + ) + + continue + } else if amount.Cmp(funderBal) >= 0 { + return errors.New("funder balance too low", + "amount", etherStr(amount), + "funder", etherStr(funderBal), + ) + } + + log.Info(accCtx, "Funding account", + "amount", etherStr(amount), + "balance", etherStr(balance), + "target_balance", etherStr(thresholds.TargetBalance()), + ) + + if dryRun { + log.Warn(accCtx, "Skipping actual funding tx due to dry-run", nil) + continue + } + + tx, rec, err := backend.Send(accCtx, eoa.Funder(), txmgr.TxCandidate{ + To: &account.Address, + GasLimit: 0, + Value: amount, + }) + if err != nil { + return errors.Wrap(err, "send tx") + } else if rec.Status != types.ReceiptStatusSuccessful { + return errors.New("funding tx failed", "tx", tx.Hash()) + } + + b, err := backend.BalanceAt(accCtx, account.Address, nil) + if err != nil { + return errors.Wrap(err, "get balance") + } + + log.Info(accCtx, "Account funded šŸŽ‰", + "amount_funded", etherStr(amount), + "resulting_balance", etherStr(b), + "tx", tx.Hash().Hex(), + ) + } + } + + return nil +} + +func etherStr(amount *big.Int) string { + b, _ := amount.Float64() + b /= params.Ether + + return fmt.Sprintf("%.4f", b) +} diff --git a/e2e/app/geth/config.go b/e2e/app/geth/config.go new file mode 100644 index 00000000..e91a641d --- /dev/null +++ b/e2e/app/geth/config.go @@ -0,0 +1,127 @@ +package geth + +import ( + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "time" + + "github.com/ethereum/go-ethereum/core" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/downloader" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" +) + +// WriteAllConfig writes all the geth config files for all iliadEVMs. +func WriteAllConfig(testnet types.Testnet, genesis core.Genesis) error { + gethGenesisBz, err := json.MarshalIndent(genesis, "", " ") + if err != nil { + return errors.Wrap(err, "marshal genesis") + } + + gethConfigFiles := func(evm types.IliadEVM) map[string][]byte { + return map[string][]byte{ + "genesis.json": gethGenesisBz, + "geth/nodekey": []byte(hex.EncodeToString(ethcrypto.FromECDSA(evm.NodeKey))), // Nodekey is hex encoded + "geth/jwtsecret": []byte(evm.JWTSecret), + } + } + + for _, evm := range testnet.IliadEVMs { + for file, data := range gethConfigFiles(evm) { + path := filepath.Join(testnet.Dir, evm.InstanceName, file) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return errors.Wrap(err, "mkdir", "path", path) + } + if err := os.WriteFile(path, data, 0o644); err != nil { + return errors.Wrap(err, "write geth config") + } + } + + conf := Config{ + Moniker: evm.InstanceName, + IsArchive: evm.IsArchive, + ChainID: evm.Chain.ChainID, + BootNodes: evm.Peers, // TODO: Use seed nodes once available. + TrustedNodes: evm.Peers, + } + if err := WriteConfigTOML(conf, filepath.Join(testnet.Dir, evm.InstanceName, "config.toml")); err != nil { + return errors.Wrap(err, "write geth config") + } + } + + return nil +} + +// WriteConfigTOML writes the geth config to a toml file. +func WriteConfigTOML(conf Config, path string) error { + bz, err := tomlSettings.Marshal(MakeGethConfig(conf)) + if err != nil { + return errors.Wrap(err, "marshal toml") + } + + if err := os.WriteFile(path, bz, 0o644); err != nil { + return errors.Wrap(err, "write toml") + } + + return nil +} + +// MakeGethConfig returns the full iliad geth config for the provided custom config. +func MakeGethConfig(conf Config) FullConfig { + cfg := defaultGethConfig() + cfg.Eth.NetworkId = conf.ChainID + cfg.Node.DataDir = "/geth" // Mount inside docker container + cfg.Node.IPCPath = "/geth/geth.ipc" + + // Use syncmode=full. Since default "snap" sync has race condition on startup. Where engineAPI newPayload fails + // if snapsync has not completed. Should probably wait for snapsync to complete before starting engineAPI? + cfg.Eth.SyncMode = downloader.FullSync + + // Disable pruning for archive nodes. + // Note that runtime flags are also required for archive nodes, specifically: + // --gcmode==archive + // --state.scheme=hash + // This will be deprecated once new state.scheme=path support archive nodes. + // See https://blog.ethereum.org/2023/09/12/geth-v1-13-0. + cfg.Eth.NoPruning = conf.IsArchive + cfg.Eth.Preimages = conf.IsArchive // Geth auto-enables this when NoPruning is set. + + // Ethereum has slow block building times (2~4s), but we need fast times (<1s). + // Use 500ms so blocks are built in less than 1s. + cfg.Eth.Miner.Recommit = 500 * time.Millisecond + + // Set the bootnodes and trusted nodes. + cfg.Node.P2P.Name = conf.Moniker + cfg.Node.P2P.DiscoveryV4 = true // TODO: Switch to v5. + cfg.Node.P2P.BootstrapNodesV5 = conf.BootNodes + cfg.Node.P2P.BootstrapNodes = conf.BootNodes + cfg.Node.P2P.TrustedNodes = conf.TrustedNodes + + // Bind listen addresses to all interfaces inside the container. + const allInterfaces = "0.0.0.0" + cfg.Node.AuthAddr = allInterfaces + cfg.Node.HTTPHost = allInterfaces + cfg.Node.WSHost = allInterfaces + cfg.Node.P2P.ListenAddr = allInterfaces + ":30303" + + // Add eth module + cfg.Node.HTTPModules = append(cfg.Node.HTTPModules, "eth") + cfg.Node.WSModules = append(cfg.Node.WSModules, "eth") + + if conf.IsArchive { + cfg.Node.HTTPModules = append(cfg.Node.HTTPModules, "debug") + cfg.Node.WSModules = append(cfg.Node.WSModules, "debug") + } + + // Allow all incoming connections. + cfg.Node.HTTPVirtualHosts = []string{"*"} + cfg.Node.AuthVirtualHosts = []string{"*"} + cfg.Node.WSOrigins = []string{"*"} + cfg.Node.HTTPCors = []string{"*"} + + return cfg +} diff --git a/e2e/app/geth/config_internal_test.go b/e2e/app/geth/config_internal_test.go new file mode 100644 index 00000000..293493c0 --- /dev/null +++ b/e2e/app/geth/config_internal_test.go @@ -0,0 +1,105 @@ +package geth + +import ( + "encoding/json" + "net" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test . -golden -clean + +func TestWriteConfigTOML(t *testing.T) { + t.Parallel() + + testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8") + node1 := enode.NewV4(&testKey.PublicKey, net.IP{127, 0, 0, 1}, 1, 1) + node2 := enode.NewV4(&testKey.PublicKey, net.IP{127, 0, 0, 2}, 2, 2) + + tests := map[string]bool{ + "archive": true, + "full": false, + } + for name, isArchive := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + data := Config{ + Moniker: name, + BootNodes: []*enode.Node{node1}, + TrustedNodes: []*enode.Node{node1, node2}, + ChainID: 15651, + IsArchive: isArchive, + } + + tempFile := filepath.Join(t.TempDir(), name+".toml") + + err := WriteConfigTOML(data, tempFile) + require.NoError(t, err) + + bz, err := os.ReadFile(tempFile) + require.NoError(t, err) + + tutil.RequireGoldenBytes(t, bz) + + // Compare our generated config against the output of `geth dumpconfig` with this as the base config. + // Geth does some custom config parsing/sanitizing/updating of the config, so we ensure our config doesn't + // get silently updated by geth. + // See https://github.com/ethereum/go-ethereum/blob/master/cmd/utils/flags.go#L1640 + // result := gethDumpConfigToml(t, MakeGethConfig(data)) + // require.Equal(t, string(bz), string(result)) + }) + } +} + +// TestGethVersion checks if the geth version is up to date. +func TestGethVersion(t *testing.T) { + t.Parallel() + + out, err := exec.Command("go", "list", "-json", "github.com/ethereum/go-ethereum").CombinedOutput() + require.NoError(t, err) + + resp := struct { + Module struct { + Version string `json:"version"` + } `json:"module"` + }{} + err = json.Unmarshal(out, &resp) + require.NoError(t, err) + + require.Equal(t, Version, resp.Module.Version, "A different geth has been released, update `geth.Version`") +} + +// gethDumpConfigToml executes `geth dumpconfig` using the provided base config and +// returns the resulting toml config file content. +// func gethDumpConfigToml(t *testing.T, baseCfg FullConfig) []byte { +// t.Helper() + +// bz, err := tomlSettings.Marshal(baseCfg) +// require.NoError(t, err) + +// baseFile := filepath.Join(t.TempDir(), "base.toml") +// err = os.WriteFile(baseFile, bz, 0o644) +// require.NoError(t, err) + +// var stdout, stderr bytes.Buffer +// cmd := exec.Command("docker", "run", +// fmt.Sprintf("--volume=%s:/tmp/config.toml", baseFile), +// fmt.Sprintf("ethereum/client-go:%s", Version), +// "dumpconfig", +// "--config=/tmp/config.toml") +// cmd.Stdout = &stdout +// cmd.Stderr = &stderr + +// t.Logf("geth dumpconfig logs:\n%s", stderr.String()) + +// return stdout.Bytes() +// } diff --git a/e2e/app/geth/testdata/TestWriteConfigTOML_archive.golden b/e2e/app/geth/testdata/TestWriteConfigTOML_archive.golden new file mode 100644 index 00000000..95d50e06 --- /dev/null +++ b/e2e/app/geth/testdata/TestWriteConfigTOML_archive.golden @@ -0,0 +1,105 @@ +[Eth] +NetworkId = 15651 +SyncMode = "full" +EthDiscoveryURLs = [] +SnapDiscoveryURLs = [] +NoPruning = true +NoPrefetch = false +TxLookupLimit = 2350000 +TransactionHistory = 2350000 +StateHistory = 90000 +LightPeers = 100 +DatabaseCache = 512 +DatabaseFreezer = "" +TrieCleanCache = 154 +TrieDirtyCache = 256 +TrieTimeout = 3600000000000 +SnapshotCache = 102 +Preimages = true +FilterLogCacheSize = 32 +EnablePreimageRecording = false +VMTrace = "" +VMTraceJsonConfig = "" +RPCGasCap = 50000000 +RPCEVMTimeout = 5000000000 +RPCTxFeeCap = 1e+00 + +[Eth.Miner] +GasCeil = 30000000 +GasPrice = 1000000 +Recommit = 500000000 + +[Eth.TxPool] +Locals = [] +NoLocals = false +Journal = "transactions.rlp" +Rejournal = 3600000000000 +PriceLimit = 1 +PriceBump = 10 +AccountSlots = 16 +GlobalSlots = 5120 +AccountQueue = 64 +GlobalQueue = 1024 +Lifetime = 10800000000000 + +[Eth.BlobPool] +Datadir = "blobpool" +Datacap = 2684354560 +PriceBump = 100 + +[Eth.GPO] +Blocks = 20 +Percentile = 60 +MaxHeaderHistory = 1024 +MaxBlockHistory = 1024 +MaxPrice = 500000000000 +IgnorePrice = 2 + +[Node] +DataDir = "/geth" +IPCPath = "/geth/geth.ipc" +HTTPHost = "0.0.0.0" +HTTPPort = 8545 +HTTPCors = ["*"] +HTTPVirtualHosts = ["*"] +HTTPModules = ["net", "web3", "eth", "debug"] +AuthAddr = "0.0.0.0" +AuthPort = 8551 +AuthVirtualHosts = ["*"] +WSHost = "0.0.0.0" +WSPort = 8546 +WSOrigins = ["*"] +WSModules = ["net", "web3", "eth", "debug"] +GraphQLVirtualHosts = ["localhost"] +BatchRequestLimit = 1000 +BatchResponseMaxSize = 25000000 + +[Node.P2P] +MaxPeers = 50 +NoDiscovery = false +DiscoveryV4 = true +BootstrapNodes = ["enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.1:1"] +BootstrapNodesV5 = ["enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.1:1"] +StaticNodes = [] +TrustedNodes = ["enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.1:1", "enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.2:2"] +ListenAddr = "0.0.0.0:30303" +DiscAddr = "" +EnableMsgEvents = false + +[Node.HTTPTimeouts] +ReadTimeout = 30000000000 +ReadHeaderTimeout = 30000000000 +WriteTimeout = 30000000000 +IdleTimeout = 120000000000 + +[Metrics] +HTTP = "127.0.0.1" +Port = 6060 +InfluxDBEndpoint = "http://localhost:8086" +InfluxDBDatabase = "geth" +InfluxDBUsername = "test" +InfluxDBPassword = "test" +InfluxDBTags = "host=localhost" +InfluxDBToken = "test" +InfluxDBBucket = "geth" +InfluxDBOrganization = "geth" diff --git a/e2e/app/geth/testdata/TestWriteConfigTOML_full.golden b/e2e/app/geth/testdata/TestWriteConfigTOML_full.golden new file mode 100644 index 00000000..9f30dc73 --- /dev/null +++ b/e2e/app/geth/testdata/TestWriteConfigTOML_full.golden @@ -0,0 +1,105 @@ +[Eth] +NetworkId = 15651 +SyncMode = "full" +EthDiscoveryURLs = [] +SnapDiscoveryURLs = [] +NoPruning = false +NoPrefetch = false +TxLookupLimit = 2350000 +TransactionHistory = 2350000 +StateHistory = 90000 +LightPeers = 100 +DatabaseCache = 512 +DatabaseFreezer = "" +TrieCleanCache = 154 +TrieDirtyCache = 256 +TrieTimeout = 3600000000000 +SnapshotCache = 102 +Preimages = false +FilterLogCacheSize = 32 +EnablePreimageRecording = false +VMTrace = "" +VMTraceJsonConfig = "" +RPCGasCap = 50000000 +RPCEVMTimeout = 5000000000 +RPCTxFeeCap = 1e+00 + +[Eth.Miner] +GasCeil = 30000000 +GasPrice = 1000000 +Recommit = 500000000 + +[Eth.TxPool] +Locals = [] +NoLocals = false +Journal = "transactions.rlp" +Rejournal = 3600000000000 +PriceLimit = 1 +PriceBump = 10 +AccountSlots = 16 +GlobalSlots = 5120 +AccountQueue = 64 +GlobalQueue = 1024 +Lifetime = 10800000000000 + +[Eth.BlobPool] +Datadir = "blobpool" +Datacap = 2684354560 +PriceBump = 100 + +[Eth.GPO] +Blocks = 20 +Percentile = 60 +MaxHeaderHistory = 1024 +MaxBlockHistory = 1024 +MaxPrice = 500000000000 +IgnorePrice = 2 + +[Node] +DataDir = "/geth" +IPCPath = "/geth/geth.ipc" +HTTPHost = "0.0.0.0" +HTTPPort = 8545 +HTTPCors = ["*"] +HTTPVirtualHosts = ["*"] +HTTPModules = ["net", "web3", "eth"] +AuthAddr = "0.0.0.0" +AuthPort = 8551 +AuthVirtualHosts = ["*"] +WSHost = "0.0.0.0" +WSPort = 8546 +WSOrigins = ["*"] +WSModules = ["net", "web3", "eth"] +GraphQLVirtualHosts = ["localhost"] +BatchRequestLimit = 1000 +BatchResponseMaxSize = 25000000 + +[Node.P2P] +MaxPeers = 50 +NoDiscovery = false +DiscoveryV4 = true +BootstrapNodes = ["enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.1:1"] +BootstrapNodesV5 = ["enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.1:1"] +StaticNodes = [] +TrustedNodes = ["enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.1:1", "enode://3a514176466fa815ed481ffad09110a2d344f6c9b78c1d14afc351c3a51be33d8072e77939dc03ba44790779b7a1025baf3003f6732430e20cd9b76d953391b3@127.0.0.2:2"] +ListenAddr = "0.0.0.0:30303" +DiscAddr = "" +EnableMsgEvents = false + +[Node.HTTPTimeouts] +ReadTimeout = 30000000000 +ReadHeaderTimeout = 30000000000 +WriteTimeout = 30000000000 +IdleTimeout = 120000000000 + +[Metrics] +HTTP = "127.0.0.1" +Port = 6060 +InfluxDBEndpoint = "http://localhost:8086" +InfluxDBDatabase = "geth" +InfluxDBUsername = "test" +InfluxDBPassword = "test" +InfluxDBTags = "host=localhost" +InfluxDBToken = "test" +InfluxDBBucket = "geth" +InfluxDBOrganization = "geth" diff --git a/e2e/app/geth/types.go b/e2e/app/geth/types.go new file mode 100644 index 00000000..5d7228ef --- /dev/null +++ b/e2e/app/geth/types.go @@ -0,0 +1,61 @@ +package geth + +import ( + "reflect" + + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/naoina/toml" + + "github.com/piplabs/story/lib/errors" +) + +// Version defines the geth version deployed to all networks. +const Version = "v1.14.5" + +// Config is the configurable options for the standard iliad geth config. +type Config struct { + // Moniker is the p2p node name. + Moniker string + // ChainID is the chain ID of the network. + ChainID uint64 + // IsArchive defines whether the node should run in archive mode. + IsArchive bool + // BootNodes are the enode URLs of the P2P bootstrap nodes. + BootNodes []*enode.Node + // TrustedNodes are the enode URLs of the P2P trusted nodes. + TrustedNodes []*enode.Node +} + +// defaultGethConfig returns the default geth config. +func defaultGethConfig() FullConfig { + return FullConfig{ + Eth: ethconfig.Defaults, + Node: node.DefaultConfig, + Metrics: metrics.DefaultConfig, // Enable prometheus metrics via command line flags --metrics --pprof --pprof.addr=0.0.0.0 + } +} + +// FullConfig is the go struct representation of the geth.toml config file. +// Copied from https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/config.go#L95 +type FullConfig struct { + Eth ethconfig.Config + Node node.Config + Metrics metrics.Config +} + +// tomlSettings is the toml settings used to parse/format the geth.toml config file. +// Copied from https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/config.go#L70. +var tomlSettings = toml.Config{ + NormFieldName: func(_ reflect.Type, key string) string { + return key + }, + FieldToKey: func(_ reflect.Type, field string) string { + return field + }, + MissingField: func(rt reflect.Type, field string) error { + return errors.New("field not defined", "field", field, "type", rt.String()) + }, +} diff --git a/e2e/app/key/gcp.go b/e2e/app/key/gcp.go new file mode 100644 index 00000000..4d74d241 --- /dev/null +++ b/e2e/app/key/gcp.go @@ -0,0 +1,71 @@ +package key + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "os/exec" + + "github.com/piplabs/story/lib/errors" +) + +// createGCPSecret creates a new GCP Secret Manager secret. +func createGCPSecret(ctx context.Context, name string, value []byte, labels map[string]string) error { + // Init the gcloud command to create a secret + cmd := exec.CommandContext(ctx, "gcloud", "secrets", "create", name, "--data-file=-") + + // Add labels to the command + for k, v := range labels { + cmd.Args = append(cmd.Args, "--labels="+k+"="+v) + } + + // Set the secret value as the input for the command + cmd.Stdin = bytes.NewReader(value) + + // Execute the command + out, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrap(err, "gcloud create secret", "out", string(out)) + } + + return nil +} + +// getGCPSecret fetches the latest version of the specified secret. +func getGCPSecret(ctx context.Context, name string) ([]byte, error) { + // Init the gcloud command to access the secret + cmd := exec.CommandContext(ctx, "gcloud", "secrets", "versions", "access", "latest", "--secret", name, "--format=json") + + var stdOut, stdErr bytes.Buffer + cmd.Stderr = &stdErr + cmd.Stdout = &stdOut + + // Execute the command and capture the output + if err := cmd.Run(); err != nil { + return nil, errors.Wrap(err, "gcloud fetch secret", "out", stdErr.String()) + } + + // Unmarshal the json response + var resp response + if err := json.Unmarshal(stdOut.Bytes(), &resp); err != nil { + return nil, errors.Wrap(err, "unmarshal secret response") + } + + // Decode the base64 encoded secret data + bz, err := base64.URLEncoding.DecodeString(resp.Payload.DataBase64) + if err != nil { + return nil, errors.Wrap(err, "decode secret data") + } + + return bz, nil +} + +// response from gloud secret manager --format=json. +type response struct { + Name string `json:"name"` + Payload struct { + DataBase64 string `json:"data"` + DataCRC32c string `json:"dataCrc32c"` + } `json:"payload"` +} diff --git a/e2e/app/key/key.go b/e2e/app/key/key.go new file mode 100644 index 00000000..2a5e21d4 --- /dev/null +++ b/e2e/app/key/key.go @@ -0,0 +1,144 @@ +package key + +import ( + "crypto/ecdsa" + + "github.com/cometbft/cometbft/crypto" + ed "github.com/cometbft/cometbft/crypto/ed25519" + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/netconf" +) + +// Type represents the type of cryptographic key. +type Type string + +const ( + Validator Type = "validator" + P2PConsensus Type = "p2p_consensus" + P2PExecution Type = "p2p_execution" + EOA Type = "eoa" +) + +func (t Type) Verify() error { + if t == "" { + return errors.New("empty key type") + } + switch t { + case Validator, P2PConsensus, P2PExecution, EOA: + return nil + default: + return errors.New("invalid key type") + } +} + +func (t Type) String() string { + return string(t) +} + +// Key wraps a cometBFT private key adding custom a address function. +type Key struct { + crypto.PrivKey +} + +// Addr returns the address of the key. +// K1 keys have ethereum 0x addresses, +// ED keys have the default SHA256-20 of the raw pubkey bytes. +func (k Key) Addr() (string, error) { + switch t := k.PrivKey.(type) { + case k1.PrivKey: + addr, err := k1util.PubKeyToAddress(t.PubKey()) + if err != nil { + return "", err + } + + return addr.Hex(), nil + case ed.PrivKey: + return t.PubKey().Address().String(), nil + default: + return "", errors.New("unknown key type") + } +} + +// ECDSA returns the ECDSA representation of the k1 key. +// This returns an error for ed25519 keys. +func (k Key) ECDSA() (*ecdsa.PrivateKey, error) { + switch t := k.PrivKey.(type) { + case k1.PrivKey: + resp, err := ethcrypto.ToECDSA(t.Bytes()) + if err != nil { + return nil, errors.Wrap(err, "converting to ECDSA") + } + + return resp, nil + default: + return nil, errors.New("ed25519 keys do not have ECDSA representation") + } +} + +// Generate generates a cryptographic key of the specified type. +// It panics since it assumes that the type is valid. +func Generate(typ Type) Key { + switch typ { + case Validator, P2PExecution, EOA: + return Key{ + PrivKey: k1.GenPrivKey(), + } + + case P2PConsensus: + return Key{ + PrivKey: ed.GenPrivKey(), + } + default: + panic("invalid key type:" + typ) + } +} + +// GenerateInsecureDeterministic generates an insecure deterministic key of the specified type +// from the provided seed. +// NOTE THIS MUST ONLY BE USED FOR TESTING: It panics if network is not ephemeral. +func GenerateInsecureDeterministic(network netconf.ID, typ Type, seed string) Key { + if !network.IsEphemeral() { + panic("only ephemeral keys are supported") + } + + secret := []byte(string(typ) + "|" + seed) // Deterministic secret from typ+seed. + + switch typ { + case Validator, P2PExecution, EOA: + return Key{ + PrivKey: k1.GenPrivKeySecp256k1(secret), + } + + case P2PConsensus: + return Key{ + PrivKey: ed.GenPrivKeyFromSecret(secret), + } + default: + panic("invalid key type:" + typ) + } +} + +// FromBytes parses the given bytes into th eprovided key type. +func FromBytes(typ Type, b []byte) (Key, error) { + switch typ { + case Validator, P2PExecution, EOA: + if len(b) != k1.PrivKeySize { + return Key{}, errors.New("invalid key size") + } + + return Key{PrivKey: k1.PrivKey(b)}, nil + case P2PConsensus: + + if len(b) != ed.PrivateKeySize { + return Key{}, errors.New("invalid key size") + } + + return Key{PrivKey: ed.PrivKey(b)}, nil + default: + return Key{}, errors.New("invalid key type") + } +} diff --git a/e2e/app/key/key_internal_test.go b/e2e/app/key/key_internal_test.go new file mode 100644 index 00000000..1f1bc711 --- /dev/null +++ b/e2e/app/key/key_internal_test.go @@ -0,0 +1,19 @@ +package key + +import ( + "context" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/netconf" +) + +func DeleteSecretForT(ctx context.Context, t *testing.T, network netconf.ID, name string, typ Type, addr string) { + t.Helper() + secret := secretName(network, name, typ, addr) + + out, err := exec.CommandContext(ctx, "gcloud", "secrets", "delete", secret, "--quiet").CombinedOutput() + require.NoError(t, err, string(out)) +} diff --git a/e2e/app/key/key_test.go b/e2e/app/key/key_test.go new file mode 100644 index 00000000..02c838d9 --- /dev/null +++ b/e2e/app/key/key_test.go @@ -0,0 +1,100 @@ +package key_test + +import ( + "context" + "flag" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" +) + +func TestKeys(t *testing.T) { + t.Parallel() + for _, typ := range []key.Type{key.Validator, key.P2PConsensus, key.P2PExecution, key.EOA} { + t.Run(typ.String(), func(t *testing.T) { + t.Parallel() + + key1 := key.Generate(typ) + + addrA, err := key1.Addr() + require.NoError(t, err) + + key2, err := key.FromBytes(typ, key1.Bytes()) + require.NoError(t, err) + + addrB, err := key2.Addr() + require.NoError(t, err) + require.Equal(t, addrA, addrB) + + require.True(t, key1.Equals(key2.PrivKey)) + + ecdsaKey, err := key1.ECDSA() + switch typ { + case key.Validator, key.P2PExecution, key.EOA: + require.NoError(t, err) + addrC := crypto.PubkeyToAddress(ecdsaKey.PublicKey) + require.Equal(t, addrA, addrC.Hex()) + case key.P2PConsensus: + require.Error(t, err) + } + }) + } +} + +var integration = flag.Bool("integration", false, "run integration tests") + +//go:generate go test . -integration -run=TestIntegration -v + +func TestIntegration(t *testing.T) { + t.Parallel() + if !*integration { + t.Skip("skipping integration tests") + } + + for _, typ := range []key.Type{key.Validator, key.P2PConsensus, key.P2PExecution} { + t.Run(typ.String(), func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + network := netconf.Simnet + name := "deleteme" + + k, err := key.UploadNew(ctx, key.UploadConfig{ + Network: network, + Name: name, + Type: typ, + }) + require.NoError(t, err) + + addr, err := k.Addr() + require.NoError(t, err) + + k2, err := key.Download(ctx, network, name, typ, addr) + tutil.RequireNoError(t, err) + + require.True(t, k.Equals(k2.PrivKey)) + + key.DeleteSecretForT(ctx, t, network, name, typ, addr) + }) + } +} + +func TestGenInsecure(t *testing.T) { + t.Parallel() + + require.Panics(t, func() { + key.GenerateInsecureDeterministic(netconf.Testnet, key.EOA, "") + }) + + k1 := key.GenerateInsecureDeterministic(netconf.Devnet, key.EOA, "test1") + require.Equal(t, "d4c14667192a5966002361dd08cdd30619ac553928c423593d744dbb50ff232a", fmt.Sprintf("%x", k1.Bytes())) + + k2 := key.GenerateInsecureDeterministic(netconf.Devnet, key.P2PConsensus, "test2") + require.Equal(t, "4cf65aca4b199f5084b217b0728355b5ee93355c3f18324436b856337d60c07300e8fb91b5af3bc124e1b0f382a88377cb126b0989c5282959631596e4f8bc0b", fmt.Sprintf("%x", k2.Bytes())) +} diff --git a/e2e/app/key/upload.go b/e2e/app/key/upload.go new file mode 100644 index 00000000..bd4086b4 --- /dev/null +++ b/e2e/app/key/upload.go @@ -0,0 +1,71 @@ +package key + +import ( + "context" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" +) + +// UploadConfig is the configuration for uploading a key. +type UploadConfig struct { + Network netconf.ID + Name string + Type Type +} + +// UploadNew generates a new key and uploads it to the gcp secret manager. +func UploadNew(ctx context.Context, cfg UploadConfig) (Key, error) { + if err := cfg.Network.Verify(); err != nil { + return Key{}, err + } + if err := cfg.Type.Verify(); err != nil { + return Key{}, err + } + + k := Generate(cfg.Type) + + addr, err := k.Addr() + if err != nil { + return Key{}, err + } + + // TODO: Delete or overwrite existing key with matching labels. + + secret := secretName(cfg.Network, cfg.Name, cfg.Type, addr) + if err := createGCPSecret(ctx, secret, k.Bytes(), nil); err != nil { + return Key{}, errors.Wrap(err, "upload key") + } + + log.Info(ctx, "šŸ” Key uploaded: "+secret, "address", addr, "type", cfg.Type, "network", cfg.Network, "secret", cfg.Name) + + return k, nil +} + +// Download retrieves a key from the gcp secret manager. +func Download(ctx context.Context, network netconf.ID, name string, typ Type, addr string) (Key, error) { + bz, err := getGCPSecret(ctx, secretName(network, name, typ, addr)) + if err != nil { + return Key{}, errors.Wrap(err, "download key", "network", network, "name", name, "type", typ, "addr", addr) + } + + k, err := FromBytes(typ, bz) + if err != nil { + return Key{}, err + } + + actualAddr, err := k.Addr() + if err != nil { + return Key{}, err + } else if actualAddr != addr { + return Key{}, errors.New("unexpected key address") + } + + return k, nil +} + +// secretName returns the name of the secret in the gcp secret manager. +func secretName(network netconf.ID, name string, typ Type, addr string) string { + return network.String() + "-" + name + "-" + typ.String() + "-" + addr +} diff --git a/e2e/app/perturb.go b/e2e/app/perturb.go new file mode 100644 index 00000000..23a26d07 --- /dev/null +++ b/e2e/app/perturb.go @@ -0,0 +1,126 @@ +package app + +import ( + "context" + "time" + + rpctypes "github.com/cometbft/cometbft/rpc/core/types" + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/cometbft/cometbft/test/e2e/pkg/infra/docker" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// perturb the running testnet. +func perturb(ctx context.Context, testnet types.Testnet) error { + for _, node := range testnet.Nodes { + for _, perturbation := range node.Perturbations { + _, err := perturbNode(ctx, node, perturbation) + if err != nil { + return err + } + time.Sleep(3 * time.Second) // Give network some time to recover between each + } + } + + for service, purturbs := range testnet.Perturb { + for _, p := range purturbs { + if err := perturbService(ctx, service, testnet.Dir, p); err != nil { + return errors.Wrap(err, "purturb service", "service", service) + } + time.Sleep(3 * time.Second) // Give network some time to recover between each + } + } + + return nil +} + +// perturbService perturbs a docker service with a given perturbation. +func perturbService(ctx context.Context, service string, testnetDir string, perturb types.Perturb) error { + ctx = log.WithCtx(ctx, "service", service) + + log.Info(ctx, "Perturbing service", "perturb", perturb) + switch perturb { + case types.PerturbRestart: + if err := docker.ExecCompose(ctx, testnetDir, "restart", service); err != nil { + return errors.Wrap(err, "restart service") + } + case types.PerturbStopStart: + if err := docker.ExecCompose(ctx, testnetDir, "stop", service); err != nil { + return errors.Wrap(err, "stop service") + } + time.Sleep(5 * time.Second) + if err := docker.ExecCompose(ctx, testnetDir, "start", service); err != nil { + return errors.Wrap(err, "start service") + } + default: + return errors.New("unknown service perturbation") + } + + log.Info(ctx, "Perturbed service", "perturb", perturb) + + return nil +} + +// perturbNode perturbs a node with a given perturbation, returning its status +// after recovering. +func perturbNode(ctx context.Context, node *e2e.Node, perturbation e2e.Perturbation) (*rpctypes.ResultStatus, error) { + testnet := node.Testnet + name := node.Name + ctx = log.WithCtx(ctx, "name", name) + + switch perturbation { + case e2e.PerturbationDisconnect: + networkName := testnet.Name + "_" + testnet.Name + log.Info(ctx, "Perturb node: disconnect") + if err := docker.Exec(ctx, "network", "disconnect", networkName, name); err != nil { + return nil, errors.Wrap(err, "disconnect node from network") + } + time.Sleep(10 * time.Second) + if err := docker.Exec(ctx, "network", "connect", networkName, name); err != nil { + return nil, errors.Wrap(err, "connect node tp network") + } + + case e2e.PerturbationKill: + log.Info(ctx, "Perturb node: kill") + if err := docker.ExecCompose(ctx, testnet.Dir, "kill", "-s", "SIGKILL", name); err != nil { + return nil, errors.Wrap(err, "kill node") + } + if err := docker.ExecCompose(ctx, testnet.Dir, "start", name); err != nil { + return nil, errors.Wrap(err, "start node") + } + + case e2e.PerturbationPause: + log.Info(ctx, "Perturb node: pause") + if err := docker.ExecCompose(ctx, testnet.Dir, "pause", name); err != nil { + return nil, errors.Wrap(err, "pause node") + } + time.Sleep(10 * time.Second) + if err := docker.ExecCompose(ctx, testnet.Dir, "unpause", name); err != nil { + return nil, errors.Wrap(err, "unpause node") + } + + case e2e.PerturbationRestart: + log.Info(ctx, "Perturb node: restart") + if err := docker.ExecCompose(ctx, testnet.Dir, "restart", name); err != nil { + return nil, errors.Wrap(err, "restart node") + } + + case e2e.PerturbationUpgrade: + return nil, errors.New("upgrade perturbation not supported") + + default: + return nil, errors.New("unexpected perturbation type", "type", perturbation) + } + + status, err := waitForNode(ctx, node, 0, 20*time.Second) + if err != nil { + return nil, err + } + + log.Info(ctx, "Node recovered from perturbation", "height", status.SyncInfo.LatestBlockHeight) + + return status, nil +} diff --git a/e2e/app/rpc.go b/e2e/app/rpc.go new file mode 100644 index 00000000..4a2c08b4 --- /dev/null +++ b/e2e/app/rpc.go @@ -0,0 +1,154 @@ +package app + +import ( + "context" + "fmt" + "time" + + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + rpctypes "github.com/cometbft/cometbft/rpc/core/types" + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/cometbft/cometbft/types" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// waitForHeight waits for the network to reach a certain height (or above), +// returning the highest height seen. Errors if the network is not making +// progress at all. +func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*types.Block, *types.BlockID, error) { + var ( + err error + maxResult *rpctypes.ResultBlock + clients = map[string]*rpchttp.HTTP{} + lastIncrease = time.Now() + ) + + currentBlock := func(ctx context.Context, client *rpchttp.HTTP) (*rpctypes.ResultBlock, error) { + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + resp, err := client.Block(ctx, nil) + if err != nil { + return nil, errors.Wrap(err, "get block") + } + + return resp, nil + } + + queryTicker := time.NewTicker(time.Second) + defer queryTicker.Stop() + logTicker := time.NewTicker(5 * time.Second) + defer logTicker.Stop() + + for { + select { + case <-ctx.Done(): + return nil, nil, errors.Wrap(ctx.Err(), "context canceled") + case <-logTicker.C: + current := "unknown" + if maxResult != nil { + current = fmt.Sprint(maxResult.Block.Height) + } + log.Debug(ctx, "Still waiting for height", "current_height", current, "wait_for_height", height) + case <-queryTicker.C: + for _, node := range testnet.Nodes { + if node.Stateless() { + continue + } + client, ok := clients[node.Name] + if !ok { + client, err = node.Client() + if err != nil { + continue + } + clients[node.Name] = client + } + + result, err := currentBlock(ctx, client) + if errors.Is(err, context.DeadlineExceeded) { + return nil, nil, errors.Wrap(err, "timeout") + } else if err != nil { + continue + } + + if result.Block != nil && (maxResult == nil || result.Block.Height > maxResult.Block.Height) { + maxResult = result + lastIncrease = time.Now() + } + if maxResult != nil && maxResult.Block.Height >= height { + return maxResult.Block, &maxResult.BlockID, nil + } + } + + if len(clients) == 0 { + return nil, nil, errors.New("unable to connect to any network nodes") + } + if time.Since(lastIncrease) >= 20*time.Second { + if maxResult == nil { + return nil, nil, errors.New("chain stalled at unknown height") + } + + return nil, nil, errors.New("chain stalled", "height", maxResult.Block.Height) + } + } + } +} + +// waitForNode waits for a node to become available and catch up to the given block height. +func waitForNode(ctx context.Context, node *e2e.Node, height int64, timeout time.Duration, +) (*rpctypes.ResultStatus, error) { + client, err := node.Client() + if err != nil { + return nil, errors.Wrap(err, "getting client") + } + + timer := time.NewTimer(0) + defer timer.Stop() + var curHeight int64 + lastChanged := time.Now() + for { + select { + case <-ctx.Done(): + return nil, errors.Wrap(ctx.Err(), "context canceled") + case <-timer.C: + status, err := client.Status(ctx) + switch { + case time.Since(lastChanged) > timeout: + return nil, errors.New("timed out waiting for height", "name", node.Name, "height", height) + case err != nil: + case status.SyncInfo.LatestBlockHeight >= height && (height == 0 || !status.SyncInfo.CatchingUp): + return status, nil + case curHeight < status.SyncInfo.LatestBlockHeight: + curHeight = status.SyncInfo.LatestBlockHeight + lastChanged = time.Now() + } + + timer.Reset(300 * time.Millisecond) + } + } +} + +// waitForAllNodes waits for all nodes to become available and catch up to the given block height. +func waitForAllNodes(ctx context.Context, testnet *e2e.Testnet, height int64, timeout time.Duration) (int64, error) { + var lastHeight int64 + + deadline := time.Now().Add(timeout) + + for _, node := range testnet.Nodes { + if node.Mode == e2e.ModeSeed { + continue + } + + status, err := waitForNode(ctx, node, height, time.Until(deadline)) + if err != nil { + return 0, err + } + + if status.SyncInfo.LatestBlockHeight > lastHeight { + lastHeight = status.SyncInfo.LatestBlockHeight + } + } + + return lastHeight, nil +} diff --git a/e2e/app/run.go b/e2e/app/run.go new file mode 100644 index 00000000..d9438821 --- /dev/null +++ b/e2e/app/run.go @@ -0,0 +1,118 @@ +package app + +import ( + "context" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +func DefaultDeployConfig() DeployConfig { + return DeployConfig{} +} + +type DeployConfig struct { + // Internal use parameters (no command line flags). + testConfig bool +} + +// Deploy a new e2e network. +func Deploy(ctx context.Context, def Definition, cfg DeployConfig) error { + if def.Testnet.Network.IsProtected() { + // If a protected network needs to be deployed temporarily comment out this check. + return errors.New("cannot deploy protected network", "network", def.Testnet.Network) + } + + if err := deployPublicCreate3(ctx, def); err != nil { + return err + } + + if err := Setup(ctx, def, cfg); err != nil { + return err + } + + // Only stop and delete existing network right before actually starting new ones. + if err := CleanInfra(ctx, def); err != nil { + return err + } + + if err := StartInitial(ctx, def.Testnet.Testnet, def.Infra); err != nil { + return err + } + + if err := fundAccounts(ctx, def); err != nil { + return err + } + + if err := deployPrivateCreate3(ctx, def); err != nil { + return err + } + + //nolint:revive // Will add more logic after this if check + if err := FundValidatorsForTesting(ctx, def); err != nil { + return err + } + + return nil +} + +// E2ETestConfig is the configuration required to run a full e2e test. +type E2ETestConfig struct { + Preserve bool +} + +// DefaultE2ETestConfig returns a default configuration for a e2e test. +func DefaultE2ETestConfig() E2ETestConfig { + return E2ETestConfig{} +} + +// E2ETest runs a full e2e test. +func E2ETest(ctx context.Context, def Definition, cfg E2ETestConfig) error { + stopValidatorUpdates := StartValidatorUpdates(ctx, def) + + if err := StartRemaining(ctx, def.Testnet.Testnet, def.Infra); err != nil { + return err + } + + if err := Wait(ctx, def.Testnet.Testnet, 5); err != nil { // allow some txs to go through + return err + } + + if def.Testnet.HasPerturbations() { + if err := perturb(ctx, def.Testnet); err != nil { + return err + } + } + + if def.Testnet.Evidence > 0 { + return errors.New("evidence injection not supported yet") + } + + if err := stopValidatorUpdates(); err != nil { + return errors.Wrap(err, "stop validator updates") + } + + // Start unit tests. + if err := Test(ctx, def, false); err != nil { + return err + } + + if cfg.Preserve { + log.Warn(ctx, "Docker containers not stopped, --preserve=true", nil) + } else if err := CleanInfra(ctx, def); err != nil { + return err + } + + return nil +} + +// Upgrade generates all local artifacts, but only copies the docker-compose file to the VMs. +// It them calls docker-compose up. +func Upgrade(ctx context.Context, def Definition, cfg DeployConfig, upgradeCfg types.UpgradeConfig) error { + if err := Setup(ctx, def, cfg); err != nil { + return err + } + + return def.Infra.Upgrade(ctx, upgradeCfg) +} diff --git a/e2e/app/setup.go b/e2e/app/setup.go new file mode 100644 index 00000000..b85e7bd7 --- /dev/null +++ b/e2e/app/setup.go @@ -0,0 +1,345 @@ +package app + +import ( + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "regexp" + "time" + + "github.com/cometbft/cometbft/config" + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/p2p" + "github.com/cometbft/cometbft/privval" + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + + iliadcmd "github.com/piplabs/story/client/cmd" + iliadcfg "github.com/piplabs/story/client/config" + "github.com/piplabs/story/client/genutil" + evmgenutil "github.com/piplabs/story/client/genutil/evm" + "github.com/piplabs/story/e2e/app/agent" + "github.com/piplabs/story/e2e/app/geth" + "github.com/piplabs/story/e2e/app/static" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/e2e/vmcompose" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" + + _ "embed" // Embed requires blank import +) + +const ( + AppAddressTCP = "tcp://127.0.0.1:30000" + AppAddressUNIX = "unix:///var/run/app.sock" + + PrivvalKeyFile = "config/priv_validator_key.json" + PrivvalStateFile = "data/priv_validator_state.json" +) + +// Setup sets up the testnet configuration. +func Setup(ctx context.Context, def Definition, depCfg DeployConfig) error { + log.Info(ctx, "Setup testnet", "dir", def.Testnet.Dir) + + if err := CleanupDir(ctx, def.Testnet.Dir); err != nil { + return err + } + + if err := os.MkdirAll(def.Testnet.Dir, os.ModePerm); err != nil { + return errors.Wrap(err, "mkdir") + } + + // Setup geth execution genesis + gethGenesis, err := evmgenutil.MakeGenesis(def.Manifest.Network) + if err != nil { + return errors.Wrap(err, "make genesis") + } + if err := geth.WriteAllConfig(def.Testnet, gethGenesis); err != nil { + return err + } + + var vals []crypto.PubKey + // var valPrivKeys []crypto.PrivKey + for val := range def.Testnet.Validators { + vals = append(vals, val.PrivvalKey.PubKey()) + // valPrivKeys = append(valPrivKeys, val.PrivvalKey) + } + + cosmosGenesis, err := genutil.MakeGenesis( + def.Manifest.Network, + time.Now(), + gethGenesis.ToBlock().Hash(), + vals...) + if err != nil { + return errors.Wrap(err, "make genesis") + } + cmtGenesis, err := cosmosGenesis.ToGenesisDoc() + if err != nil { + return errors.Wrap(err, "convert genesis") + } + + logCfg := logConfig(def) + + if err := writeAnvilState(def.Testnet); err != nil { + return err + } + + for _, node := range def.Testnet.Nodes { + nodeDir := filepath.Join(def.Testnet.Dir, node.Name) + + dirs := []string{ + filepath.Join(nodeDir, "config"), + filepath.Join(nodeDir, "data"), + } + for _, dir := range dirs { + err := os.MkdirAll(dir, 0o755) + if err != nil { + return errors.Wrap(err, "make dir") + } + } + + cfg, err := MakeConfig(node, nodeDir) + if err != nil { + return err + } + config.WriteConfigFile(filepath.Join(nodeDir, "config", "config.toml"), cfg) // panics + + iliadEVM := iliadEVMByPrefix(def.Testnet, node.Name) + + if err := writeIliadConfig( + def.Testnet.Network, + nodeDir, + def.Cfg, + logCfg, + depCfg.testConfig, + node.Mode, + iliadEVM.InstanceName, + ); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(nodeDir, "config", "jwtsecret"), []byte(iliadEVM.JWTSecret), 0o600); err != nil { + return errors.Wrap(err, "write jwtsecret") + } + + err = cmtGenesis.SaveAs(filepath.Join(nodeDir, "config", "genesis.json")) + if err != nil { + return errors.Wrap(err, "write genesis") + } + + err = (&p2p.NodeKey{PrivKey: node.NodeKey}).SaveAs(filepath.Join(nodeDir, "config", "node_key.json")) + if err != nil { + return errors.Wrap(err, "write node key") + } + + (privval.NewFilePV(node.PrivvalKey, + filepath.Join(nodeDir, PrivvalKeyFile), + filepath.Join(nodeDir, PrivvalStateFile), + )).Save() + + // Initialize the node's data directory (with noop logger since it is noisy). + initCfg := iliadcmd.InitConfig{ + HomeDir: nodeDir, + Network: def.Testnet.Network, + } + if err := iliadcmd.InitFiles(log.WithNoopLogger(ctx), initCfg); err != nil { + return errors.Wrap(err, "init files") + } + } + + if def.Testnet.Prometheus { + if err := agent.WriteConfig(ctx, def.Testnet, def.Cfg.AgentSecrets); err != nil { + return errors.Wrap(err, "write prom config") + } + } + + if err := def.Infra.Setup(); err != nil { + return errors.Wrap(err, "setup provider") + } + + return nil +} + +// writeAnvilState writes the embedded /static/el-anvil-state.json +// to /anvil/state.json for use by all anvil chains. +func writeAnvilState(testnet types.Testnet) error { + anvilStateFile := filepath.Join(testnet.Dir, "anvil", "state.json") + if err := os.MkdirAll(filepath.Dir(anvilStateFile), 0o755); err != nil { + return errors.Wrap(err, "mkdir") + } + if err := os.WriteFile(anvilStateFile, static.GetDevnetAnvilState(), 0o644); err != nil { + return errors.Wrap(err, "write anvil state") + } + + return nil +} + +// MakeConfig generates a CometBFT config for a node. +// +//nolint:lll // CometBFT super long names :( +func MakeConfig(node *e2e.Node, nodeDir string) (*config.Config, error) { + cfg := iliadcmd.DefaultCometConfig(nodeDir) + cfg.Moniker = node.Name + cfg.ProxyApp = AppAddressTCP + cfg.RPC.ListenAddress = "tcp://0.0.0.0:26657" + cfg.RPC.PprofListenAddress = ":6060" + cfg.P2P.ExternalAddress = fmt.Sprintf("tcp://%v", node.AddressP2P(false)) + cfg.P2P.AddrBookStrict = false + cfg.DBBackend = node.Database + cfg.StateSync.DiscoveryTime = 5 * time.Second + cfg.BlockSync.Version = node.BlockSyncVersion + cfg.Mempool.ExperimentalMaxGossipConnectionsToNonPersistentPeers = int(node.Testnet.ExperimentalMaxGossipConnectionsToNonPersistentPeers) + cfg.Mempool.ExperimentalMaxGossipConnectionsToPersistentPeers = int(node.Testnet.ExperimentalMaxGossipConnectionsToPersistentPeers) + + switch node.ABCIProtocol { + case e2e.ProtocolUNIX: + cfg.ProxyApp = AppAddressUNIX + case e2e.ProtocolTCP: + cfg.ProxyApp = AppAddressTCP + case e2e.ProtocolGRPC: + cfg.ProxyApp = AppAddressTCP + cfg.ABCI = "grpc" + case e2e.ProtocolBuiltin, e2e.ProtocolBuiltinConnSync: + cfg.ProxyApp = "" + cfg.ABCI = "" + default: + return nil, errors.New("unexpected ABCI protocol") + } + + // CometBFT errors if it does not have a privval key set up, regardless of whether + // it's actually needed (e.g. for remote KMS or non-validators). We set up a dummy + // key here by default, and use the real key for actual validators that should use + // the file privval. + cfg.PrivValidatorListenAddr = "" + cfg.PrivValidatorKey = PrivvalKeyFile + cfg.PrivValidatorState = PrivvalStateFile + + if node.PrivvalProtocol != e2e.ProtocolFile { + return nil, errors.New("only PrivvalKeyFile is supported") + } + + if node.Mode == types.ModeSeed { + cfg.P2P.SeedMode = true + cfg.P2P.PexReactor = true + } + + if node.StateSync { + cfg.StateSync.Enable = true + cfg.StateSync.RPCServers = []string{} + for _, peer := range node.Testnet.ArchiveNodes() { + if peer.Name == node.Name { + continue + } + cfg.StateSync.RPCServers = append(cfg.StateSync.RPCServers, peer.AddressRPC()) + } + if len(cfg.StateSync.RPCServers) < 2 { + return nil, errors.New("unable to find 2 suitable state sync RPC servers") + } + } + + cfg.P2P.Seeds = "" + for _, seed := range node.Seeds { + if len(cfg.P2P.Seeds) > 0 { + cfg.P2P.Seeds += "," + } + cfg.P2P.Seeds += seed.AddressP2P(true) + } + cfg.P2P.PersistentPeers = "" + for _, peer := range node.PersistentPeers { + if len(cfg.P2P.PersistentPeers) > 0 { + cfg.P2P.PersistentPeers += "," + } + cfg.P2P.PersistentPeers += peer.AddressP2P(true) + } + + if node.Prometheus { + cfg.Instrumentation.Prometheus = true + } + + return &cfg, nil +} + +// writeIliadConfig generates an iliad application config for a node and writes it to disk. +func writeIliadConfig( + network netconf.ID, + nodeDir string, + defCfg DefinitionConfig, + logCfg log.Config, + testCfg bool, + mode e2e.Mode, + evmInstance string, +) error { + cfg := iliadcfg.DefaultConfig() + + switch mode { + case e2e.ModeValidator, e2e.ModeFull: + cfg.PruningOption = "nothing" + cfg.MinRetainBlocks = 0 + case e2e.ModeSeed, e2e.ModeLight: + cfg.PruningOption = "everything" + cfg.MinRetainBlocks = 1 + default: + cfg.PruningOption = "default" + cfg.MinRetainBlocks = 0 + } + + cfg.Network = network + cfg.HomeDir = nodeDir + cfg.EngineEndpoint = fmt.Sprintf("http://%s:8551", evmInstance) //nolint:nosprintfhostport // net.JoinHostPort doesn't prefix http. + cfg.EngineJWTFile = "/client/config/jwtsecret" // Absolute path inside docker container + cfg.Tracer.Endpoint = defCfg.TracingEndpoint + cfg.Tracer.Headers = defCfg.TracingHeaders + + if testCfg { + cfg.SnapshotInterval = 1 // Write snapshots each block in e2e tests + cfg.SnapshotKeepRecent = 0 // Keep all snapshots in e2e tests + } + + return iliadcfg.WriteConfigTOML(cfg, logCfg) +} + +// updateConfigStateSync updates the state sync config for a node. +func updateConfigStateSync(nodeDir string, height int64, hash []byte) error { + cfgPath := filepath.Join(nodeDir, "config", "config.toml") + + // FIXME Apparently there's no function to simply load a config file without + // involving the entire Viper apparatus, so we'll just resort to regexps. + bz, err := os.ReadFile(cfgPath) + if err != nil { + return errors.Wrap(err, "read config") + } + + before := string(bz) + + bz = regexp.MustCompile(`(?m)^trust_height =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust_height = %v`, height))) + bz = regexp.MustCompile(`(?m)^trust_hash =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust_hash = "%X"`, hash))) + bz = regexp.MustCompile(`(?m)^log_level =.*`).ReplaceAll(bz, []byte(`log_level = "info"`)) // Increase log level. + + after := string(bz) + if before == after { + return errors.New("no changes to config") + } + + if err := os.WriteFile(cfgPath, bz, 0o644); err != nil { + return errors.Wrap(err, "write config") + } + + return nil +} + +// logConfig returns a default e2e log config. +// Default format is console (local dev), but vmcompose uses logfmt. +func logConfig(def Definition) log.Config { + format := log.FormatConsole + if def.Infra.GetInfrastructureData().Provider == vmcompose.ProviderName { + format = log.FormatLogfmt + } + + return log.Config{ + Format: format, + Level: slog.LevelDebug.String(), + Color: log.ColorForce, + } +} diff --git a/e2e/app/setup_internal_test.go b/e2e/app/setup_internal_test.go new file mode 100644 index 00000000..b414567d --- /dev/null +++ b/e2e/app/setup_internal_test.go @@ -0,0 +1,48 @@ +package app + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test . -golden -clean + +func TestUpdateConfigStateSync(t *testing.T) { + t.Parallel() + config := ` +# Database directory +db_dir = "data" + +# Output level for logging, including package level options +log_level = "error" + +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + +# For Cosmos SDK-based chains, trust_period should usually be about 2/3 of the unbonding time (~2 +# weeks) during which they can be financially punished (slashed) for misbehavior. +rpc_servers = "" +trust_height = 0 +trust_hash = "" +trust_period = "168h0m0s" +` + + dir := os.TempDir() + configFile := filepath.Join(dir, "config", "config.toml") + require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755)) + err := os.WriteFile(configFile, []byte(config), 0o644) + require.NoError(t, err) + + err = updateConfigStateSync(dir, 1, []byte("test")) + require.NoError(t, err) + + bz, err := os.ReadFile(configFile) + require.NoError(t, err) + + tutil.RequireGoldenBytes(t, bz) +} diff --git a/e2e/app/start.go b/e2e/app/start.go new file mode 100644 index 00000000..982fc29f --- /dev/null +++ b/e2e/app/start.go @@ -0,0 +1,170 @@ +package app + +import ( + "context" + "path/filepath" + "slices" + "sort" + "time" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/cometbft/cometbft/test/e2e/pkg/infra" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// StartInitial starts the initial nodes (start_at==0). +func StartInitial(ctx context.Context, testnet *e2e.Testnet, p infra.Provider) error { + allNodes, err := getSortedNodes(testnet) + if err != nil { + return err + } + + // Start initial nodes (StartAt: 0) + initialNodes := make([]*e2e.Node, 0) + for _, node := range allNodes { + if node.StartAt > 0 { + continue + } + initialNodes = append(initialNodes, node) + } + if len(initialNodes) == 0 { + return errors.New("no initial nodes in testnet") + } + + log.Info(ctx, "Starting initial network nodes...", "count", len(initialNodes)) + + if err = p.StartNodes(ctx, initialNodes...); err != nil { + return errors.Wrap(err, "starting initial nodes") + } + + for _, node := range initialNodes { + log.Info(ctx, "Starting node", + "name", node.Name, + "external_ip", node.ExternalIP, + "proxy_port", node.ProxyPort, + "prom", node.PrometheusProxyPort, + ) + if _, err := waitForNode(ctx, node, 0, 15*time.Second); err != nil { + return err + } + } + + networkHeight := testnet.InitialHeight + + // Wait for initial height + log.Info(ctx, "Waiting for initial height", + "height", networkHeight, + "initial", len(initialNodes), + "pending", len(allNodes)-len(initialNodes)) + + _, _, err = waitForHeight(ctx, testnet, networkHeight) + if err != nil { + return err + } + + return nil +} + +// StartRemaining starts the remaining nodes (start_at>0). +func StartRemaining(ctx context.Context, testnet *e2e.Testnet, p infra.Provider) error { + nodeQueue, err := getSortedNodes(testnet) + if err != nil { + return err + } + + var remaining []*e2e.Node + for _, node := range nodeQueue { + if node.StartAt > 0 { + remaining = append(remaining, node) + } + } + + if len(remaining) == 0 { + return nil + } + + block, blockID, err := waitForHeight(ctx, testnet, testnet.InitialHeight) + if err != nil { + return err + } + networkHeight := block.Height + + log.Debug(ctx, "Setting catchup node state sync", "height", block.Height, "catchup_nodes", len(remaining)) + + // Update any state sync nodes with a trusted height and hash + for _, node := range remaining { + if node.StateSync || node.Mode == e2e.ModeLight { + nodeDir := filepath.Join(testnet.Dir, node.Name) + err = updateConfigStateSync(nodeDir, block.Height, blockID.Hash.Bytes()) + if err != nil { + return err + } + } + } + + for _, node := range remaining { + if node.StartAt > networkHeight { + // if we're starting a node that's ahead of + // the last known height of the network, then + // we should make sure that the rest of the + // network has reached at least the height + // that this node will start at before we + // start the node. + + log.Info(ctx, "Waiting for network to advance before starting catchup node", + "node", node.Name, + "current_height", networkHeight, + "wait_for_height", node.StartAt) + + networkHeight = node.StartAt + + if _, _, err := waitForHeight(ctx, testnet, networkHeight); err != nil { + return err + } + } + + log.Info(ctx, "Starting catchup node", "node", node.Name, "height", node.StartAt) + + err := p.StartNodes(ctx, node) + if err != nil { + return errors.Wrap(err, "starting catchup node") + } + status, err := waitForNode(ctx, node, node.StartAt, 3*time.Minute) + if err != nil { + return err + } + log.Info(ctx, "Started catchup node", "name", node.Name, "height", status.SyncInfo.LatestBlockHeight) + } + + return nil +} + +// getSortedNodes returns a copy of the testnet nodes by startAt, then mode, then name. +func getSortedNodes(testnet *e2e.Testnet) ([]*e2e.Node, error) { + if len(testnet.Nodes) == 0 { + return nil, errors.New("no nodes in testnet") + } + + nodeQueue := slices.Clone(testnet.Nodes) + sort.SliceStable(nodeQueue, func(i, j int) bool { + a, b := nodeQueue[i], nodeQueue[j] + switch { + case a.Mode == b.Mode: + return false + case a.Mode == e2e.ModeSeed: + return true + case a.Mode == e2e.ModeValidator && b.Mode == e2e.ModeFull: + return true + } + + return false + }) + + sort.SliceStable(nodeQueue, func(i, j int) bool { + return nodeQueue[i].StartAt < nodeQueue[j].StartAt + }) + + return nodeQueue, nil +} diff --git a/e2e/app/static/anvil-state.json b/e2e/app/static/anvil-state.json new file mode 100644 index 00000000..87dd9786 --- /dev/null +++ b/e2e/app/static/anvil-state.json @@ -0,0 +1,292 @@ +{ + "block": { + "number": "0x0", + "coinbase": "0x0000000000000000000000000000000000000000", + "timestamp": "0x65fb7439", + "gas_limit": "0x1c9c380", + "basefee": "0x41ee1a7", + "difficulty": "0x0", + "prevrandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "blob_excess_gas_and_price": { + "excess_blob_gas": 0, + "blob_gasprice": 1 + } + }, + "accounts": { + "0x0000000000000000000000000000000000000000": { + "nonce": 0, + "balance": "0x135e801bef60c00", + "code": "0x", + "storage": {} + }, + "0x05b4cb126885fb10464fdd12666feb25e2563b76": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106101425760003560e01c80638da5cb5b116100b8578063d79aceab1161007c578063d79aceab146102f8578063df5cf7231461031f578063ec76f44214610346578063f2fde38b14610359578063f698da251461036c578063fabc1cbc1461037457600080fd5b80638da5cb5b1461029b5780639926ee7d146102ac578063a1060c88146102bf578063a364f4da146102d2578063a98fb355146102e557600080fd5b806349075da31161010a57806349075da3146101fa578063595c6a67146102355780635ac86ab71461023d5780635c975abb14610260578063715018a614610268578063886f11951461027057600080fd5b806310d67a2f14610147578063136439dd1461015c5780631794bb3c1461016f57806320606b7014610182578063374823b5146101bc575b600080fd5b61015a6101553660046117b2565b610387565b005b61015a61016a3660046117d6565b610443565b61015a61017d3660046117ef565b610582565b6101a97f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681565b6040519081526020015b60405180910390f35b6101ea6101ca366004611830565b609960209081526000928352604080842090915290825290205460ff1681565b60405190151581526020016101b3565b61022861020836600461185c565b609860209081526000928352604080842090915290825290205460ff1681565b6040516101b391906118ab565b61015a6106ac565b6101ea61024b3660046118d3565b606654600160ff9092169190911b9081161490565b6066546101a9565b61015a610773565b606554610283906001600160a01b031681565b6040516001600160a01b0390911681526020016101b3565b6033546001600160a01b0316610283565b61015a6102ba366004611966565b610787565b6101a96102cd366004611a4d565b610b1a565b61015a6102e03660046117b2565b610bd3565b61015a6102f3366004611a93565b610d3c565b6101a97fda2c89bafdd34776a2b8bb9c83c82f419e20cc8c67207f70edd58249b92661bd81565b6102837f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af81565b61015a6103543660046117d6565b610d83565b61015a6103673660046117b2565b610e2e565b6101a9610ea4565b61015a6103823660046117d6565b610ee2565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103da573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103fe9190611b05565b6001600160a01b0316336001600160a01b0316146104375760405162461bcd60e51b815260040161042e90611b22565b60405180910390fd5b6104408161103e565b50565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa15801561048b573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104af9190611b6c565b6104cb5760405162461bcd60e51b815260040161042e90611b8e565b606654818116146105445760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c6974790000000000000000606482015260840161042e565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b600054610100900460ff16158080156105a25750600054600160ff909116105b806105bc5750303b1580156105bc575060005460ff166001145b61061f5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b606482015260840161042e565b6000805460ff191660011790558015610642576000805461ff0019166101001790555b61064c8383611135565b61065461121f565b609755610660846112b6565b80156106a6576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b50505050565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa1580156106f4573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107189190611b6c565b6107345760405162461bcd60e51b815260040161042e90611b8e565b600019606681905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b61077b611308565b61078560006112b6565b565b606654600090600190811614156107dc5760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b604482015260640161042e565b42826040015110156108445760405162461bcd60e51b815260206004820152603e6024820152600080516020611c8d83398151915260448201527f56533a206f70657261746f72207369676e617475726520657870697265640000606482015260840161042e565b60013360009081526098602090815260408083206001600160a01b038816845290915290205460ff16600181111561087e5761087e611895565b14156108e05760405162461bcd60e51b815260206004820152603f6024820152600080516020611c8d83398151915260448201527f56533a206f70657261746f7220616c7265616479207265676973746572656400606482015260840161042e565b6001600160a01b038316600090815260996020908152604080832085830151845290915290205460ff16156109645760405162461bcd60e51b81526020600482015260366024820152600080516020611c8d8339815191526044820152751594ce881cd85b1d08185b1c9958591e481cdc195b9d60521b606482015260840161042e565b6040516336b87bd760e11b81526001600160a01b0384811660048301527f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af1690636d70f7ae90602401602060405180830381865afa1580156109ca573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109ee9190611b6c565b610a645760405162461bcd60e51b815260206004820152604d6024820152600080516020611c8d83398151915260448201527f56533a206f70657261746f72206e6f74207265676973746572656420746f204560648201526c1a59d95b93185e595c881e595d609a1b608482015260a40161042e565b6000610a7a843385602001518660400151610b1a565b9050610a8b84828560000151611362565b3360008181526098602090815260408083206001600160a01b0389168085529083528184208054600160ff199182168117909255609985528386208a860151875290945293829020805490931684179092555190917ff0952b1c65271d819d39983d2abb044b9cace59bcc4d4dd389f586ebdcb15b4191610b0c91906118ab565b60405180910390a350505050565b604080517fda2c89bafdd34776a2b8bb9c83c82f419e20cc8c67207f70edd58249b92661bd6020808301919091526001600160a01b0387811683850152861660608301526080820185905260a08083018590528351808403909101815260c0909201909252805191012060009081610b90610ea4565b60405161190160f01b602082015260228101919091526042810183905260620160408051808303601f190181529190528051602090910120979650505050505050565b60665460009060019081161415610c285760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b604482015260640161042e565b60013360009081526098602090815260408083206001600160a01b038716845290915290205460ff166001811115610c6257610c62611895565b14610cd55760405162461bcd60e51b815260206004820152603f60248201527f4156534469726563746f72792e646572656769737465724f70657261746f724660448201527f726f6d4156533a206f70657261746f72206e6f74207265676973746572656400606482015260840161042e565b3360008181526098602090815260408083206001600160a01b0387168085529252808320805460ff191690555190917ff0952b1c65271d819d39983d2abb044b9cace59bcc4d4dd389f586ebdcb15b4191610d3091906118ab565b60405180910390a35050565b336001600160a01b03167fa89c1dc243d8908a96dd84944bcc97d6bc6ac00dd78e20621576be6a3c9437138383604051610d77929190611bd6565b60405180910390a25050565b33600090815260996020908152604080832084845290915290205460ff1615610e085760405162461bcd60e51b815260206004820152603160248201527f4156534469726563746f72792e63616e63656c53616c743a2063616e6e6f742060448201527018d85b98d95b081cdc195b9d081cd85b1d607a1b606482015260840161042e565b33600090815260996020908152604080832093835292905220805460ff19166001179055565b610e36611308565b6001600160a01b038116610e9b5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b606482015260840161042e565b610440816112b6565b60007f0000000000000000000000000000000000000000000000000000000000007a69461415610ed5575060975490565b610edd61121f565b905090565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610f35573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f599190611b05565b6001600160a01b0316336001600160a01b031614610f895760405162461bcd60e51b815260040161042e90611b22565b6066541981196066541916146110075760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c6974790000000000000000606482015260840161042e565b606681905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c90602001610577565b6001600160a01b0381166110cc5760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a40161042e565b606554604080516001600160a01b03928316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1606580546001600160a01b0319166001600160a01b0392909216919091179055565b6065546001600160a01b031615801561115657506001600160a01b03821615155b6111d85760405162461bcd60e51b815260206004820152604760248201527f5061757361626c652e5f696e697469616c697a655061757365723a205f696e6960448201527f7469616c697a6550617573657228292063616e206f6e6c792062652063616c6c6064820152666564206f6e636560c81b608482015260a40161042e565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a261121b8261103e565b5050565b604080518082018252600a81526922b4b3b2b72630bcb2b960b11b60209182015281517f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866818301527f71b625cfad44bac63b13dba07f2e1d6084ee04b6f8752101ece6126d584ee6ea81840152466060820152306080808301919091528351808303909101815260a0909101909252815191012090565b603380546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6033546001600160a01b031633146107855760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161042e565b6001600160a01b0383163b1561148157604051630b135d3f60e11b808252906001600160a01b03851690631626ba7e906113a29086908690600401611c05565b602060405180830381865afa1580156113bf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906113e39190611c62565b6001600160e01b0319161461147c5760405162461bcd60e51b815260206004820152605360248201527f454950313237315369676e61747572655574696c732e636865636b5369676e6160448201527f747572655f454950313237313a2045524331323731207369676e6174757265206064820152721d995c9a599a58d85d1a5bdb8819985a5b1959606a1b608482015260a40161042e565b505050565b826001600160a01b03166114958383611521565b6001600160a01b03161461147c5760405162461bcd60e51b815260206004820152604760248201527f454950313237315369676e61747572655574696c732e636865636b5369676e6160448201527f747572655f454950313237313a207369676e6174757265206e6f742066726f6d6064820152661039b4b3b732b960c91b608482015260a40161042e565b60008060006115308585611545565b9150915061153d8161158b565b509392505050565b60008082516041141561157c5760208301516040840151606085015160001a611570878285856116d9565b94509450505050611584565b506000905060025b9250929050565b600081600481111561159f5761159f611895565b14156115a85750565b60018160048111156115bc576115bc611895565b141561160a5760405162461bcd60e51b815260206004820152601860248201527f45434453413a20696e76616c6964207369676e61747572650000000000000000604482015260640161042e565b600281600481111561161e5761161e611895565b141561166c5760405162461bcd60e51b815260206004820152601f60248201527f45434453413a20696e76616c6964207369676e6174757265206c656e67746800604482015260640161042e565b600381600481111561168057611680611895565b14156104405760405162461bcd60e51b815260206004820152602260248201527f45434453413a20696e76616c6964207369676e6174757265202773272076616c604482015261756560f01b606482015260840161042e565b6000807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311156117105750600090506003611794565b6040805160008082526020820180845289905260ff881692820192909252606081018690526080810185905260019060a0016020604051602081039080840390855afa158015611764573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b03811661178d57600060019250925050611794565b9150600090505b94509492505050565b6001600160a01b038116811461044057600080fd5b6000602082840312156117c457600080fd5b81356117cf8161179d565b9392505050565b6000602082840312156117e857600080fd5b5035919050565b60008060006060848603121561180457600080fd5b833561180f8161179d565b9250602084013561181f8161179d565b929592945050506040919091013590565b6000806040838503121561184357600080fd5b823561184e8161179d565b946020939093013593505050565b6000806040838503121561186f57600080fd5b823561187a8161179d565b9150602083013561188a8161179d565b809150509250929050565b634e487b7160e01b600052602160045260246000fd5b60208101600283106118cd57634e487b7160e01b600052602160045260246000fd5b91905290565b6000602082840312156118e557600080fd5b813560ff811681146117cf57600080fd5b634e487b7160e01b600052604160045260246000fd5b6040516060810167ffffffffffffffff8111828210171561192f5761192f6118f6565b60405290565b604051601f8201601f1916810167ffffffffffffffff8111828210171561195e5761195e6118f6565b604052919050565b6000806040838503121561197957600080fd5b82356119848161179d565b915060208381013567ffffffffffffffff808211156119a257600080fd5b90850190606082880312156119b657600080fd5b6119be61190c565b8235828111156119cd57600080fd5b8301601f810189136119de57600080fd5b8035838111156119f0576119f06118f6565b611a02601f8201601f19168701611935565b93508084528986828401011115611a1857600080fd5b808683018786013760008682860101525050818152838301358482015260408301356040820152809450505050509250929050565b60008060008060808587031215611a6357600080fd5b8435611a6e8161179d565b93506020850135611a7e8161179d565b93969395505050506040820135916060013590565b60008060208385031215611aa657600080fd5b823567ffffffffffffffff80821115611abe57600080fd5b818501915085601f830112611ad257600080fd5b813581811115611ae157600080fd5b866020828501011115611af357600080fd5b60209290920196919550909350505050565b600060208284031215611b1757600080fd5b81516117cf8161179d565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b600060208284031215611b7e57600080fd5b815180151581146117cf57600080fd5b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b606082015260800190565b60208152816020820152818360408301376000818301604090810191909152601f909201601f19160101919050565b82815260006020604081840152835180604085015260005b81811015611c3957858101830151858201606001528201611c1d565b81811115611c4b576000606083870101525b50601f01601f191692909201606001949350505050565b600060208284031215611c7457600080fd5b81516001600160e01b0319811681146117cf57600080fdfe4156534469726563746f72792e72656769737465724f70657261746f72546f41a2646970667358221220357ae0ebbbd5cff467b8031d418d71991e1379a4836ab611d82a77f612fa836964736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0x0c8e79f3534b00d9a3d4a856b665bf4ebc22f2ba": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0xd04ff4a75edd737a73e92b2f2274cb887d96e110", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + }, + "0x12975173b87f7595ee45dffb2ab812ece596bf84": { + "nonce": 1, + "balance": "0x0", + "code": "0x6080604052600436106100345760003560e01c80632289511814610039578063621fd13014610052578063c5f2892f14610077575b600080fd5b6100506100473660046100dc565b50505050505050565b005b34801561005e57600080fd5b50606060405161006e919061017f565b60405180910390f35b34801561008357600080fd5b506040516000815260200161006e565b60008083601f8401126100a557600080fd5b50813567ffffffffffffffff8111156100bd57600080fd5b6020830191508360208285010111156100d557600080fd5b9250929050565b60008060008060008060006080888a0312156100f757600080fd5b873567ffffffffffffffff8082111561010f57600080fd5b61011b8b838c01610093565b909950975060208a013591508082111561013457600080fd5b6101408b838c01610093565b909750955060408a013591508082111561015957600080fd5b506101668a828b01610093565b989b979a50959894979596606090950135949350505050565b600060208083528351808285015260005b818110156101ac57858101830151858201604001528201610190565b818111156101be576000604083870101525b50601f01601f191692909201604001939250505056fea2646970667358221220f9063df0d2ce20889bef5c74a36468da24316bb8892074a75b8b5b237ad0e1dc64736f6c634300080c0033", + "storage": {} + }, + "0x14dc79964da2c08b23698b3d3cc7ca32193d9955": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x196dbcbb54b8ec4958c959d8949ebfe87ac2aaaf": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106100575760003560e01c80633659cfe61461005c5780635c60da1b14610071578063715018a61461009a5780638da5cb5b146100a2578063f2fde38b146100b3575b600080fd5b61006f61006a3660046102ee565b6100c6565b005b6001546001600160a01b03165b6040516001600160a01b03909116815260200160405180910390f35b61006f61010e565b6000546001600160a01b031661007e565b61006f6100c13660046102ee565b610122565b6100ce6101af565b6100d781610209565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6101166101af565b610120600061029e565b565b61012a6101af565b6001600160a01b0381166101945760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084015b60405180910390fd5b61019d8161029e565b50565b6001600160a01b03163b151590565b6000546001600160a01b031633146101205760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161018b565b6001600160a01b0381163b61027c5760405162461bcd60e51b815260206004820152603360248201527f5570677261646561626c65426561636f6e3a20696d706c656d656e746174696f6044820152721b881a5cc81b9bdd08184818dbdb9d1c9858dd606a1b606482015260840161018b565b600180546001600160a01b0319166001600160a01b0392909216919091179055565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b60006020828403121561030057600080fd5b81356001600160a01b038116811461031757600080fd5b939250505056fea2646970667358221220ca5748d604d0e0b2e2d1f245764d61c1a68c535b585e13b3c2793b00a76d688964736f6c634300080c0033", + "storage": { + "0x0": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x1": "0x82dc47734901ee7d4f4232f398752cb9dd5daccc" + } + }, + "0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x29a79095352a718b3d7fe84e1f14e9f34a35598e": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040526004361061014b5760003560e01c806385594e58116100b6578063e4f4f8871161006f578063e4f4f887146103cc578063e5db06c014610405578063eb990c5914610425578063ecb7cb1b14610445578063f2fde38b14610472578063fabc1cbc1461049257600080fd5b806385594e5814610317578063886f1195146103445780638da5cb5b14610364578063c0db354c14610382578063ca661c0414610395578063d44e1b76146103ac57600080fd5b806350f73e7c1161010857806350f73e7c14610254578063595c6a67146102785780635ac86ab71461028d5780635c975abb146102cd578063715018a6146102e257806375608896146102f757600080fd5b806310d67a2f14610150578063136439dd146101725780631f39d87f146101925780633e1de008146101c85780634665bcda146101e85780634d50f9a414610234575b600080fd5b34801561015c57600080fd5b5061017061016b36600461192a565b6104b2565b005b34801561017e57600080fd5b5061017061018d36600461194e565b61056e565b34801561019e57600080fd5b506101b26101ad36600461192a565b6106ad565b6040516101bf9190611985565b60405180910390f35b3480156101d457600080fd5b506101b26101e336600461192a565b6108a8565b3480156101f457600080fd5b5061021c7f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e81565b6040516001600160a01b0390911681526020016101bf565b34801561024057600080fd5b5061017061024f36600461194e565b6109ee565b34801561026057600080fd5b5061026a60c95481565b6040519081526020016101bf565b34801561028457600080fd5b506101706109ff565b34801561029957600080fd5b506102bd6102a83660046119d2565b609854600160ff9092169190911b9081161490565b60405190151581526020016101bf565b3480156102d957600080fd5b5060985461026a565b3480156102ee57600080fd5b50610170610ac6565b34801561030357600080fd5b506102bd6103123660046119f5565b610ada565b34801561032357600080fd5b506103376103323660046119f5565b610b5d565b6040516101bf9190611a21565b34801561035057600080fd5b5060975461021c906001600160a01b031681565b34801561037057600080fd5b506033546001600160a01b031661021c565b610170610390366004611a2f565b610bdd565b3480156103a157600080fd5b5061026a62034bc081565b3480156103b857600080fd5b506101706103c736600461194e565b610e9d565b3480156103d857600080fd5b5061026a6103e736600461192a565b6001600160a01b0316600090815260ca602052604090206001015490565b34801561041157600080fd5b506101706104203660046119f5565b610ee3565b34801561043157600080fd5b50610170610440366004611a68565b610f2d565b34801561045157600080fd5b5061046561046036600461192a565b611055565b6040516101bf9190611aae565b34801561047e57600080fd5b5061017061048d36600461192a565b61110f565b34801561049e57600080fd5b506101706104ad36600461194e565b611185565b609760009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610505573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105299190611b04565b6001600160a01b0316336001600160a01b0316146105625760405162461bcd60e51b815260040161055990611b21565b60405180910390fd5b61056b816112e1565b50565b60975460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa1580156105b6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105da9190611b6b565b6105f65760405162461bcd60e51b815260040161055990611b8d565b6098548181161461066f5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c69747900000000000000006064820152608401610559565b609881905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b6001600160a01b038116600090815260ca6020526040812080546001909101546060926106da8383611beb565b90508060005b82811015610786576001600160a01b038716600090815260ca6020526040812060010161070d8388611c02565b8154811061071d5761071d611c1a565b6000918252602091829020604080518082019091529101546001600160e01b0381168252600160e01b900463ffffffff1691810182905260c95490925061076391611c02565b4310156107735781925050610786565b508061077e81611c30565b9150506106e0565b508060008167ffffffffffffffff8111156107a3576107a3611c4b565b6040519080825280602002602001820160405280156107e857816020015b60408051808201909152600080825260208201528152602001906001900390816107c15790505b509050811561089d5760005b8281101561089b576001600160a01b038916600090815260ca602052604090206001016108218289611c02565b8154811061083157610831611c1a565b6000918252602091829020604080518082019091529101546001600160e01b0381168252600160e01b900463ffffffff1691810191909152825183908390811061087d5761087d611c1a565b6020026020010181905250808061089390611c30565b9150506107f4565b505b979650505050505050565b6001600160a01b038116600090815260ca6020526040812080546001909101546060926108d58383611beb565b905060008167ffffffffffffffff8111156108f2576108f2611c4b565b60405190808252806020026020018201604052801561093757816020015b60408051808201909152600080825260208201528152602001906001900390816109105790505b50905060005b828110156109e4576001600160a01b038716600090815260ca6020526040902060010161096a8287611c02565b8154811061097a5761097a611c1a565b6000918252602091829020604080518082019091529101546001600160e01b0381168252600160e01b900463ffffffff169181019190915282518390839081106109c6576109c6611c1a565b602002602001018190525080806109dc90611c30565b91505061093d565b5095945050505050565b6109f66113d8565b61056b81611432565b60975460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa158015610a47573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a6b9190611b6b565b610a875760405162461bcd60e51b815260040161055990611b8d565b600019609881905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b610ace6113d8565b610ad860006114fa565b565b6001600160a01b038216600090815260ca60205260408120548210801590610b54575060c9546001600160a01b038416600090815260ca60205260409020600101805484908110610b2d57610b2d611c1a565b600091825260209091200154610b509190600160e01b900463ffffffff16611c02565b4310155b90505b92915050565b60408051808201909152600080825260208201526001600160a01b038316600090815260ca60205260409020600101805483908110610b9e57610b9e611c1a565b6000918252602091829020604080518082019091529101546001600160e01b0381168252600160e01b900463ffffffff16918101919091529392505050565b60405163a38406a360e01b81526001600160a01b038084166004830152839133917f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e169063a38406a390602401602060405180830381865afa158015610c47573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c6b9190611b04565b6001600160a01b031614610ce75760405162461bcd60e51b815260206004820152603d60248201527f44656c617965645769746864726177616c526f757465722e6f6e6c794569676560448201527f6e506f643a206e6f7420706f644f776e6572277320456967656e506f640000006064820152608401610559565b60985460009060019081161415610d105760405162461bcd60e51b815260040161055990611c61565b6001600160a01b038316610da65760405162461bcd60e51b815260206004820152605160248201527f44656c617965645769746864726177616c526f757465722e637265617465446560448201527f6c617965645769746864726177616c3a20726563697069656e742063616e6e6f60648201527074206265207a65726f206164647265737360781b608482015260a401610559565b346001600160e01b03811615610e96576040805180820182526001600160e01b03808416825263ffffffff43811660208085019182526001600160a01b038a16600081815260ca8352968720600190810180548083018255818a5293892088519551909616600160e01b029490961693909317939091019290925593525490917fb8f1b14c7caf74150801dcc9bc18d575cbeaf5b421943497e409df92c92e0f5991889188918691610e5791611beb565b604080516001600160a01b0395861681529490931660208501526001600160e01b039091169183019190915260608201526080015b60405180910390a1505b5050505050565b610ea561154c565b60985460009060019081161415610ece5760405162461bcd60e51b815260040161055990611c61565b610ed833836115a6565b5061056b6001606555565b610eeb61154c565b60985460009060019081161415610f145760405162461bcd60e51b815260040161055990611c61565b610f1e83836115a6565b50610f296001606555565b5050565b600054610100900460ff1615808015610f4d5750600054600160ff909116105b80610f675750303b158015610f67575060005460ff166001145b610fca5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b6064820152608401610559565b6000805460ff191660011790558015610fed576000805461ff0019166101001790555b610ff6856114fa565b6110008484611711565b61100982611432565b8015610e96576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15050505050565b6040805180820190915260008152606060208201526001600160a01b038216600090815260ca6020908152604080832081518083018352815481526001820180548451818702810187019095528085529195929486810194939192919084015b8282101561110157600084815260209081902060408051808201909152908401546001600160e01b0381168252600160e01b900463ffffffff16818301528252600190920191016110b5565b505050915250909392505050565b6111176113d8565b6001600160a01b03811661117c5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b6064820152608401610559565b61056b816114fa565b609760009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156111d8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111fc9190611b04565b6001600160a01b0316336001600160a01b03161461122c5760405162461bcd60e51b815260040161055990611b21565b6098541981196098541916146112aa5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c69747900000000000000006064820152608401610559565b609881905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c906020016106a2565b6001600160a01b03811661136f5760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a401610559565b609754604080516001600160a01b03928316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1609780546001600160a01b0319166001600160a01b0392909216919091179055565b6033546001600160a01b03163314610ad85760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610559565b62034bc08111156114b95760405162461bcd60e51b815260206004820152604560248201527f44656c617965645769746864726177616c526f757465722e5f7365745769746860448201527f64726177616c44656c6179426c6f636b733a206e657756616c756520746f6f206064820152646c6172676560d81b608482015260a401610559565b60c95460408051918252602082018390527f4ffb00400574147429ee377a5633386321e66d45d8b14676014b5fa393e61e9e910160405180910390a160c955565b603380546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6002606554141561159f5760405162461bcd60e51b815260206004820152601f60248201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c006044820152606401610559565b6002606555565b6001600160a01b038216600090815260ca602052604081208054600190910154825b84811080156115df5750816115dd8285611c02565b105b1561168c576001600160a01b038616600090815260ca602052604081206001016116098386611c02565b8154811061161957611619611c1a565b6000918252602091829020604080518082019091529101546001600160e01b0381168252600160e01b900463ffffffff1691810182905260c95490925061165f91611c02565b43101561166c575061168c565b8051611681906001600160e01b031686611c02565b9450506001016115c8565b6116968184611c02565b6001600160a01b038716600090815260ca602052604090205583156116bf576116bf86856117f7565b7f6b7151500bd0b5cc211bcc47b3029831b769004df4549e8e1c9a69da05bb094386856116ec8487611c02565b604080516001600160a01b039094168452602084019290925290820152606001610e8c565b6097546001600160a01b031615801561173257506001600160a01b03821615155b6117b45760405162461bcd60e51b815260206004820152604760248201527f5061757361626c652e5f696e697469616c697a655061757365723a205f696e6960448201527f7469616c697a6550617573657228292063616e206f6e6c792062652063616c6c6064820152666564206f6e636560c81b608482015260a401610559565b609881905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2610f29826112e1565b804710156118475760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a20696e73756666696369656e742062616c616e63650000006044820152606401610559565b6000826001600160a01b03168260405160006040518083038185875af1925050503d8060008114611894576040519150601f19603f3d011682016040523d82523d6000602084013e611899565b606091505b50509050806119105760405162461bcd60e51b815260206004820152603a60248201527f416464726573733a20756e61626c6520746f2073656e642076616c75652c207260448201527f6563697069656e74206d617920686176652072657665727465640000000000006064820152608401610559565b505050565b6001600160a01b038116811461056b57600080fd5b60006020828403121561193c57600080fd5b813561194781611915565b9392505050565b60006020828403121561196057600080fd5b5035919050565b80516001600160e01b0316825260209081015163ffffffff16910152565b602080825282518282018190526000919060409081850190868401855b828110156119c5576119b5848351611967565b92840192908501906001016119a2565b5091979650505050505050565b6000602082840312156119e457600080fd5b813560ff8116811461194757600080fd5b60008060408385031215611a0857600080fd5b8235611a1381611915565b946020939093013593505050565b60408101610b578284611967565b60008060408385031215611a4257600080fd5b8235611a4d81611915565b91506020830135611a5d81611915565b809150509250929050565b60008060008060808587031215611a7e57600080fd5b8435611a8981611915565b93506020850135611a9981611915565b93969395505050506040820135916060013590565b602080825282518282015282810151604080840181905281516060850181905260009392830191849160808701905b8084101561089b57611af0828651611967565b938501936001939093019290820190611add565b600060208284031215611b1657600080fd5b815161194781611915565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b600060208284031215611b7d57600080fd5b8151801515811461194757600080fd5b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b606082015260800190565b634e487b7160e01b600052601160045260246000fd5b600082821015611bfd57611bfd611bd5565b500390565b60008219821115611c1557611c15611bd5565b500190565b634e487b7160e01b600052603260045260246000fd5b6000600019821415611c4457611c44611bd5565b5060010190565b634e487b7160e01b600052604160045260246000fd5b60208082526019908201527f5061757361626c653a20696e646578206973207061757365640000000000000060408201526060019056fea2646970667358221220ce2fc4b5cd610b8fde4bd0087332394fd991351e261b38fd93bbb9c91135e97264736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0x2a264f26859166c5bf3868a54593ee716aebc848": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106102275760003560e01c806394f649dd11610130578063c6656702116100b8578063df5cf7231161007c578063df5cf72314610596578063e7a050aa146105bd578063f2fde38b146105d0578063f698da25146105e3578063fabc1cbc146105eb57600080fd5b8063c665670214610520578063cbc2bd6214610533578063cd293f6f14610546578063cf756fdf14610570578063df5b35471461058357600080fd5b8063b43b514b116100ff578063b43b514b146104b1578063b5d8b5b8146104c4578063c3c6b3a9146104d7578063c4623ea1146104fa578063c608c7f31461050d57600080fd5b806394f649dd14610433578063967fc0d2146104545780639b4da03d14610467578063b13442711461048a57600080fd5b80635c975abb116101b35780637ecebe00116101825780637ecebe00146103b3578063886f1195146103d35780638b8aac3c146103e65780638c80d4e51461040f5780638da5cb5b1461042257600080fd5b80635c975abb14610355578063663c1de41461035d578063715018a6146103805780637a7e0d921461038857600080fd5b80634665bcda116101fa5780634665bcda146102a157806348825e94146102e05780634e5a426314610307578063595c6a671461031a5780635ac86ab71461032257600080fd5b806310d67a2f1461022c578063136439dd1461024157806320606b701461025457806332e89ace1461028e575b600080fd5b61023f61023a366004612a1e565b6105fe565b005b61023f61024f366004612a3b565b6106ba565b61027b7f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681565b6040519081526020015b60405180910390f35b61027b61029c366004612ad4565b6107f9565b6102c87f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e81565b6040516001600160a01b039091168152602001610285565b61027b7f4337f82d142e41f2a8c10547cd8c859bddb92262a61058e77842e24d9dea922481565b61023f610315366004612bc1565b610a9d565b61023f610ad5565b610345610330366004612bfa565b609854600160ff9092169190911b9081161490565b6040519015158152602001610285565b60985461027b565b61034561036b366004612a1e565b60d16020526000908152604090205460ff1681565b61023f610b9c565b61027b610396366004612c1d565b60cd60209081526000928352604080842090915290825290205481565b61027b6103c1366004612a1e565b60ca6020526000908152604090205481565b6097546102c8906001600160a01b031681565b61027b6103f4366004612a1e565b6001600160a01b0316600090815260ce602052604090205490565b61023f61041d366004612c4b565b610bb0565b6033546001600160a01b03166102c8565b610446610441366004612a1e565b610c09565b604051610285929190612d00565b60cb546102c8906001600160a01b031681565b610345610475366004612a1e565b60d36020526000908152604090205460ff1681565b6102c87f0000000000000000000000000c8e79f3534b00d9a3d4a856b665bf4ebc22f2ba81565b61027b6104bf366004612e9e565b610d89565b61023f6104d2366004612fb9565b610dd6565b6103456104e5366004612a3b565b60cf6020526000908152604090205460ff1681565b61023f610508366004612ffb565b610f4a565b61023f61051b36600461304c565b610f9e565b61023f61052e366004612a1e565b611056565b6102c861054136600461309f565b611067565b610559610554366004612e9e565b61109f565b604080519215158352602083019190915201610285565b61023f61057e366004612ffb565b611133565b61023f6105913660046130cb565b611267565b6102c87f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af81565b61027b6105cb366004612c4b565b611490565b61023f6105de366004612a1e565b61150f565b61027b611585565b61023f6105f9366004612a3b565b6115c3565b609760009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610651573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106759190613137565b6001600160a01b0316336001600160a01b0316146106ae5760405162461bcd60e51b81526004016106a590613154565b60405180910390fd5b6106b78161171f565b50565b60975460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa158015610702573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610726919061319e565b6107425760405162461bcd60e51b81526004016106a5906131bb565b609854818116146107bb5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c697479000000000000000060648201526084016106a5565b609881905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b6098546000908190600190811614156108505760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b60448201526064016106a5565b610858611816565b6001600160a01b038816600090815260d3602052604090205460ff16156108fa5760405162461bcd60e51b815260206004820152604a60248201527f53747261746567794d616e616765722e6465706f736974496e746f537472617460448201527f656779576974685369676e61747572653a207468697264207472616e736665726064820152691cc8191a5cd8589b195960b21b608482015260a4016106a5565b4284101561097c5760405162461bcd60e51b815260206004820152604360248201527f53747261746567794d616e616765722e6465706f736974496e746f537472617460448201527f656779576974685369676e61747572653a207369676e617475726520657870696064820152621c995960ea1b608482015260a4016106a5565b6001600160a01b03858116600081815260ca602090815260408083205481517f4337f82d142e41f2a8c10547cd8c859bddb92262a61058e77842e24d9dea922493810193909352908201939093528b84166060820152928a16608084015260a0830189905260c0830182905260e0830187905290916101000160408051601f1981840301815291815281516020928301206001600160a01b038a16600090815260ca9093529082206001850190559150610a34611585565b60405161190160f01b6020820152602281019190915260428101839052606201604051602081830303815290604052805190602001209050610a77888288611870565b610a83888c8c8c611a2f565b9450505050610a926001606555565b509695505050505050565b60cb546001600160a01b03163314610ac75760405162461bcd60e51b81526004016106a590613203565b610ad18282611bfe565b5050565b60975460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa158015610b1d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b41919061319e565b610b5d5760405162461bcd60e51b81526004016106a5906131bb565b600019609881905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b610ba4611c6c565b610bae6000611cc6565b565b336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af1614610bf85760405162461bcd60e51b81526004016106a59061326d565b610c03838383611d18565b50505050565b6001600160a01b038116600090815260ce60205260408120546060918291908167ffffffffffffffff811115610c4157610c41612a64565b604051908082528060200260200182016040528015610c6a578160200160208202803683370190505b50905060005b82811015610cfb576001600160a01b038616600090815260cd6020908152604080832060ce9092528220805491929184908110610caf57610caf6132cb565b60009182526020808320909101546001600160a01b031683528201929092526040019020548251839083908110610ce857610ce86132cb565b6020908102919091010152600101610c70565b5060ce6000866001600160a01b03166001600160a01b031681526020019081526020016000208181805480602002602001604051908101604052809291908181526020018280548015610d7757602002820191906000526020600020905b81546001600160a01b03168152600190910190602001808311610d59575b50505050509150935093505050915091565b80516020808301516040808501516060860151608087015160a08801519351600097610db99790969591016132e1565b604051602081830303815290604052805190602001209050919050565b60cb546001600160a01b03163314610e005760405162461bcd60e51b81526004016106a590613203565b8060005b81811015610c035760d16000858584818110610e2257610e226132cb565b9050602002016020810190610e379190612a1e565b6001600160a01b0316815260208101919091526040016000205460ff1615610f4257600060d16000868685818110610e7157610e716132cb565b9050602002016020810190610e869190612a1e565b6001600160a01b031681526020810191909152604001600020805460ff19169115159190911790557f4074413b4b443e4e58019f2855a8765113358c7c72e39509c6af45fc0f5ba030848483818110610ee157610ee16132cb565b9050602002016020810190610ef69190612a1e565b6040516001600160a01b03909116815260200160405180910390a1610f42848483818110610f2657610f266132cb565b9050602002016020810190610f3b9190612a1e565b6000611bfe565b600101610e04565b336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af1614610f925760405162461bcd60e51b81526004016106a59061326d565b610c0384848484611e74565b336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af1614610fe65760405162461bcd60e51b81526004016106a59061326d565b604051636ce5768960e11b81526001600160a01b03858116600483015282811660248301526044820184905284169063d9caed1290606401600060405180830381600087803b15801561103857600080fd5b505af115801561104c573d6000803e3d6000fd5b5050505050505050565b61105e611c6c565b6106b781612101565b60ce602052816000526040600020818154811061108357600080fd5b6000918252602090912001546001600160a01b03169150829050565b600080336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af16146110ea5760405162461bcd60e51b81526004016106a59061326d565b60006110f584610d89565b600081815260cf60205260408120549192509060ff161561112a5750600081815260cf60205260409020805460ff1916905560015b92509050915091565b600054610100900460ff16158080156111535750600054600160ff909116105b8061116d5750303b15801561116d575060005460ff166001145b6111d05760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b60648201526084016106a5565b6000805460ff1916600117905580156111f3576000805461ff0019166101001790555b6111fb61216a565b60c9556112088383612201565b61121185611cc6565b61121a84612101565b8015611260576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050505050565b60cb546001600160a01b031633146112915760405162461bcd60e51b81526004016106a590613203565b82811461131a5760405162461bcd60e51b815260206004820152604b60248201527f53747261746567794d616e616765722e61646453747261746567696573546f4460448201527f65706f73697457686974656c6973743a206172726179206c656e67746873206460648201526a0de40dcdee840dac2e8c6d60ab1b608482015260a4016106a5565b8260005b818110156114885760d1600087878481811061133c5761133c6132cb565b90506020020160208101906113519190612a1e565b6001600160a01b0316815260208101919091526040016000205460ff1661148057600160d1600088888581811061138a5761138a6132cb565b905060200201602081019061139f9190612a1e565b6001600160a01b031681526020810191909152604001600020805460ff19169115159190911790557f0c35b17d91c96eb2751cd456e1252f42a386e524ef9ff26ecc9950859fdc04fe8686838181106113fa576113fa6132cb565b905060200201602081019061140f9190612a1e565b6040516001600160a01b03909116815260200160405180910390a161148086868381811061143f5761143f6132cb565b90506020020160208101906114549190612a1e565b858584818110611466576114666132cb565b905060200201602081019061147b919061335a565b611bfe565b60010161131e565b505050505050565b6098546000908190600190811614156114e75760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b60448201526064016106a5565b6114ef611816565b6114fb33868686611a2f565b91506115076001606555565b509392505050565b611517611c6c565b6001600160a01b03811661157c5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084016106a5565b6106b781611cc6565b60007f0000000000000000000000000000000000000000000000000000000000007a694614156115b6575060c95490565b6115be61216a565b905090565b609760009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015611616573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061163a9190613137565b6001600160a01b0316336001600160a01b03161461166a5760405162461bcd60e51b81526004016106a590613154565b6098541981196098541916146116e85760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c697479000000000000000060648201526084016106a5565b609881905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c906020016107ee565b6001600160a01b0381166117ad5760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a4016106a5565b609754604080516001600160a01b03928316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1609780546001600160a01b0319166001600160a01b0392909216919091179055565b600260655414156118695760405162461bcd60e51b815260206004820152601f60248201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c0060448201526064016106a5565b6002606555565b6001600160a01b0383163b1561198f57604051630b135d3f60e11b808252906001600160a01b03851690631626ba7e906118b090869086906004016133cf565b602060405180830381865afa1580156118cd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906118f191906133e8565b6001600160e01b0319161461198a5760405162461bcd60e51b815260206004820152605360248201527f454950313237315369676e61747572655574696c732e636865636b5369676e6160448201527f747572655f454950313237313a2045524331323731207369676e6174757265206064820152721d995c9a599a58d85d1a5bdb8819985a5b1959606a1b608482015260a4016106a5565b505050565b826001600160a01b03166119a383836122e7565b6001600160a01b03161461198a5760405162461bcd60e51b815260206004820152604760248201527f454950313237315369676e61747572655574696c732e636865636b5369676e6160448201527f747572655f454950313237313a207369676e6174757265206e6f742066726f6d6064820152661039b4b3b732b960c91b608482015260a4016106a5565b6001600160a01b038316600090815260d16020526040812054849060ff16611ad55760405162461bcd60e51b815260206004820152604d60248201527f53747261746567794d616e616765722e6f6e6c7953747261746567696573576860448201527f6974656c6973746564466f724465706f7369743a207374726174656779206e6f60648201526c1d081dda1a5d195b1a5cdd1959609a1b608482015260a4016106a5565b611aea6001600160a01b038516338786612303565b6040516311f9fbc960e21b81526001600160a01b038581166004830152602482018590528616906347e7ef24906044016020604051808303816000875af1158015611b39573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611b5d9190613412565b9150611b6b86858785611e74565b604051631452b9d760e11b81526001600160a01b0387811660048301528681166024830152604482018490527f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af16906328a573ae90606401600060405180830381600087803b158015611bdd57600080fd5b505af1158015611bf1573d6000803e3d6000fd5b5050505050949350505050565b604080516001600160a01b038416815282151560208201527f77d930df4937793473a95024d87a98fd2ccb9e92d3c2463b3dacd65d3e6a5786910160405180910390a16001600160a01b0391909116600090815260d360205260409020805460ff1916911515919091179055565b6033546001600160a01b03163314610bae5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016106a5565b603380546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b600081611d8d5760405162461bcd60e51b815260206004820152603e60248201527f53747261746567794d616e616765722e5f72656d6f76655368617265733a207360448201527f68617265416d6f756e742073686f756c64206e6f74206265207a65726f21000060648201526084016106a5565b6001600160a01b03808516600090815260cd602090815260408083209387168352929052205480831115611e1f5760405162461bcd60e51b815260206004820152603360248201527f53747261746567794d616e616765722e5f72656d6f76655368617265733a20736044820152720d0c2e4ca82dadeeadce840e8dede40d0d2ced606b1b60648201526084016106a5565b6001600160a01b03808616600090815260cd602090815260408083209388168352929052208382039081905590831415611e6757611e5d858561235d565b6001915050611e6d565b60009150505b9392505050565b6001600160a01b038416611ef05760405162461bcd60e51b815260206004820152603960248201527f53747261746567794d616e616765722e5f6164645368617265733a207374616b60448201527f65722063616e6e6f74206265207a65726f20616464726573730000000000000060648201526084016106a5565b80611f5c5760405162461bcd60e51b815260206004820152603660248201527f53747261746567794d616e616765722e5f6164645368617265733a207368617260448201527565732073686f756c64206e6f74206265207a65726f2160501b60648201526084016106a5565b6001600160a01b03808516600090815260cd602090815260408083209386168352929052205461206d576001600160a01b038416600090815260ce60209081526040909120541061202e5760405162461bcd60e51b815260206004820152605060248201527f53747261746567794d616e616765722e5f6164645368617265733a206465706f60448201527f73697420776f756c6420657863656564204d41585f5354414b45525f5354524160648201526f0a88a8eb2be9892a6a8be988a9c8ea8960831b608482015260a4016106a5565b6001600160a01b03848116600090815260ce602090815260408220805460018101825590835291200180546001600160a01b0319169184169190911790555b6001600160a01b03808516600090815260cd60209081526040808320938616835292905290812080548392906120a4908490613441565b9091555050604080516001600160a01b03868116825285811660208301528416818301526060810183905290517f7cfff908a4b583f36430b25d75964c458d8ede8a99bd61be750e97ee1b2f3a969181900360800190a150505050565b60cb54604080516001600160a01b03928316815291831660208301527f4264275e593955ff9d6146a51a4525f6ddace2e81db9391abcc9d1ca48047d29910160405180910390a160cb80546001600160a01b0319166001600160a01b0392909216919091179055565b604080518082018252600a81526922b4b3b2b72630bcb2b960b11b60209182015281517f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866818301527f71b625cfad44bac63b13dba07f2e1d6084ee04b6f8752101ece6126d584ee6ea81840152466060820152306080808301919091528351808303909101815260a0909101909252815191012090565b6097546001600160a01b031615801561222257506001600160a01b03821615155b6122a45760405162461bcd60e51b815260206004820152604760248201527f5061757361626c652e5f696e697469616c697a655061757365723a205f696e6960448201527f7469616c697a6550617573657228292063616e206f6e6c792062652063616c6c6064820152666564206f6e636560c81b608482015260a4016106a5565b609881905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2610ad18261171f565b60008060006122f6858561254f565b9150915061150781612595565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b179052610c039085906126e3565b6001600160a01b038216600090815260ce6020526040812054905b81811015612478576001600160a01b03848116600090815260ce60205260409020805491851691839081106123af576123af6132cb565b6000918252602090912001546001600160a01b03161415612470576001600160a01b038416600090815260ce6020526040902080546123f090600190613459565b81548110612400576124006132cb565b60009182526020808320909101546001600160a01b03878116845260ce909252604090922080549190921691908390811061243d5761243d6132cb565b9060005260206000200160006101000a8154816001600160a01b0302191690836001600160a01b03160217905550612478565b600101612378565b818114156125005760405162461bcd60e51b815260206004820152604960248201527f53747261746567794d616e616765722e5f72656d6f766553747261746567794660448201527f726f6d5374616b657253747261746567794c6973743a207374726174656779206064820152681b9bdd08199bdd5b9960ba1b608482015260a4016106a5565b6001600160a01b038416600090815260ce6020526040902080548061252757612527613470565b600082815260209020810160001990810180546001600160a01b031916905501905550505050565b6000808251604114156125865760208301516040840151606085015160001a61257a878285856127b8565b9450945050505061258e565b506000905060025b9250929050565b60008160048111156125a9576125a9613486565b14156125b25750565b60018160048111156125c6576125c6613486565b14156126145760405162461bcd60e51b815260206004820152601860248201527f45434453413a20696e76616c6964207369676e6174757265000000000000000060448201526064016106a5565b600281600481111561262857612628613486565b14156126765760405162461bcd60e51b815260206004820152601f60248201527f45434453413a20696e76616c6964207369676e6174757265206c656e6774680060448201526064016106a5565b600381600481111561268a5761268a613486565b14156106b75760405162461bcd60e51b815260206004820152602260248201527f45434453413a20696e76616c6964207369676e6174757265202773272076616c604482015261756560f01b60648201526084016106a5565b6000612738826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b031661287c9092919063ffffffff16565b9050805160001480612759575080806020019051810190612759919061319e565b61198a5760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b60648201526084016106a5565b6000807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311156127ef5750600090506003612873565b6040805160008082526020820180845289905260ff881692820192909252606081018690526080810185905260019060a0016020604051602081039080840390855afa158015612843573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b03811661286c57600060019250925050612873565b9150600090505b94509492505050565b606061288b8484600085612893565b949350505050565b6060824710156128f45760405162461bcd60e51b815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f6044820152651c8818d85b1b60d21b60648201526084016106a5565b600080866001600160a01b03168587604051612910919061349c565b60006040518083038185875af1925050503d806000811461294d576040519150601f19603f3d011682016040523d82523d6000602084013e612952565b606091505b50915091506129638783838761296e565b979650505050505050565b606083156129da5782516129d3576001600160a01b0385163b6129d35760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064016106a5565b508161288b565b61288b83838151156129ef5781518083602001fd5b8060405162461bcd60e51b81526004016106a591906134b8565b6001600160a01b03811681146106b757600080fd5b600060208284031215612a3057600080fd5b8135611e6d81612a09565b600060208284031215612a4d57600080fd5b5035919050565b8035612a5f81612a09565b919050565b634e487b7160e01b600052604160045260246000fd5b60405160c0810167ffffffffffffffff81118282101715612a9d57612a9d612a64565b60405290565b604051601f8201601f1916810167ffffffffffffffff81118282101715612acc57612acc612a64565b604052919050565b60008060008060008060c08789031215612aed57600080fd5b8635612af881612a09565b9550602087810135612b0981612a09565b9550604088013594506060880135612b2081612a09565b93506080880135925060a088013567ffffffffffffffff80821115612b4457600080fd5b818a0191508a601f830112612b5857600080fd5b813581811115612b6a57612b6a612a64565b612b7c601f8201601f19168501612aa3565b91508082528b84828501011115612b9257600080fd5b80848401858401376000848284010152508093505050509295509295509295565b80151581146106b757600080fd5b60008060408385031215612bd457600080fd5b8235612bdf81612a09565b91506020830135612bef81612bb3565b809150509250929050565b600060208284031215612c0c57600080fd5b813560ff81168114611e6d57600080fd5b60008060408385031215612c3057600080fd5b8235612c3b81612a09565b91506020830135612bef81612a09565b600080600060608486031215612c6057600080fd5b8335612c6b81612a09565b92506020840135612c7b81612a09565b929592945050506040919091013590565b600081518084526020808501945080840160005b83811015612cc55781516001600160a01b031687529582019590820190600101612ca0565b509495945050505050565b600081518084526020808501945080840160005b83811015612cc557815187529582019590820190600101612ce4565b604081526000612d136040830185612c8c565b8281036020840152612d258185612cd0565b95945050505050565b600067ffffffffffffffff821115612d4857612d48612a64565b5060051b60200190565b600082601f830112612d6357600080fd5b81356020612d78612d7383612d2e565b612aa3565b82815260059290921b84018101918181019086841115612d9757600080fd5b8286015b84811015610a92578035612dae81612a09565b8352918301918301612d9b565b600082601f830112612dcc57600080fd5b81356020612ddc612d7383612d2e565b82815260059290921b84018101918181019086841115612dfb57600080fd5b8286015b84811015610a925780358352918301918301612dff565b600060408284031215612e2857600080fd5b6040516040810181811067ffffffffffffffff82111715612e4b57612e4b612a64565b6040529050808235612e5c81612a09565b815260208301356bffffffffffffffffffffffff81168114612e7d57600080fd5b6020919091015292915050565b803563ffffffff81168114612a5f57600080fd5b600060208284031215612eb057600080fd5b813567ffffffffffffffff80821115612ec857600080fd5b9083019060e08286031215612edc57600080fd5b612ee4612a7a565b823582811115612ef357600080fd5b612eff87828601612d52565b825250602083013582811115612f1457600080fd5b612f2087828601612dbb565b602083015250612f3260408401612a54565b6040820152612f448660608501612e16565b6060820152612f5560a08401612e8a565b6080820152612f6660c08401612a54565b60a082015295945050505050565b60008083601f840112612f8657600080fd5b50813567ffffffffffffffff811115612f9e57600080fd5b6020830191508360208260051b850101111561258e57600080fd5b60008060208385031215612fcc57600080fd5b823567ffffffffffffffff811115612fe357600080fd5b612fef85828601612f74565b90969095509350505050565b6000806000806080858703121561301157600080fd5b843561301c81612a09565b9350602085013561302c81612a09565b9250604085013561303c81612a09565b9396929550929360600135925050565b6000806000806080858703121561306257600080fd5b843561306d81612a09565b9350602085013561307d81612a09565b925060408501359150606085013561309481612a09565b939692955090935050565b600080604083850312156130b257600080fd5b82356130bd81612a09565b946020939093013593505050565b600080600080604085870312156130e157600080fd5b843567ffffffffffffffff808211156130f957600080fd5b61310588838901612f74565b9096509450602087013591508082111561311e57600080fd5b5061312b87828801612f74565b95989497509550505050565b60006020828403121561314957600080fd5b8151611e6d81612a09565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b6000602082840312156131b057600080fd5b8151611e6d81612bb3565b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b606082015260800190565b60208082526044908201527f53747261746567794d616e616765722e6f6e6c7953747261746567795768697460408201527f656c69737465723a206e6f742074686520737472617465677957686974656c6960608201526339ba32b960e11b608082015260a00190565b602080825260409082018190527f53747261746567794d616e616765722e6f6e6c7944656c65676174696f6e4d61908201527f6e616765723a206e6f74207468652044656c65676174696f6e4d616e61676572606082015260800190565b634e487b7160e01b600052603260045260246000fd5b60e0815260006132f460e0830189612c8c565b82810360208401526133068189612cd0565b6001600160a01b0397881660408501528651881660608501526020909601516bffffffffffffffffffffffff166080840152505063ffffffff9290921660a083015290921660c09092019190915292915050565b60006020828403121561336c57600080fd5b8135611e6d81612bb3565b60005b8381101561339257818101518382015260200161337a565b83811115610c035750506000910152565b600081518084526133bb816020860160208601613377565b601f01601f19169290920160200192915050565b82815260406020820152600061288b60408301846133a3565b6000602082840312156133fa57600080fd5b81516001600160e01b031981168114611e6d57600080fd5b60006020828403121561342457600080fd5b5051919050565b634e487b7160e01b600052601160045260246000fd5b600082198211156134545761345461342b565b500190565b60008282101561346b5761346b61342b565b500390565b634e487b7160e01b600052603160045260246000fd5b634e487b7160e01b600052602160045260246000fd5b600082516134ae818460208701613377565b9190910192915050565b602081526000611e6d60208301846133a356fea2646970667358221220c4d14edc8209c4112dca230018f1f879727d443599b024a8f5a55e488a4e312564736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x45009dd3abbe29db54fc5d893ceaa98a624882df": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106100b45760003560e01c806340c10f191161007157806340c10f191461014157806370a082311461015657806395d89b411461017f578063a457c2d714610187578063a9059cbb1461019a578063dd62ed3e146101ad57600080fd5b806306fdde03146100b9578063095ea7b3146100d757806318160ddd146100fa57806323b872dd1461010c578063313ce5671461011f578063395093511461012e575b600080fd5b6100c16101c0565b6040516100ce9190610787565b60405180910390f35b6100ea6100e53660046107f8565b610252565b60405190151581526020016100ce565b6002545b6040519081526020016100ce565b6100ea61011a366004610822565b61026a565b604051601281526020016100ce565b6100ea61013c3660046107f8565b61028e565b61015461014f3660046107f8565b6102b0565b005b6100fe61016436600461085e565b6001600160a01b031660009081526020819052604090205490565b6100c16102be565b6100ea6101953660046107f8565b6102cd565b6100ea6101a83660046107f8565b61034d565b6100fe6101bb366004610880565b61035b565b6060600380546101cf906108b3565b80601f01602080910402602001604051908101604052809291908181526020018280546101fb906108b3565b80156102485780601f1061021d57610100808354040283529160200191610248565b820191906000526020600020905b81548152906001019060200180831161022b57829003601f168201915b5050505050905090565b600033610260818585610386565b5060019392505050565b6000336102788582856104aa565b610283858585610524565b506001949350505050565b6000336102608185856102a1838361035b565b6102ab91906108ee565b610386565b6102ba82826106c8565b5050565b6060600480546101cf906108b3565b600033816102db828661035b565b9050838110156103405760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b6102838286868403610386565b600033610260818585610524565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166103e85760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608401610337565b6001600160a01b0382166104495760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608401610337565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104b6848461035b565b9050600019811461051e57818110156105115760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610337565b61051e8484848403610386565b50505050565b6001600160a01b0383166105885760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608401610337565b6001600160a01b0382166105ea5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608401610337565b6001600160a01b038316600090815260208190526040902054818110156106625760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608401610337565b6001600160a01b03848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a361051e565b6001600160a01b03821661071e5760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610337565b806002600082825461073091906108ee565b90915550506001600160a01b038216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b600060208083528351808285015260005b818110156107b457858101830151858201604001528201610798565b818111156107c6576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b03811681146107f357600080fd5b919050565b6000806040838503121561080b57600080fd5b610814836107dc565b946020939093013593505050565b60008060006060848603121561083757600080fd5b610840846107dc565b925061084e602085016107dc565b9150604084013590509250925092565b60006020828403121561087057600080fd5b610879826107dc565b9392505050565b6000806040838503121561089357600080fd5b61089c836107dc565b91506108aa602084016107dc565b90509250929050565b600181811c908216806108c757607f821691505b602082108114156108e857634e487b7160e01b600052602260045260246000fd5b50919050565b6000821982111561090f57634e487b7160e01b600052601160045260246000fd5b50019056fea2646970667358221220ad178920ddad0e9ccdede99a0ebf77eb0e2987daf014917631edf4b9b145843b64736f6c634300080c0033", + "storage": { + "0x3": "0x7765746800000000000000000000000000000000000000000000000000000008", + "0x4": "0x5745544800000000000000000000000000000000000000000000000000000008" + } + }, + "0x4e59b44847b379578588920ca78fbf26c0b4956c": { + "nonce": 0, + "balance": "0x0", + "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", + "storage": {} + }, + "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040526004361061007b5760003560e01c80639623609d1161004e5780639623609d1461011157806399a88ec414610124578063f2fde38b14610144578063f3b7dead1461016457600080fd5b8063204e1c7a14610080578063715018a6146100bc5780637eff275e146100d35780638da5cb5b146100f3575b600080fd5b34801561008c57600080fd5b506100a061009b366004610499565b610184565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c857600080fd5b506100d1610215565b005b3480156100df57600080fd5b506100d16100ee3660046104bd565b610229565b3480156100ff57600080fd5b506000546001600160a01b03166100a0565b6100d161011f36600461050c565b610291565b34801561013057600080fd5b506100d161013f3660046104bd565b610300565b34801561015057600080fd5b506100d161015f366004610499565b610336565b34801561017057600080fd5b506100a061017f366004610499565b6103b4565b6000806000836001600160a01b03166040516101aa90635c60da1b60e01b815260040190565b600060405180830381855afa9150503d80600081146101e5576040519150601f19603f3d011682016040523d82523d6000602084013e6101ea565b606091505b5091509150816101f957600080fd5b8080602001905181019061020d91906105e2565b949350505050565b61021d6103da565b6102276000610434565b565b6102316103da565b6040516308f2839760e41b81526001600160a01b038281166004830152831690638f283970906024015b600060405180830381600087803b15801561027557600080fd5b505af1158015610289573d6000803e3d6000fd5b505050505050565b6102996103da565b60405163278f794360e11b81526001600160a01b03841690634f1ef2869034906102c990869086906004016105ff565b6000604051808303818588803b1580156102e257600080fd5b505af11580156102f6573d6000803e3d6000fd5b5050505050505050565b6103086103da565b604051631b2ce7f360e11b81526001600160a01b038281166004830152831690633659cfe69060240161025b565b61033e6103da565b6001600160a01b0381166103a85760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084015b60405180910390fd5b6103b181610434565b50565b6000806000836001600160a01b03166040516101aa906303e1469160e61b815260040190565b6000546001600160a01b031633146102275760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161039f565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6001600160a01b03811681146103b157600080fd5b6000602082840312156104ab57600080fd5b81356104b681610484565b9392505050565b600080604083850312156104d057600080fd5b82356104db81610484565b915060208301356104eb81610484565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b60008060006060848603121561052157600080fd5b833561052c81610484565b9250602084013561053c81610484565b9150604084013567ffffffffffffffff8082111561055957600080fd5b818601915086601f83011261056d57600080fd5b81358181111561057f5761057f6104f6565b604051601f8201601f19908116603f011681019083821181831017156105a7576105a76104f6565b816040528281528960208487010111156105c057600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b6000602082840312156105f457600080fd5b81516104b681610484565b60018060a01b038316815260006020604081840152835180604085015260005b8181101561063b5785810183015185820160600152820161061f565b8181111561064d576000606083870101525b50601f01601f19169290920160600194935050505056fea2646970667358221220c05fd01e9cb5a6d57ff48f40dd8e3c410a74fcc7c0c567cb80c11c5959dda6a164736f6c634300080c0033", + "storage": { + "0x0": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720" + } + }, + "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x82c6d3ed4cd33d8ec1e51d0b5cc1d822eaa0c3dc": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106103785760003560e01c806360d7faed116101d3578063b7f06ebe11610104578063cf80873e116100a2578063f16172b01161007c578063f16172b01461097d578063f2fde38b14610990578063f698da25146109a3578063fabc1cbc146109ab57600080fd5b8063cf80873e14610936578063da8be86414610957578063eea9064b1461096a57600080fd5b8063c488375a116100de578063c488375a14610853578063c5e480db14610873578063c94b511114610919578063ca661c041461092c57600080fd5b8063b7f06ebe146107f9578063bb45fef21461081c578063c448feb81461084a57600080fd5b8063886f1195116101715780639104c3191161014b5780639104c3191461078457806399be81c81461079f578063a1788484146107b2578063b1344271146107d257600080fd5b8063886f1195146107405780638da5cb5b14610753578063900413471461076457600080fd5b80636d70f7ae116101ad5780636d70f7ae146106e7578063715018a6146106fa578063778e55f3146107025780637f5480711461072d57600080fd5b806360d7faed14610698578063635bbd10146106ab57806365da1264146106be57600080fd5b806329c77d4f116102ad5780634fc40b611161024b5780635ac86ab7116102255780635ac86ab71461062e5780635c975abb146106515780635cfe8d2c146106595780635f966f141461066c57600080fd5b80634fc40b6114610609578063595c6a6714610613578063597b36da1461061b57600080fd5b80633cdeb5e0116102875780633cdeb5e0146105695780633e28391d1461059857806343377382146105bb5780634665bcda146105e257600080fd5b806329c77d4f146104f7578063334043961461051757806339b70e381461052a57600080fd5b8063136439dd1161031a5780631bbce091116102f45780631bbce0911461049757806320606b70146104aa57806322bf40e4146104d157806328a573ae146104e457600080fd5b8063136439dd146104385780631522bf021461044b578063169283651461045e57600080fd5b80630dd8dd02116103565780630dd8dd02146103dd5780630f589e59146103fd57806310d67a2f14610412578063132d49671461042557600080fd5b80630449ca391461037d57806304a4f979146103a35780630b9f487a146103ca575b600080fd5b61039061038b366004614993565b6109be565b6040519081526020015b60405180910390f35b6103907f14bde674c9f64b2ad00eaaee4a8bed1fabef35c7507e3c5b9cfc9436909a2dad81565b6103906103d83660046149f9565b610a43565b6103f06103eb366004614993565b610b05565b60405161039a9190614a54565b61041061040b366004614af1565b610e6e565b005b610410610420366004614b44565b610fbe565b610410610433366004614b68565b611071565b610410610446366004614ba9565b611128565b610410610459366004614bc2565b611267565b61039061046c366004614b44565b6001600160a01b0316600090815260996020526040902060010154600160a01b900463ffffffff1690565b6103906104a5366004614b68565b61127b565b6103907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681565b6104106104df366004614c2d565b6112a9565b6104106104f2366004614b68565b6113ed565b610390610505366004614b44565b609b6020526000908152604090205481565b610410610525366004614cd4565b61149d565b6105517f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f4881565b6040516001600160a01b03909116815260200161039a565b610551610577366004614b44565b6001600160a01b039081166000908152609960205260409020600101541690565b6105ab6105a6366004614b44565b611584565b604051901515815260200161039a565b6103907f39111bc4a4d688e1f685123d7497d4615370152a8ee4a0593e647bd06ad8bb0b81565b6105517f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e81565b6103906213c68081565b6104106115a4565b610390610629366004614ff3565b61166b565b6105ab61063c36600461502f565b606654600160ff9092169190911b9081161490565b606654610390565b6104106106673660046150a5565b61169b565b61055161067a366004614b44565b6001600160a01b039081166000908152609960205260409020541690565b6104106106a6366004615205565b611946565b6104106106b9366004614ba9565b611996565b6105516106cc366004614b44565b609a602052600090815260409020546001600160a01b031681565b6105ab6106f5366004614b44565b6119a7565b6104106119c7565b610390610710366004615294565b609860209081526000928352604080842090915290825290205481565b61041061073b366004615375565b6119db565b606554610551906001600160a01b031681565b6033546001600160a01b0316610551565b610777610772366004615405565b611ae0565b60405161039a919061548f565b61055173beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac081565b6104106107ad3660046154a2565b611bba565b6103906107c0366004614b44565b609f6020526000908152604090205481565b6105517f0000000000000000000000000c8e79f3534b00d9a3d4a856b665bf4ebc22f2ba81565b6105ab610807366004614ba9565b609e6020526000908152604090205460ff1681565b6105ab61082a3660046154d7565b609c60209081526000928352604080842090915290825290205460ff1681565b610390609d5481565b610390610861366004614b44565b60a16020526000908152604090205481565b6108e3610881366004614b44565b6040805160608082018352600080835260208084018290529284018190526001600160a01b03948516815260998352839020835191820184528054851682526001015493841691810191909152600160a01b90920463ffffffff169082015290565b6040805182516001600160a01b039081168252602080850151909116908201529181015163ffffffff169082015260600161039a565b610390610927366004615503565b611c8c565b61039062034bc081565b610949610944366004614b44565b611d45565b60405161039a929190615584565b6103f0610965366004614b44565b6120fd565b6104106109783660046155a9565b6125c1565b61041061098b366004615601565b6125cd565b61041061099e366004614b44565b61265e565b6103906126d4565b6104106109b9366004614ba9565b612712565b609d54600090815b83811015610a3b57600060a160008787858181106109e6576109e661561d565b90506020020160208101906109fb9190614b44565b6001600160a01b03166001600160a01b0316815260200190815260200160002054905082811115610a2a578092505b50610a3481615649565b90506109c6565b509392505050565b604080517f14bde674c9f64b2ad00eaaee4a8bed1fabef35c7507e3c5b9cfc9436909a2dad6020808301919091526001600160a01b038681168385015288811660608401528716608083015260a0820185905260c08083018590528351808403909101815260e0909201909252805191012060009081610ac16126d4565b60405161190160f01b602082015260228101919091526042810183905260620160408051808303601f19018152919052805160209091012098975050505050505050565b60665460609060019060029081161415610b3a5760405162461bcd60e51b8152600401610b3190615664565b60405180910390fd5b6000836001600160401b03811115610b5457610b54614d76565b604051908082528060200260200182016040528015610b7d578160200160208202803683370190505b50336000908152609a60205260408120549192506001600160a01b03909116905b85811015610e6357868682818110610bb857610bb861561d565b9050602002810190610bca919061569b565b610bd89060208101906156bb565b9050878783818110610bec57610bec61561d565b9050602002810190610bfe919061569b565b610c0890806156bb565b905014610c7d5760405162461bcd60e51b815260206004820152603860248201527f44656c65676174696f6e4d616e616765722e717565756557697468647261776160448201527f6c3a20696e707574206c656e677468206d69736d6174636800000000000000006064820152608401610b31565b33878783818110610c9057610c9061561d565b9050602002810190610ca2919061569b565b610cb3906060810190604001614b44565b6001600160a01b031614610d2f5760405162461bcd60e51b815260206004820152603c60248201527f44656c65676174696f6e4d616e616765722e717565756557697468647261776160448201527f6c3a2077697468647261776572206d757374206265207374616b6572000000006064820152608401610b31565b610e343383898985818110610d4657610d4661561d565b9050602002810190610d58919061569b565b610d69906060810190604001614b44565b8a8a86818110610d7b57610d7b61561d565b9050602002810190610d8d919061569b565b610d9790806156bb565b808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152508e92508d9150889050818110610ddd57610ddd61561d565b9050602002810190610def919061569b565b610dfd9060208101906156bb565b8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061286e92505050565b838281518110610e4657610e4661561d565b602090810291909101015280610e5b81615649565b915050610b9e565b509095945050505050565b336000908152609960205260409020546001600160a01b031615610f085760405162461bcd60e51b815260206004820152604560248201527f44656c65676174696f6e4d616e616765722e726567697374657241734f70657260448201527f61746f723a206f70657261746f722068617320616c72656164792072656769736064820152641d195c995960da1b608482015260a401610b31565b610f123384612e2e565b604080518082019091526060815260006020820152610f3433808360006130ca565b336001600160a01b03167f8e8485583a2310d41f7c82b9427d0bd49bad74bb9cff9d3402a29d8f9b28a0e285604051610f6d9190615704565b60405180910390a2336001600160a01b03167f02a919ed0e2acad1dd90f17ef2fa4ae5462ee1339170034a8531cca4b67080908484604051610fb0929190615756565b60405180910390a250505050565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015611011573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110359190615785565b6001600160a01b0316336001600160a01b0316146110655760405162461bcd60e51b8152600401610b31906157a2565b61106e8161346f565b50565b336001600160a01b037f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f481614806110d05750336001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e16145b6110ec5760405162461bcd60e51b8152600401610b31906157ec565b6110f583611584565b15611123576001600160a01b038084166000908152609a60205260409020541661112181858585613566565b505b505050565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa158015611170573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111949190615849565b6111b05760405162461bcd60e51b8152600401610b3190615866565b606654818116146112295760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c69747900000000000000006064820152608401610b31565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b61126f6135e1565b6111218484848461363b565b6001600160a01b0383166000908152609b60205260408120546112a085828686611c8c565b95945050505050565b600054610100900460ff16158080156112c95750600054600160ff909116105b806112e35750303b1580156112e3575060005460ff166001145b6113465760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b6064820152608401610b31565b6000805460ff191660011790558015611369576000805461ff0019166101001790555b6113738888613859565b61137b61393f565b609755611387896139d6565b61139086613a28565b61139c8585858561363b565b80156113e2576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b505050505050505050565b336001600160a01b037f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f4816148061144c5750336001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e16145b6114685760405162461bcd60e51b8152600401610b31906157ec565b61147183611584565b15611123576001600160a01b038084166000908152609a60205260409020541661112181858585613b22565b606654600290600490811614156114c65760405162461bcd60e51b8152600401610b3190615664565b6114ce613b9d565b60005b88811015611579576115698a8a838181106114ee576114ee61561d565b905060200281019061150091906158ae565b8989848181106115125761151261561d565b905060200281019061152491906156bb565b8989868181106115365761153661561d565b9050602002013588888781811061154f5761154f61561d565b905060200201602081019061156491906158c4565b613bf7565b61157281615649565b90506114d1565b506113e2600160c955565b6001600160a01b039081166000908152609a602052604090205416151590565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa1580156115ec573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906116109190615849565b61162c5760405162461bcd60e51b8152600401610b3190615866565b600019606681905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b60008160405160200161167e9190615955565b604051602081830303815290604052805190602001209050919050565b60005b81518110156119425760008282815181106116bb576116bb61561d565b602002602001015190506000807f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f486001600160a01b031663cd293f6f846040518263ffffffff1660e01b81526004016117149190615968565b60408051808303816000875af1158015611732573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117569190615a14565b915091508115611934576040808401516001600160a01b0381166000908152609f6020529182208054919282919061178d83615649565b919050555060006040518060e00160405280846001600160a01b031681526020018760a001516001600160a01b031681526020018760600151600001516001600160a01b03168152602001838152602001876080015163ffffffff1681526020018760000151815260200187602001518152509050600061180d8261166b565b6000818152609e602052604090205490915060ff16156118a35760405162461bcd60e51b815260206004820152604560248201527f44656c65676174696f6e4d616e616765722e6d6967726174655175657565645760448201527f69746864726177616c733a207769746864726177616c20616c72656164792065606482015264786973747360d81b608482015260a401610b31565b6000818152609e602052604090819020805460ff19166001179055517f9009ab153e8014fbfb02f2217f5cde7aa7f9ad734ae85ca3ee3f4ca2fdd499f9906118ee9083908590615a42565b60405180910390a160408051868152602081018390527fdc00758b65eef71dc3780c04ebe36cab6bdb266c3a698187e29e0f0dca012630910160405180910390a1505050505b83600101935050505061169e565b5050565b6066546002906004908116141561196f5760405162461bcd60e51b8152600401610b3190615664565b611977613b9d565b6119848686868686613bf7565b61198e600160c955565b505050505050565b61199e6135e1565b61106e81613a28565b6001600160a01b0390811660009081526099602052604090205416151590565b6119cf6135e1565b6119d960006139d6565b565b4283602001511015611a5f5760405162461bcd60e51b815260206004820152604160248201527f44656c65676174696f6e4d616e616765722e64656c6567617465546f4279536960448201527f676e61747572653a207374616b6572207369676e6174757265206578706972656064820152601960fa1b608482015260a401610b31565b6000609b6000876001600160a01b03166001600160a01b031681526020019081526020016000205490506000611a9b8783888860200151611c8c565b6001600160a01b0388166000908152609b602052604090206001840190558551909150611acb90889083906143e1565b611ad7878786866130ca565b50505050505050565b6060600082516001600160401b03811115611afd57611afd614d76565b604051908082528060200260200182016040528015611b26578160200160208202803683370190505b50905060005b8351811015610a3b576001600160a01b03851660009081526098602052604081208551909190869084908110611b6457611b6461561d565b60200260200101516001600160a01b03166001600160a01b0316815260200190815260200160002054828281518110611b9f57611b9f61561d565b6020908102919091010152611bb381615649565b9050611b2c565b611bc3336119a7565b611c455760405162461bcd60e51b815260206004820152604760248201527f44656c65676174696f6e4d616e616765722e7570646174654f70657261746f7260448201527f4d657461646174615552493a2063616c6c6572206d75737420626520616e206f6064820152663832b930ba37b960c91b608482015260a401610b31565b336001600160a01b03167f02a919ed0e2acad1dd90f17ef2fa4ae5462ee1339170034a8531cca4b67080908383604051611c80929190615756565b60405180910390a25050565b604080517f39111bc4a4d688e1f685123d7497d4615370152a8ee4a0593e647bd06ad8bb0b6020808301919091526001600160a01b0387811683850152851660608301526080820186905260a08083018590528351808403909101815260c0909201909252805191012060009081611d026126d4565b60405161190160f01b602082015260228101919091526042810183905260620160408051808303601f190181529190528051602090910120979650505050505050565b6040516360f4062b60e01b81526001600160a01b03828116600483015260609182916000917f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e909116906360f4062b90602401602060405180830381865afa158015611db5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611dd99190615a5b565b6040516394f649dd60e01b81526001600160a01b03868116600483015291925060009182917f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f48909116906394f649dd90602401600060405180830381865afa158015611e49573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611e719190810190615acf565b9150915060008313611e8857909590945092505050565b606080835160001415611f42576040805160018082528183019092529060208083019080368337505060408051600180825281830190925292945090506020808301908036833701905050905073beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac082600081518110611efd57611efd61561d565b60200260200101906001600160a01b031690816001600160a01b0316815250508481600081518110611f3157611f3161561d565b6020026020010181815250506120f0565b8351611f4f906001615b89565b6001600160401b03811115611f6657611f66614d76565b604051908082528060200260200182016040528015611f8f578160200160208202803683370190505b50915081516001600160401b03811115611fab57611fab614d76565b604051908082528060200260200182016040528015611fd4578160200160208202803683370190505b50905060005b845181101561206e57848181518110611ff557611ff561561d565b602002602001015183828151811061200f5761200f61561d565b60200260200101906001600160a01b031690816001600160a01b0316815250508381815181106120415761204161561d565b602002602001015182828151811061205b5761205b61561d565b6020908102919091010152600101611fda565b5073beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac082600184516120939190615ba1565b815181106120a3576120a361561d565b60200260200101906001600160a01b031690816001600160a01b0316815250508481600184516120d39190615ba1565b815181106120e3576120e361561d565b6020026020010181815250505b9097909650945050505050565b606654606090600190600290811614156121295760405162461bcd60e51b8152600401610b3190615664565b61213283611584565b6121b25760405162461bcd60e51b8152602060048201526044602482018190527f44656c65676174696f6e4d616e616765722e756e64656c65676174653a207374908201527f616b6572206d7573742062652064656c65676174656420746f20756e64656c656064820152636761746560e01b608482015260a401610b31565b6121bb836119a7565b1561222e5760405162461bcd60e51b815260206004820152603d60248201527f44656c65676174696f6e4d616e616765722e756e64656c65676174653a206f7060448201527f657261746f72732063616e6e6f7420626520756e64656c6567617465640000006064820152608401610b31565b6001600160a01b0383166122aa5760405162461bcd60e51b815260206004820152603c60248201527f44656c65676174696f6e4d616e616765722e756e64656c65676174653a20636160448201527f6e6e6f7420756e64656c6567617465207a65726f2061646472657373000000006064820152608401610b31565b6001600160a01b038084166000818152609a6020526040902054909116903314806122dd5750336001600160a01b038216145b8061230457506001600160a01b038181166000908152609960205260409020600101541633145b6123765760405162461bcd60e51b815260206004820152603d60248201527f44656c65676174696f6e4d616e616765722e756e64656c65676174653a20636160448201527f6c6c65722063616e6e6f7420756e64656c6567617465207374616b65720000006064820152608401610b31565b60008061238286611d45565b9092509050336001600160a01b038716146123d857826001600160a01b0316866001600160a01b03167ff0eddf07e6ea14f388b47e1e94a0f464ecbd9eed4171130e0fc0e99fb4030a8a60405160405180910390a35b826001600160a01b0316866001600160a01b03167ffee30966a256b71e14bc0ebfc94315e28ef4a97a7131a9e2b7a310a73af4467660405160405180910390a36001600160a01b0386166000908152609a6020526040902080546001600160a01b0319169055815161245a5760408051600081526020810190915294506125b8565b81516001600160401b0381111561247357612473614d76565b60405190808252806020026020018201604052801561249c578160200160208202803683370190505b50945060005b82518110156125b6576040805160018082528183019092526000916020808301908036833750506040805160018082528183019092529293506000929150602080830190803683370190505090508483815181106125025761250261561d565b60200260200101518260008151811061251d5761251d61561d565b60200260200101906001600160a01b031690816001600160a01b03168152505083838151811061254f5761254f61561d565b60200260200101518160008151811061256a5761256a61561d565b60200260200101818152505061258389878b858561286e565b8884815181106125955761259561561d565b602002602001018181525050505080806125ae90615649565b9150506124a2565b505b50505050919050565b611123338484846130ca565b6125d6336119a7565b6126545760405162461bcd60e51b815260206004820152604360248201527f44656c65676174696f6e4d616e616765722e6d6f646966794f70657261746f7260448201527f44657461696c733a2063616c6c6572206d75737420626520616e206f706572616064820152623a37b960e91b608482015260a401610b31565b61106e3382612e2e565b6126666135e1565b6001600160a01b0381166126cb5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b6064820152608401610b31565b61106e816139d6565b60007f0000000000000000000000000000000000000000000000000000000000007a69461415612705575060975490565b61270d61393f565b905090565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015612765573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127899190615785565b6001600160a01b0316336001600160a01b0316146127b95760405162461bcd60e51b8152600401610b31906157a2565b6066541981196066541916146128375760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c69747900000000000000006064820152608401610b31565b606681905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c9060200161125c565b60006001600160a01b0386166129055760405162461bcd60e51b815260206004820152605060248201527f44656c65676174696f6e4d616e616765722e5f72656d6f76655368617265734160448201527f6e6451756575655769746864726177616c3a207374616b65722063616e6e6f7460648201526f206265207a65726f206164647265737360801b608482015260a401610b31565b825161298f5760405162461bcd60e51b815260206004820152604d60248201527f44656c65676174696f6e4d616e616765722e5f72656d6f76655368617265734160448201527f6e6451756575655769746864726177616c3a207374726174656769657320636160648201526c6e6e6f7420626520656d70747960981b608482015260a401610b31565b60005b8351811015612d3c576001600160a01b038616156129e8576129e886888684815181106129c1576129c161561d565b60200260200101518685815181106129db576129db61561d565b6020026020010151613566565b73beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac06001600160a01b0316848281518110612a1857612a1861561d565b60200260200101516001600160a01b03161415612ae1577f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031663beffbb8988858481518110612a7157612a7161561d565b60200260200101516040518363ffffffff1660e01b8152600401612aaa9291906001600160a01b03929092168252602082015260400190565b600060405180830381600087803b158015612ac457600080fd5b505af1158015612ad8573d6000803e3d6000fd5b50505050612d34565b846001600160a01b0316876001600160a01b03161480612bb357507f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f486001600160a01b0316639b4da03d858381518110612b3d57612b3d61561d565b60200260200101516040518263ffffffff1660e01b8152600401612b7091906001600160a01b0391909116815260200190565b602060405180830381865afa158015612b8d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612bb19190615849565b155b612c7f5760405162461bcd60e51b8152602060048201526084602482018190527f44656c65676174696f6e4d616e616765722e5f72656d6f76655368617265734160448301527f6e6451756575655769746864726177616c3a2077697468647261776572206d7560648301527f73742062652073616d652061646472657373206173207374616b657220696620908201527f746869726450617274795472616e7366657273466f7262696464656e2061726560a482015263081cd95d60e21b60c482015260e401610b31565b7f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f486001600160a01b0316638c80d4e588868481518110612cc157612cc161561d565b6020026020010151868581518110612cdb57612cdb61561d565b60200260200101516040518463ffffffff1660e01b8152600401612d0193929190615bb8565b600060405180830381600087803b158015612d1b57600080fd5b505af1158015612d2f573d6000803e3d6000fd5b505050505b600101612992565b506001600160a01b0386166000908152609f60205260408120805491829190612d6483615649565b919050555060006040518060e00160405280896001600160a01b03168152602001886001600160a01b03168152602001876001600160a01b031681526020018381526020014363ffffffff1681526020018681526020018581525090506000612dcc8261166b565b6000818152609e602052604090819020805460ff19166001179055519091507f9009ab153e8014fbfb02f2217f5cde7aa7f9ad734ae85ca3ee3f4ca2fdd499f990612e1a9083908590615a42565b60405180910390a198975050505050505050565b6000612e3d6020830183614b44565b6001600160a01b03161415612ed75760405162461bcd60e51b815260206004820152605460248201527f44656c65676174696f6e4d616e616765722e5f7365744f70657261746f72446560448201527f7461696c733a2063616e6e6f742073657420606561726e696e677352656365696064820152737665726020746f207a65726f206164647265737360601b608482015260a401610b31565b6213c680612eeb6060830160408401615bdc565b63ffffffff161115612fa05760405162461bcd60e51b815260206004820152606c60248201527f44656c65676174696f6e4d616e616765722e5f7365744f70657261746f72446560448201527f7461696c733a207374616b65724f70744f757457696e646f77426c6f636b732060648201527f63616e6e6f74206265203e204d41585f5354414b45525f4f50545f4f55545f5760848201526b494e444f575f424c4f434b5360a01b60a482015260c401610b31565b6001600160a01b0382166000908152609960205260409081902060010154600160a01b900463ffffffff1690612fdc9060608401908401615bdc565b63ffffffff1610156130725760405162461bcd60e51b815260206004820152605360248201527f44656c65676174696f6e4d616e616765722e5f7365744f70657261746f72446560448201527f7461696c733a207374616b65724f70744f757457696e646f77426c6f636b732060648201527218d85b9b9bdd08189948191958dc99585cd959606a1b608482015260a401610b31565b6001600160a01b038216600090815260996020526040902081906130968282615c19565b505060405133907ffebe5cd24b2cbc7b065b9d0fdeb904461e4afcff57dd57acda1e7832031ba7ac90611c80908490615704565b606654600090600190811614156130f35760405162461bcd60e51b8152600401610b3190615664565b6130fc85611584565b156131795760405162461bcd60e51b815260206004820152604160248201527f44656c65676174696f6e4d616e616765722e5f64656c65676174653a2073746160448201527f6b657220697320616c7265616479206163746976656c792064656c65676174656064820152601960fa1b608482015260a401610b31565b613182846119a7565b6132025760405162461bcd60e51b815260206004820152604560248201527f44656c65676174696f6e4d616e616765722e5f64656c65676174653a206f706560448201527f7261746f72206973206e6f74207265676973746572656420696e20456967656e6064820152642630bcb2b960d91b608482015260a401610b31565b6001600160a01b038085166000908152609960205260409020600101541680158015906132385750336001600160a01b03821614155b801561324d5750336001600160a01b03861614155b156133ba5742846020015110156132cc5760405162461bcd60e51b815260206004820152603760248201527f44656c65676174696f6e4d616e616765722e5f64656c65676174653a2061707060448201527f726f766572207369676e617475726520657870697265640000000000000000006064820152608401610b31565b6001600160a01b0381166000908152609c6020908152604080832086845290915290205460ff16156133665760405162461bcd60e51b815260206004820152603760248201527f44656c65676174696f6e4d616e616765722e5f64656c65676174653a2061707060448201527f726f76657253616c7420616c7265616479207370656e740000000000000000006064820152608401610b31565b6001600160a01b0381166000908152609c6020908152604080832086845282528220805460ff191660011790558501516133a7908890889085908890610a43565b90506133b8828287600001516143e1565b505b6001600160a01b038681166000818152609a602052604080822080546001600160a01b031916948a169485179055517fc3ee9f2e5fda98e8066a1f745b2df9285f416fe98cf2559cd21484b3d87433049190a360008061341988611d45565b9150915060005b82518110156113e257613467888a8584815181106134405761344061561d565b602002602001015185858151811061345a5761345a61561d565b6020026020010151613b22565b600101613420565b6001600160a01b0381166134fd5760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a401610b31565b606554604080516001600160a01b03928316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1606580546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b0380851660009081526098602090815260408083209386168352929052908120805483929061359d908490615ba1565b92505081905550836001600160a01b03167f6909600037b75d7b4733aedd815442b5ec018a827751c832aaff64eba5d6d2dd848484604051610fb093929190615bb8565b6033546001600160a01b031633146119d95760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610b31565b8281146136c35760405162461bcd60e51b815260206004820152604a60248201527f44656c65676174696f6e4d616e616765722e5f7365745374726174656779576960448201527f746864726177616c44656c6179426c6f636b733a20696e707574206c656e67746064820152690d040dad2e6dac2e8c6d60b31b608482015260a401610b31565b8260005b8181101561198e5760008686838181106136e3576136e361561d565b90506020020160208101906136f89190614b44565b6001600160a01b038116600090815260a160205260408120549192508686858181106137265761372661561d565b90506020020135905062034bc08111156137ea5760405162461bcd60e51b815260206004820152607360248201527f44656c65676174696f6e4d616e616765722e5f7365745374726174656779576960448201527f746864726177616c44656c6179426c6f636b733a205f7769746864726177616c60648201527f44656c6179426c6f636b732063616e6e6f74206265203e204d41585f5749544860848201527244524157414c5f44454c41595f424c4f434b5360681b60a482015260c401610b31565b6001600160a01b038316600081815260a160209081526040918290208490558151928352820184905281018290527f0e7efa738e8b0ce6376a0c1af471655540d2e9a81647d7b09ed823018426576d9060600160405180910390a15050508061385290615649565b90506136c7565b6065546001600160a01b031615801561387a57506001600160a01b03821615155b6138fc5760405162461bcd60e51b815260206004820152604760248201527f5061757361626c652e5f696e697469616c697a655061757365723a205f696e6960448201527f7469616c697a6550617573657228292063616e206f6e6c792062652063616c6c6064820152666564206f6e636560c81b608482015260a401610b31565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a26119428261346f565b604080518082018252600a81526922b4b3b2b72630bcb2b960b11b60209182015281517f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866818301527f71b625cfad44bac63b13dba07f2e1d6084ee04b6f8752101ece6126d584ee6ea81840152466060820152306080808301919091528351808303909101815260a0909101909252815191012090565b603380546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b62034bc0811115613ae15760405162461bcd60e51b815260206004820152607160248201527f44656c65676174696f6e4d616e616765722e5f7365744d696e5769746864726160448201527f77616c44656c6179426c6f636b733a205f6d696e5769746864726177616c446560648201527f6c6179426c6f636b732063616e6e6f74206265203e204d41585f5749544844526084820152704157414c5f44454c41595f424c4f434b5360781b60a482015260c401610b31565b609d5460408051918252602082018390527fafa003cd76f87ff9d62b35beea889920f33c0c42b8d45b74954d61d50f4b6b69910160405180910390a1609d55565b6001600160a01b03808516600090815260986020908152604080832093861683529290529081208054839290613b59908490615b89565b92505081905550836001600160a01b03167f1ec042c965e2edd7107b51188ee0f383e22e76179041ab3a9d18ff151405166c848484604051610fb093929190615bb8565b600260c9541415613bf05760405162461bcd60e51b815260206004820152601f60248201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c006044820152606401610b31565b600260c955565b6000613c0561062987615c7c565b6000818152609e602052604090205490915060ff16613c865760405162461bcd60e51b81526020600482015260436024820152600080516020615db483398151915260448201527f645769746864726177616c3a20616374696f6e206973206e6f7420696e20717560648201526265756560e81b608482015260a401610b31565b609d544390613c9b60a0890160808a01615bdc565b63ffffffff16613cab9190615b89565b1115613d335760405162461bcd60e51b815260206004820152605f6024820152600080516020615db483398151915260448201527f645769746864726177616c3a206d696e5769746864726177616c44656c61794260648201527f6c6f636b7320706572696f6420686173206e6f74207965742070617373656400608482015260a401610b31565b613d436060870160408801614b44565b6001600160a01b0316336001600160a01b031614613dd05760405162461bcd60e51b81526020600482015260506024820152600080516020615db483398151915260448201527f645769746864726177616c3a206f6e6c7920776974686472617765722063616e60648201526f1031b7b6b83632ba329030b1ba34b7b760811b608482015260a401610b31565b8115613e5257613de360a08701876156bb565b85149050613e525760405162461bcd60e51b81526020600482015260426024820152600080516020615db483398151915260448201527f645769746864726177616c3a20696e707574206c656e677468206d69736d61746064820152610c6d60f31b608482015260a401610b31565b6000818152609e60205260409020805460ff191690558115613fb75760005b613e7e60a08801886156bb565b9050811015613fb1574360a16000613e9960a08b018b6156bb565b85818110613ea957613ea961561d565b9050602002016020810190613ebe9190614b44565b6001600160a01b03168152602081019190915260400160002054613ee860a08a0160808b01615bdc565b63ffffffff16613ef89190615b89565b1115613f165760405162461bcd60e51b8152600401610b3190615c8e565b613fa9613f266020890189614b44565b33613f3460a08b018b6156bb565b85818110613f4457613f4461561d565b9050602002016020810190613f599190614b44565b613f6660c08c018c6156bb565b86818110613f7657613f7661561d565b905060200201358a8a87818110613f8f57613f8f61561d565b9050602002016020810190613fa49190614b44565b61459b565b600101613e71565b506143a6565b336000908152609a60205260408120546001600160a01b0316905b613fdf60a08901896156bb565b90508110156143a3574360a16000613ffa60a08c018c6156bb565b8581811061400a5761400a61561d565b905060200201602081019061401f9190614b44565b6001600160a01b0316815260208101919091526040016000205461404960a08b0160808c01615bdc565b63ffffffff166140599190615b89565b11156140775760405162461bcd60e51b8152600401610b3190615c8e565b73beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac061409960a08a018a6156bb565b838181106140a9576140a961561d565b90506020020160208101906140be9190614b44565b6001600160a01b0316141561420e5760006140dc60208a018a614b44565b905060006001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e16630e81073c8361411d60c08e018e6156bb565b8781811061412d5761412d61561d565b6040516001600160e01b031960e087901b1681526001600160a01b03909416600485015260200291909101356024830152506044016020604051808303816000875af1158015614181573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906141a59190615a5b565b6001600160a01b038084166000908152609a60205260409020549192501680156142065761420681846141db60a08f018f6156bb565b888181106141eb576141eb61561d565b90506020020160208101906142009190614b44565b85613b22565b50505061439b565b7f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f486001600160a01b031663c4623ea1338989858181106142505761425061561d565b90506020020160208101906142659190614b44565b61427260a08d018d6156bb565b868181106142825761428261561d565b90506020020160208101906142979190614b44565b6142a460c08e018e6156bb565b878181106142b4576142b461561d565b60405160e088901b6001600160e01b03191681526001600160a01b03968716600482015294861660248601529290941660448401526020909102013560648201526084019050600060405180830381600087803b15801561431457600080fd5b505af1158015614328573d6000803e3d6000fd5b505050506001600160a01b0382161561439b5761439b823361434d60a08c018c6156bb565b8581811061435d5761435d61561d565b90506020020160208101906143729190614b44565b61437f60c08d018d6156bb565b8681811061438f5761438f61561d565b90506020020135613b22565b600101613fd2565b50505b6040518181527fc97098c2f658800b4df29001527f7324bcdffcf6e8751a699ab920a1eced5b1d9060200160405180910390a1505050505050565b6001600160a01b0383163b156144fb57604051630b135d3f60e11b808252906001600160a01b03851690631626ba7e906144219086908690600401615d16565b602060405180830381865afa15801561443e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906144629190615d73565b6001600160e01b031916146111235760405162461bcd60e51b815260206004820152605360248201527f454950313237315369676e61747572655574696c732e636865636b5369676e6160448201527f747572655f454950313237313a2045524331323731207369676e6174757265206064820152721d995c9a599a58d85d1a5bdb8819985a5b1959606a1b608482015260a401610b31565b826001600160a01b031661450f83836146db565b6001600160a01b0316146111235760405162461bcd60e51b815260206004820152604760248201527f454950313237315369676e61747572655574696c732e636865636b5369676e6160448201527f747572655f454950313237313a207369676e6174757265206e6f742066726f6d6064820152661039b4b3b732b960c91b608482015260a401610b31565b6001600160a01b03831673beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac014156146465760405162387b1360e81b81526001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e169063387b13009061460f90889088908790600401615bb8565b600060405180830381600087803b15801561462957600080fd5b505af115801561463d573d6000803e3d6000fd5b505050506146d4565b60405163c608c7f360e01b81526001600160a01b03858116600483015284811660248301526044820184905282811660648301527f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f48169063c608c7f390608401600060405180830381600087803b1580156146c057600080fd5b505af11580156113e2573d6000803e3d6000fd5b5050505050565b60008060006146ea85856146f7565b91509150610a3b8161473d565b60008082516041141561472e5760208301516040840151606085015160001a6147228782858561488b565b94509450505050614736565b506000905060025b9250929050565b600081600481111561475157614751615d9d565b141561475a5750565b600181600481111561476e5761476e615d9d565b14156147bc5760405162461bcd60e51b815260206004820152601860248201527f45434453413a20696e76616c6964207369676e617475726500000000000000006044820152606401610b31565b60028160048111156147d0576147d0615d9d565b141561481e5760405162461bcd60e51b815260206004820152601f60248201527f45434453413a20696e76616c6964207369676e6174757265206c656e677468006044820152606401610b31565b600381600481111561483257614832615d9d565b141561106e5760405162461bcd60e51b815260206004820152602260248201527f45434453413a20696e76616c6964207369676e6174757265202773272076616c604482015261756560f01b6064820152608401610b31565b6000807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311156148c25750600090506003614946565b6040805160008082526020820180845289905260ff881692820192909252606081018690526080810185905260019060a0016020604051602081039080840390855afa158015614916573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b03811661493f57600060019250925050614946565b9150600090505b94509492505050565b60008083601f84011261496157600080fd5b5081356001600160401b0381111561497857600080fd5b6020830191508360208260051b850101111561473657600080fd5b600080602083850312156149a657600080fd5b82356001600160401b038111156149bc57600080fd5b6149c88582860161494f565b90969095509350505050565b6001600160a01b038116811461106e57600080fd5b80356149f4816149d4565b919050565b600080600080600060a08688031215614a1157600080fd5b8535614a1c816149d4565b94506020860135614a2c816149d4565b93506040860135614a3c816149d4565b94979396509394606081013594506080013592915050565b6020808252825182820181905260009190848201906040850190845b81811015614a8c57835183529284019291840191600101614a70565b50909695505050505050565b600060608284031215614aaa57600080fd5b50919050565b60008083601f840112614ac257600080fd5b5081356001600160401b03811115614ad957600080fd5b60208301915083602082850101111561473657600080fd5b600080600060808486031215614b0657600080fd5b614b108585614a98565b925060608401356001600160401b03811115614b2b57600080fd5b614b3786828701614ab0565b9497909650939450505050565b600060208284031215614b5657600080fd5b8135614b61816149d4565b9392505050565b600080600060608486031215614b7d57600080fd5b8335614b88816149d4565b92506020840135614b98816149d4565b929592945050506040919091013590565b600060208284031215614bbb57600080fd5b5035919050565b60008060008060408587031215614bd857600080fd5b84356001600160401b0380821115614bef57600080fd5b614bfb8883890161494f565b90965094506020870135915080821115614c1457600080fd5b50614c218782880161494f565b95989497509550505050565b60008060008060008060008060c0898b031215614c4957600080fd5b8835614c54816149d4565b97506020890135614c64816149d4565b9650604089013595506060890135945060808901356001600160401b0380821115614c8e57600080fd5b614c9a8c838d0161494f565b909650945060a08b0135915080821115614cb357600080fd5b50614cc08b828c0161494f565b999c989b5096995094979396929594505050565b6000806000806000806000806080898b031215614cf057600080fd5b88356001600160401b0380821115614d0757600080fd5b614d138c838d0161494f565b909a50985060208b0135915080821115614d2c57600080fd5b614d388c838d0161494f565b909850965060408b0135915080821115614d5157600080fd5b614d5d8c838d0161494f565b909650945060608b0135915080821115614cb357600080fd5b634e487b7160e01b600052604160045260246000fd5b60405160e081016001600160401b0381118282101715614dae57614dae614d76565b60405290565b604080519081016001600160401b0381118282101715614dae57614dae614d76565b60405160c081016001600160401b0381118282101715614dae57614dae614d76565b604051601f8201601f191681016001600160401b0381118282101715614e2057614e20614d76565b604052919050565b63ffffffff8116811461106e57600080fd5b80356149f481614e28565b60006001600160401b03821115614e5e57614e5e614d76565b5060051b60200190565b600082601f830112614e7957600080fd5b81356020614e8e614e8983614e45565b614df8565b82815260059290921b84018101918181019086841115614ead57600080fd5b8286015b84811015614ed1578035614ec4816149d4565b8352918301918301614eb1565b509695505050505050565b600082601f830112614eed57600080fd5b81356020614efd614e8983614e45565b82815260059290921b84018101918181019086841115614f1c57600080fd5b8286015b84811015614ed15780358352918301918301614f20565b600060e08284031215614f4957600080fd5b614f51614d8c565b9050614f5c826149e9565b8152614f6a602083016149e9565b6020820152614f7b604083016149e9565b604082015260608201356060820152614f9660808301614e3a565b608082015260a08201356001600160401b0380821115614fb557600080fd5b614fc185838601614e68565b60a084015260c0840135915080821115614fda57600080fd5b50614fe784828501614edc565b60c08301525092915050565b60006020828403121561500557600080fd5b81356001600160401b0381111561501b57600080fd5b61502784828501614f37565b949350505050565b60006020828403121561504157600080fd5b813560ff81168114614b6157600080fd5b60006040828403121561506457600080fd5b61506c614db4565b90508135615079816149d4565b815260208201356bffffffffffffffffffffffff8116811461509a57600080fd5b602082015292915050565b600060208083850312156150b857600080fd5b82356001600160401b03808211156150cf57600080fd5b818501915085601f8301126150e357600080fd5b81356150f1614e8982614e45565b81815260059190911b8301840190848101908883111561511057600080fd5b8585015b838110156151ea5780358581111561512c5760008081fd5b860160e0818c03601f19018113156151445760008081fd5b61514c614dd6565b898301358881111561515e5760008081fd5b61516c8e8c83870101614e68565b825250604080840135898111156151835760008081fd5b6151918f8d83880101614edc565b8c8401525060606151a38186016149e9565b82840152608091506151b78f838701615052565b908301526151c760c08501614e3a565b908201526151d68383016149e9565b60a082015285525050918601918601615114565b5098975050505050505050565b801515811461106e57600080fd5b60008060008060006080868803121561521d57600080fd5b85356001600160401b038082111561523457600080fd5b9087019060e0828a03121561524857600080fd5b9095506020870135908082111561525e57600080fd5b5061526b8882890161494f565b909550935050604086013591506060860135615286816151f7565b809150509295509295909350565b600080604083850312156152a757600080fd5b82356152b2816149d4565b915060208301356152c2816149d4565b809150509250929050565b6000604082840312156152df57600080fd5b6152e7614db4565b905081356001600160401b038082111561530057600080fd5b818401915084601f83011261531457600080fd5b813560208282111561532857615328614d76565b61533a601f8301601f19168201614df8565b9250818352868183860101111561535057600080fd5b8181850182850137600081838501015282855280860135818601525050505092915050565b600080600080600060a0868803121561538d57600080fd5b8535615398816149d4565b945060208601356153a8816149d4565b935060408601356001600160401b03808211156153c457600080fd5b6153d089838a016152cd565b945060608801359150808211156153e657600080fd5b506153f3888289016152cd565b95989497509295608001359392505050565b6000806040838503121561541857600080fd5b8235615423816149d4565b915060208301356001600160401b0381111561543e57600080fd5b61544a85828601614e68565b9150509250929050565b600081518084526020808501945080840160005b8381101561548457815187529582019590820190600101615468565b509495945050505050565b602081526000614b616020830184615454565b600080602083850312156154b557600080fd5b82356001600160401b038111156154cb57600080fd5b6149c885828601614ab0565b600080604083850312156154ea57600080fd5b82356154f5816149d4565b946020939093013593505050565b6000806000806080858703121561551957600080fd5b8435615524816149d4565b935060208501359250604085013561553b816149d4565b9396929550929360600135925050565b600081518084526020808501945080840160005b838110156154845781516001600160a01b03168752958201959082019060010161555f565b604081526000615597604083018561554b565b82810360208401526112a08185615454565b6000806000606084860312156155be57600080fd5b83356155c9816149d4565b925060208401356001600160401b038111156155e457600080fd5b6155f0868287016152cd565b925050604084013590509250925092565b60006060828403121561561357600080fd5b614b618383614a98565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052601160045260246000fd5b600060001982141561565d5761565d615633565b5060010190565b60208082526019908201527f5061757361626c653a20696e6465782069732070617573656400000000000000604082015260600190565b60008235605e198336030181126156b157600080fd5b9190910192915050565b6000808335601e198436030181126156d257600080fd5b8301803591506001600160401b038211156156ec57600080fd5b6020019150600581901b360382131561473657600080fd5b606081018235615713816149d4565b6001600160a01b03908116835260208401359061572f826149d4565b166020830152604083013561574381614e28565b63ffffffff811660408401525092915050565b60208152816020820152818360408301376000818301604090810191909152601f909201601f19160101919050565b60006020828403121561579757600080fd5b8151614b61816149d4565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b60208082526037908201527f44656c65676174696f6e4d616e616765723a206f6e6c7953747261746567794d60408201527f616e616765724f72456967656e506f644d616e61676572000000000000000000606082015260800190565b60006020828403121561585b57600080fd5b8151614b61816151f7565b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b606082015260800190565b6000823560de198336030181126156b157600080fd5b6000602082840312156158d657600080fd5b8135614b61816151f7565b600060018060a01b03808351168452806020840151166020850152806040840151166040850152506060820151606084015263ffffffff608083015116608084015260a082015160e060a085015261593c60e085018261554b565b905060c083015184820360c08601526112a08282615454565b602081526000614b6160208301846158e1565b602081526000825160e0602084015261598561010084018261554b565b90506020840151601f198483030160408501526159a28282615454565b915050604084015160018060a01b03808216606086015260608601519150808251166080860152506bffffffffffffffffffffffff60208201511660a08501525060808401516159fa60c085018263ffffffff169052565b5060a08401516001600160a01b03811660e0850152610a3b565b60008060408385031215615a2757600080fd5b8251615a32816151f7565b6020939093015192949293505050565b82815260406020820152600061502760408301846158e1565b600060208284031215615a6d57600080fd5b5051919050565b600082601f830112615a8557600080fd5b81516020615a95614e8983614e45565b82815260059290921b84018101918181019086841115615ab457600080fd5b8286015b84811015614ed15780518352918301918301615ab8565b60008060408385031215615ae257600080fd5b82516001600160401b0380821115615af957600080fd5b818501915085601f830112615b0d57600080fd5b81516020615b1d614e8983614e45565b82815260059290921b84018101918181019089841115615b3c57600080fd5b948201945b83861015615b63578551615b54816149d4565b82529482019490820190615b41565b91880151919650909350505080821115615b7c57600080fd5b5061544a85828601615a74565b60008219821115615b9c57615b9c615633565b500190565b600082821015615bb357615bb3615633565b500390565b6001600160a01b039384168152919092166020820152604081019190915260600190565b600060208284031215615bee57600080fd5b8135614b6181614e28565b80546001600160a01b0319166001600160a01b0392909216919091179055565b8135615c24816149d4565b615c2e8183615bf9565b50600181016020830135615c41816149d4565b615c4b8183615bf9565b506040830135615c5a81614e28565b815463ffffffff60a01b191660a09190911b63ffffffff60a01b161790555050565b6000615c883683614f37565b92915050565b6020808252606e90820152600080516020615db483398151915260408201527f645769746864726177616c3a207769746864726177616c44656c6179426c6f6360608201527f6b7320706572696f6420686173206e6f74207965742070617373656420666f7260808201526d207468697320737472617465677960901b60a082015260c00190565b82815260006020604081840152835180604085015260005b81811015615d4a57858101830151858201606001528201615d2e565b81811115615d5c576000606083870101525b50601f01601f191692909201606001949350505050565b600060208284031215615d8557600080fd5b81516001600160e01b031981168114614b6157600080fd5b634e487b7160e01b600052602160045260246000fdfe44656c65676174696f6e4d616e616765722e5f636f6d706c6574655175657565a264697066735822122065bef68dcce15bc7412c9185c1d02811cb4839b5e272883a936d7cdaab2c67fd64736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0x82dc47734901ee7d4f4232f398752cb9dd5daccc": { + "nonce": 1, + "balance": "0x0", + "code": "0x6080604052600436106101855760003560e01c806374cdd798116100d1578063c49074421161008a578063e251ef5211610064578063e251ef5214610563578063e2c8344514610583578063f2882461146105a3578063fe80b087146105d757600080fd5b8063c490744214610503578063c4d66de814610523578063dda3346c1461054357600080fd5b806374cdd7981461044057806387e0d289146104745780639b4e46341461049b578063a50600f4146104ae578063b522538a146104ce578063baa7145a146104ee57600080fd5b806334bea20a1161013e57806358eaee791161011857806358eaee791461038f5780635d3f65b6146103bc5780636fcd0e53146103dc5780637439841f1461040957600080fd5b806334bea20a146103005780633f65cf191461033b5780634665bcda1461035b57600080fd5b80630b18ff66146101db5780630cd4649e146102185780631a5057be1461022f5780631d905d5c146102635780633106ab53146102af5780633474aa16146102e057600080fd5b366101d657346037600082825461019c9190614ab6565b90915550506040513481527f6fdd3dbdb173299608c0aa9f368735857c8842b581f8389238bf05bd04b3bf499060200160405180910390a1005b600080fd5b3480156101e757600080fd5b506033546101fb906001600160a01b031681565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561022457600080fd5b5061022d6105fb565b005b34801561023b57600080fd5b506101fb7f000000000000000000000000f7cd8fa9b94db2aa972023b379c7f72c65e4de9d81565b34801561026f57600080fd5b506102977f000000000000000000000000000000000000000000000000000000077359400081565b6040516001600160401b03909116815260200161020f565b3480156102bb57600080fd5b506034546102d090600160401b900460ff1681565b604051901515815260200161020f565b3480156102ec57600080fd5b50603454610297906001600160401b031681565b34801561030c57600080fd5b506102d061031b366004614af3565b603560209081526000928352604080842090915290825290205460ff1681565b34801561034757600080fd5b5061022d610356366004614b86565b610764565b34801561036757600080fd5b506101fb7f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e81565b34801561039b57600080fd5b506103af6103aa366004614c97565b610c05565b60405161020f9190614d10565b3480156103c857600080fd5b50603854610297906001600160401b031681565b3480156103e857600080fd5b506103fc6103f7366004614d1e565b610c6a565b60405161020f9190614d37565b34801561041557600080fd5b506103af610424366004614d1e565b600090815260366020526040902054600160c01b900460ff1690565b34801561044c57600080fd5b506101fb7f00000000000000000000000012975173b87f7595ee45dffb2ab812ece596bf8481565b34801561048057600080fd5b5060335461029790600160a01b90046001600160401b031681565b61022d6104a9366004614d7f565b610d17565b3480156104ba57600080fd5b5061022d6104c9366004614df2565b610ec4565b3480156104da57600080fd5b506103fc6104e9366004614c97565b611293565b3480156104fa57600080fd5b5061022d611386565b34801561050f57600080fd5b5061022d61051e366004614e9c565b6113f1565b34801561052f57600080fd5b5061022d61053e366004614ec8565b61162e565b34801561054f57600080fd5b5061022d61055e366004614fe2565b611806565b34801561056f57600080fd5b5061022d61057e3660046150b3565b6119d9565b34801561058f57600080fd5b5061022d61059e366004614e9c565b611da4565b3480156105af57600080fd5b506102977f000000000000000000000000000000000000000000000000000000006059f46081565b3480156105e357600080fd5b506105ed60375481565b60405190815260200161020f565b604051635ac86ab760e01b8152600260048201819052907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031690635ac86ab790602401602060405180830381865afa158015610663573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061068791906151ae565b156106ad5760405162461bcd60e51b81526004016106a4906151d0565b60405180910390fd5b6033546001600160a01b031633146106d75760405162461bcd60e51b81526004016106a49061522d565b603454600160401b900460ff16156107015760405162461bcd60e51b81526004016106a490615275565b6034805460ff60401b1916600160401b179055603354610729906001600160a01b0316611f87565b6033546040516001600160a01b03909116907fca8dfc8c5e0a67a74501c072a3325f685259bebbae7cfd230ab85198a78b70cd90600090a250565b6033546001600160a01b0316331461078e5760405162461bcd60e51b81526004016106a49061522d565b604051635ac86ab760e01b8152600260048201819052907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031690635ac86ab790602401602060405180830381865afa1580156107f6573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061081a91906151ae565b156108375760405162461bcd60e51b81526004016106a4906151d0565b60335489906001600160401b03600160a01b90910481169082161161086e5760405162461bcd60e51b81526004016106a4906152c4565b603454600160401b900460ff166108e65760405162461bcd60e51b815260206004820152603660248201527f456967656e506f642e686173456e61626c656452657374616b696e673a2072656044820152751cdd185ada5b99c81a5cc81b9bdd08195b98589b195960521b60648201526084016106a4565b86851480156108f457508483145b6109845760405162461bcd60e51b815260206004820152605560248201527f456967656e506f642e7665726966795769746864726177616c43726564656e7460448201527f69616c733a2076616c696461746f72496e646963657320616e642070726f6f666064820152740e640daeae6e840c4ca40e6c2daca40d8cadccee8d605b1b608482015260a4016106a4565b4261099a613f486001600160401b038d16614ab6565b1015610a235760405162461bcd60e51b815260206004820152604c60248201527f456967656e506f642e7665726966795769746864726177616c43726564656e7460448201527f69616c733a207370656369666965642074696d657374616d7020697320746f6f60648201526b0819985c881a5b881c185cdd60a21b608482015260a4016106a4565b60405163d1c64cc960e01b81526001600160401b038b166004820152610acc907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b03169063d1c64cc990602401602060405180830381865afa158015610a94573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ab8919061535f565b8a35610ac760208d018d615378565b611fbb565b6000805b88811015610b7057610b528c8c358c8c85818110610af057610af06153be565b9050602002016020810190610b0591906153d4565b8b8b86818110610b1757610b176153be565b9050602002810190610b299190615378565b8b8b88818110610b3b57610b3b6153be565b9050602002810190610b4d91906153fb565b612149565b610b5c9083614ab6565b915080610b6881615444565b915050610ad0565b5060335460405163030b147160e61b81526001600160a01b039182166004820152602481018390527f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e9091169063c2c51c4090604401600060405180830381600087803b158015610be057600080fd5b505af1158015610bf4573d6000803e3d6000fd5b505050505050505050505050505050565b600080610c4784848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506125ee92505050565b600090815260366020526040902054600160c01b900460ff169150505b92915050565b610c926040805160808101825260008082526020820181905291810182905290606082015290565b600082815260366020908152604091829020825160808101845281546001600160401b038082168352600160401b8204811694830194909452600160801b810490931693810193909352906060830190600160c01b900460ff166002811115610cfd57610cfd614cd8565b6002811115610d0e57610d0e614cd8565b90525092915050565b336001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e1614610d5f5760405162461bcd60e51b81526004016106a49061545f565b346801bc16d674ec80000014610deb5760405162461bcd60e51b8152602060048201526044602482018190527f456967656e506f642e7374616b653a206d75737420696e697469616c6c792073908201527f74616b6520666f7220616e792076616c696461746f72207769746820333220656064820152633a3432b960e11b608482015260a4016106a4565b7f00000000000000000000000012975173b87f7595ee45dffb2ab812ece596bf846001600160a01b031663228951186801bc16d674ec8000008787610e2e6126e8565b8888886040518863ffffffff1660e01b8152600401610e5296959493929190615531565b6000604051808303818588803b158015610e6b57600080fd5b505af1158015610e7f573d6000803e3d6000fd5b50505050507f606865b7934a25d4aed43f6cdb426403353fa4b3009c4d228407474581b01e238585604051610eb5929190615580565b60405180910390a15050505050565b604051635ac86ab760e01b8152600360048201819052907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031690635ac86ab790602401602060405180830381865afa158015610f2c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f5091906151ae565b15610f6d5760405162461bcd60e51b81526004016106a4906151d0565b8684148015610f7b57508382145b6110045760405162461bcd60e51b815260206004820152604e60248201527f456967656e506f642e76657269667942616c616e6365557064617465733a207660448201527f616c696461746f72496e646963657320616e642070726f6f6673206d7573742060648201526d0c4ca40e6c2daca40d8cadccee8d60931b608482015260a4016106a4565b4261101a613f486001600160401b038c16614ab6565b101561109c5760405162461bcd60e51b815260206004820152604560248201527f456967656e506f642e76657269667942616c616e6365557064617465733a207360448201527f70656369666965642074696d657374616d7020697320746f6f2066617220696e606482015264081c185cdd60da1b608482015260a4016106a4565b60405163d1c64cc960e01b81526001600160401b038a166004820152611140907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b03169063d1c64cc990602401602060405180830381865afa15801561110d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611131919061535f565b8735610ac760208a018a615378565b6000805b888110156111e4576111c68b8b8b84818110611162576111626153be565b905060200201602081019061117791906153d4565b8a358a8a8681811061118b5761118b6153be565b905060200281019061119d9190615378565b8a8a888181106111af576111af6153be565b90506020028101906111c191906153fb565b61272d565b6111d09083615594565b9150806111dc81615444565b915050611144565b506033546001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e81169163c2c51c409116611229633b9aca00856155d5565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401600060405180830381600087803b15801561126f57600080fd5b505af1158015611283573d6000803e3d6000fd5b5050505050505050505050505050565b6112bb6040805160808101825260008082526020820181905291810182905290606082015290565b603660006112fe85858080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506125ee92505050565b81526020808201929092526040908101600020815160808101835281546001600160401b038082168352600160401b8204811695830195909552600160801b81049094169281019290925290916060830190600160c01b900460ff16600281111561136b5761136b614cd8565b600281111561137c5761137c614cd8565b9052509392505050565b6033546001600160a01b031633146113b05760405162461bcd60e51b81526004016106a49061522d565b603454600160401b900460ff16156113da5760405162461bcd60e51b81526004016106a490615275565b6033546113ef906001600160a01b0316611f87565b565b336001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e16146114395760405162461bcd60e51b81526004016106a49061545f565b611447633b9aca0082615670565b156114d15760405162461bcd60e51b815260206004820152604e60248201527f456967656e506f642e776974686472617752657374616b6564426561636f6e4360448201527f6861696e4554483a20616d6f756e74576569206d75737420626520612077686f60648201526d1b194811ddd95a48185b5bdd5b9d60921b608482015260a4016106a4565b60006114e1633b9aca0083615684565b6034549091506001600160401b03908116908216111561159a5760405162461bcd60e51b815260206004820152606260248201527f456967656e506f642e776974686472617752657374616b6564426561636f6e4360448201527f6861696e4554483a20616d6f756e74477765692065786365656473207769746860648201527f6472617761626c6552657374616b6564457865637574696f6e4c617965724777608482015261656960f01b60a482015260c4016106a4565b603480548291906000906115b89084906001600160401b0316615698565b92506101000a8154816001600160401b0302191690836001600160401b03160217905550826001600160a01b03167f8947fd2ce07ef9cc302c4e8f0461015615d91ce851564839e91cc804c2f49d8e8360405161161791815260200190565b60405180910390a26116298383612c0b565b505050565b600054610100900460ff161580801561164e5750600054600160ff909116105b806116685750303b158015611668575060005460ff166001145b6116cb5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b60648201526084016106a4565b6000805460ff1916600117905580156116ee576000805461ff0019166101001790555b6001600160a01b0382166117615760405162461bcd60e51b815260206004820152603460248201527f456967656e506f642e696e697469616c697a653a20706f644f776e65722063616044820152736e6e6f74206265207a65726f206164647265737360601b60648201526084016106a4565b603380546001600160a01b0384166001600160a01b031990911681179091556034805460ff60401b1916600160401b1790556040517fca8dfc8c5e0a67a74501c072a3325f685259bebbae7cfd230ab85198a78b70cd90600090a28015611802576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050565b6033546001600160a01b031633146118305760405162461bcd60e51b81526004016106a49061522d565b604051635ac86ab760e01b8152600560048201819052907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031690635ac86ab790602401602060405180830381865afa158015611898573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906118bc91906151ae565b156118d95760405162461bcd60e51b81526004016106a4906151d0565b82518451146119645760405162461bcd60e51b815260206004820152604b60248201527f456967656e506f642e7265636f766572546f6b656e733a20746f6b656e4c697360448201527f7420616e6420616d6f756e7473546f5769746864726177206d7573742062652060648201526a0e6c2daca40d8cadccee8d60ab1b608482015260a4016106a4565b60005b84518110156119d2576119c083858381518110611986576119866153be565b60200260200101518784815181106119a0576119a06153be565b60200260200101516001600160a01b0316612c159092919063ffffffff16565b806119ca81615444565b915050611967565b5050505050565b604051635ac86ab760e01b81526004808201819052907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031690635ac86ab790602401602060405180830381865afa158015611a40573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a6491906151ae565b15611a815760405162461bcd60e51b81526004016106a4906151d0565b8386148015611a8f57508588145b8015611a9a57508782145b611b0e576040805162461bcd60e51b81526020600482015260248101919091527f456967656e506f642e766572696679416e6450726f636573735769746864726160448201527f77616c733a20696e70757473206d7573742062652073616d65206c656e67746860648201526084016106a4565b60405163d1c64cc960e01b81526001600160401b038c166004820152611bb2907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b03169063d1c64cc990602401602060405180830381865afa158015611b7f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ba3919061535f565b8b35610ac760208e018e615378565b604080518082019091526000808252602082015260005b83811015611cb2576000611c6d8d358d8d85818110611bea57611bea6153be565b9050602002810190611bfc91906156c0565b8c8c86818110611c0e57611c0e6153be565b9050602002810190611c209190615378565b8c8c88818110611c3257611c326153be565b9050602002810190611c4491906153fb565b8c8c8a818110611c5657611c566153be565b9050602002810190611c6891906153fb565b612c67565b80518451919250908490611c82908390614ab6565b9052506020808201519084018051611c9b908390615594565b905250819050611caa81615444565b915050611bc9565b50805115611ce1576033548151611ce1916001600160a01b031690611cdc90633b9aca00906156e1565b613152565b602081015115611d965760335460208201516001600160a01b037f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e81169263c2c51c4092911690611d3790633b9aca00906155d5565b6040516001600160e01b031960e085901b1681526001600160a01b0390921660048301526024820152604401600060405180830381600087803b158015611d7d57600080fd5b505af1158015611d91573d6000803e3d6000fd5b505050505b505050505050505050505050565b6033546001600160a01b03163314611dce5760405162461bcd60e51b81526004016106a49061522d565b604051635ac86ab760e01b8152600560048201819052907f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b031690635ac86ab790602401602060405180830381865afa158015611e36573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e5a91906151ae565b15611e775760405162461bcd60e51b81526004016106a4906151d0565b603754821115611f285760405162461bcd60e51b815260206004820152606a60248201527f456967656e506f642e77697468647261776e6f6e426561636f6e436861696e4560448201527f544842616c616e63655765693a20616d6f756e74546f5769746864726177206960648201527f732067726561746572207468616e206e6f6e426561636f6e436861696e45544860848201526942616c616e636557656960b01b60a482015260c4016106a4565b8160376000828254611f3a9190615700565b90915550506040518281526001600160a01b038416907f30420aacd028abb3c1fd03aba253ae725d6ddd52d16c9ac4cb5742cd43f530969060200160405180910390a26116298383613152565b6033805467ffffffffffffffff60a01b19164263ffffffff16600160a01b021790556000603755611fb88147613152565b50565b611fc7600360206156e1565b81146120575760405162461bcd60e51b815260206004820152605360248201527f426561636f6e436861696e50726f6f66732e7665726966795374617465526f6f60448201527f74416761696e73744c6174657374426c6f636b526f6f743a2050726f6f6620686064820152720c2e640d2dcc6dee4e4cac6e840d8cadccee8d606b1b608482015260a4016106a4565b61209c82828080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150600390506131e0565b6121435760405162461bcd60e51b815260206004820152606660248201527f426561636f6e436861696e50726f6f66732e7665726966795374617465526f6f60448201527f74416761696e73744c6174657374426c6f636b526f6f743a20496e76616c696460648201527f206c617465737420626c6f636b2068656164657220726f6f74206d65726b6c6560848201526510383937b7b360d11b60a482015260c4016106a4565b50505050565b6000806121888484808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152506131fa92505050565b6000818152603660209081526040808320815160808101835281546001600160401b038082168352600160401b8204811695830195909552600160801b8104909416928101929092529394509192906060830190600160c01b900460ff1660028111156121f7576121f7614cd8565b600281111561220857612208614cd8565b905250905060008160600151600281111561222557612225614cd8565b146122ce5760405162461bcd60e51b815260206004820152606760248201527f456967656e506f642e766572696679436f72726563745769746864726177616c60448201527f43726564656e7469616c733a2056616c696461746f72206d757374206265206960648201527f6e61637469766520746f2070726f7665207769746864726177616c2063726564608482015266656e7469616c7360c81b60a482015260c4016106a4565b6122d66126e8565b6122df90615717565b61231b86868080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061321e92505050565b146123a25760405162461bcd60e51b815260206004820152604b60248201527f456967656e506f642e766572696679436f72726563745769746864726177616c60448201527f43726564656e7469616c733a2050726f6f66206973206e6f7420666f7220746860648201526a1a5cc8115a59d95b941bd960aa1b608482015260a4016106a4565b60006123e086868080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061323392505050565b90506123f08a87878b8b8e613258565b6001606083015264ffffffffff891682526001600160401b038b811660408401527f000000000000000000000000000000000000000000000000000000077359400081169082161115612471576001600160401b037f0000000000000000000000000000000000000000000000000000000773594000166020830152612481565b6001600160401b03811660208301525b6000838152603660209081526040918290208451815492860151938601516001600160401b03908116600160801b0267ffffffffffffffff60801b19958216600160401b026001600160801b0319909516919092161792909217928316821781556060850151859391929091839160ff60c01b191668ffffffffffffffffff60801b1990911617600160c01b83600281111561251f5761251f614cd8565b02179055505060405164ffffffffff8b1681527f2d0800bbc377ea54a08c5db6a87aafff5e3e9c8fead0eda110e40e0c10441449915060200160405180910390a17f0e5fac175b83177cc047381e030d8fb3b42b37bd1c025e22c280facad62c32df898c84602001516040516125ba9392919064ffffffffff9390931683526001600160401b03918216602084015216604082015260600190565b60405180910390a1633b9aca0082602001516001600160401b03166125df91906156e1565b9b9a5050505050505050505050565b600081516030146126775760405162461bcd60e51b815260206004820152604760248201527f456967656e506f642e5f63616c63756c61746556616c696461746f725075626b60448201527f657948617368206d75737420626520612034382d6279746520424c53207075626064820152666c6963206b657960c81b608482015260a4016106a4565b60405160029061268e90849060009060200161573b565b60408051601f19818403018152908290526126a89161576a565b602060405180830381855afa1580156126c5573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610c64919061535f565b60408051600160f81b60208201526000602182015230606090811b6bffffffffffffffffffffffff1916602c8301529101604051602081830303815290604052905090565b60008061276c84848080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061323392505050565b905060006127ac8585808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152506131fa92505050565b6000818152603660209081526040808320815160808101835281546001600160401b038082168352600160401b8204811695830195909552600160801b8104909416928101929092529394509192906060830190600160c01b900460ff16600281111561281b5761281b614cd8565b600281111561282c5761282c614cd8565b8152505090508a6001600160401b031681604001516001600160401b0316106128e35760405162461bcd60e51b815260206004820152605c60248201527f456967656e506f642e76657269667942616c616e63655570646174653a20566160448201527f6c696461746f72732062616c616e63652068617320616c72656164792062656560648201527f6e207570646174656420666f7220746869732074696d657374616d7000000000608482015260a4016106a4565b6001816060015160028111156128fb576128fb614cd8565b146129635760405162461bcd60e51b815260206004820152603260248201527f456967656e506f642e76657269667942616c616e63655570646174653a2056616044820152716c696461746f72206e6f742061637469766560701b60648201526084016106a4565b61296c8b6134af565b6001600160401b03166129b187878080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061359992505050565b6001600160401b031611612a54576000836001600160401b031611612a545760405162461bcd60e51b815260206004820152604d60248201527f456967656e506f642e76657269667942616c616e63655570646174653a20766160448201527f6c696461746f7220697320776974686472617761626c6520627574206861732060648201526c3737ba103bb4ba34323930bbb760991b608482015260a4016106a4565b612a628987878b8b8f613258565b602081015160006001600160401b037f000000000000000000000000000000000000000000000000000000077359400081169086161115612ac457507f0000000000000000000000000000000000000000000000000000000773594000612ac7565b50835b6001600160401b0380821660208086019182528f831660408088019182526000898152603690935290912086518154935192518516600160801b0267ffffffffffffffff60801b19938616600160401b026001600160801b031990951691909516179290921790811683178255606086015186939091839160ff60c01b191668ffffffffffffffffff60801b1990911617600160c01b836002811115612b6f57612b6f614cd8565b0217905550905050816001600160401b0316816001600160401b031614612bfb577f0e5fac175b83177cc047381e030d8fb3b42b37bd1c025e22c280facad62c32df8c8e83604051612be69392919064ffffffffff9390931683526001600160401b03918216602084015216604082015260600190565b60405180910390a1612bf881836135b1565b95505b5050505050979650505050505050565b61180282826135d0565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526116299084906136e9565b6040805180820190915260008082526020820152612c8c612c87896157eb565b6137be565b6033546001600160401b03600160a01b909104811690821611612cc15760405162461bcd60e51b81526004016106a4906152c4565b6000612ccf612c878b6157eb565b90506000612d0f8888808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152506131fa92505050565b905060008082815260366020526040902054600160c01b900460ff166002811115612d3c57612d3c614cd8565b1415612df35760405162461bcd60e51b815260206004820152607460248201527f456967656e506f642e5f766572696679416e6450726f6365737357697468647260448201527f6177616c3a2056616c696461746f72206e657665722070726f76656e20746f2060648201527f68617665207769746864726177616c2063726564656e7469616c7320706f696e6084820152731d1959081d1bc81d1a1a5cc818dbdb9d1c9858dd60621b60a482015260c4016106a4565b60008181526035602090815260408083206001600160401b038616845290915290205460ff1615612eb25760405162461bcd60e51b815260206004820152605b60248201527f456967656e506f642e5f766572696679416e6450726f6365737357697468647260448201527f6177616c3a207769746864726177616c2068617320616c72656164792062656560648201527f6e2070726f76656e20666f7220746869732074696d657374616d700000000000608482015260a4016106a4565b6001603560008381526020019081526020016000206000846001600160401b03166001600160401b0316815260200190815260200160002060006101000a81548160ff021916908315150217905550612f8f8c87878e7f000000000000000000000000ed1db453c3156ff3155a97ad217b3087d5dc5f6e6001600160a01b03166344e71c806040518163ffffffff1660e01b8152600401602060405180830381865afa158015612f66573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612f8a9190615927565b6137ce565b6000612fcd8787808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152506141ef92505050565b9050612fdd8d8a8a8e8e86613258565b600061301b88888080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061420792505050565b90506130598a8a8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525061359992505050565b6001600160401b031661307361306e8f6157eb565b61421f565b6001600160401b03161061312b57603354600084815260366020908152604091829020825160808101845281546001600160401b038082168352600160401b8204811694830194909452600160801b81049093169381019390935261312093869388938a936001600160a01b03909316928892916060830190600160c01b900460ff16600281111561310757613107614cd8565b600281111561311857613118614cd8565b905250614231565b955050505050613145565b60335461312090839086906001600160a01b031684614442565b5098975050505050505050565b603354604051633036cd5360e21b81526001600160a01b03918216600482015283821660248201527f000000000000000000000000f7cd8fa9b94db2aa972023b379c7f72c65e4de9d9091169063c0db354c9083906044016000604051808303818588803b1580156131c357600080fd5b505af11580156131d7573d6000803e3d6000fd5b50505050505050565b6000836131ee868585614520565b1490505b949350505050565b60008160008151811061320f5761320f6153be565b60200260200101519050919050565b60008160018151811061320f5761320f6153be565b6000610c648260028151811061324b5761324b6153be565b602002602001015161466c565b61326460036002615a28565b84146132ef5760405162461bcd60e51b815260206004820152604e60248201527f426561636f6e436861696e50726f6f66732e76657269667956616c696461746f60448201527f724669656c64733a2056616c696461746f72206669656c64732068617320696e60648201526d0c6dee4e4cac6e840d8cadccee8d60931b608482015260a4016106a4565b60056132fd60286001614ab6565b6133079190614ab6565b6133129060206156e1565b82146133925760405162461bcd60e51b815260206004820152604360248201527f426561636f6e436861696e50726f6f66732e76657269667956616c696461746f60448201527f724669656c64733a2050726f6f662068617320696e636f7272656374206c656e6064820152620cee8d60eb1b608482015260a4016106a4565b600064ffffffffff82166133a860286001614ab6565b600b901b17905060006133ed8787808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152506146d392505050565b905061343385858080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508591508690506131e0565b6134a55760405162461bcd60e51b815260206004820152603d60248201527f426561636f6e436861696e50726f6f66732e76657269667956616c696461746f60448201527f724669656c64733a20496e76616c6964206d65726b6c652070726f6f6600000060648201526084016106a4565b5050505050505050565b60007f000000000000000000000000000000000000000000000000000000006059f4606001600160401b0316826001600160401b031610156135595760405162461bcd60e51b815260206004820152603760248201527f456967656e506f642e5f74696d657374616d70546f45706f63683a2074696d6560448201527f7374616d70206973206265666f72652067656e6573697300000000000000000060648201526084016106a4565b613565600c6020615a34565b61358f7f000000000000000000000000000000000000000000000000000000006059f46084615698565b610c649190615a63565b6000610c648260078151811061324b5761324b6153be565b60006135c96001600160401b03808416908516615a89565b9392505050565b804710156136205760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a20696e73756666696369656e742062616c616e636500000060448201526064016106a4565b6000826001600160a01b03168260405160006040518083038185875af1925050503d806000811461366d576040519150601f19603f3d011682016040523d82523d6000602084013e613672565b606091505b50509050806116295760405162461bcd60e51b815260206004820152603a60248201527f416464726573733a20756e61626c6520746f2073656e642076616c75652c207260448201527f6563697069656e74206d6179206861766520726576657274656400000000000060648201526084016106a4565b600061373e826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166149809092919063ffffffff16565b905080516000148061375f57508080602001905181019061375f91906151ae565b6116295760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b60648201526084016106a4565b6000610c6482610140015161466c565b6137d9600280615a28565b831461384d5760405162461bcd60e51b81526020600482015260496024820152600080516020615b2483398151915260448201527f616c3a207769746864726177616c4669656c64732068617320696e636f7272656064820152680c6e840d8cadccee8d60bb1b608482015260a4016106a4565b613859600d6002615a28565b61386960c0840160a08501615ac8565b6001600160401b0316106138d35760405162461bcd60e51b815260206004820152603f6024820152600080516020615b2483398151915260448201527f616c3a20626c6f636b526f6f74496e64657820697320746f6f206c617267650060648201526084016106a4565b6138df60046002615a28565b6138f0610100840160e08501615ac8565b6001600160401b03161061395c576040805162461bcd60e51b8152602060048201526024810191909152600080516020615b2483398151915260448201527f616c3a207769746864726177616c496e64657820697320746f6f206c6172676560648201526084016106a4565b61396860186002615a28565b61397860e0840160c08501615ac8565b6001600160401b0316106139f25760405162461bcd60e51b81526020600482015260476024820152600080516020615b2483398151915260448201527f616c3a20686973746f726963616c53756d6d617279496e64657820697320746f6064820152666f206c6172676560c81b608482015260a4016106a4565b60006001600160401b038216613a0a612c87856157eb565b6001600160401b031610613a1f576005613a22565b60045b9050613a2f600482614ab6565b613a3a906001614ab6565b613a459060206156e1565b613a4f8480615378565b905014613ac35760405162461bcd60e51b81526020600482015260486024820152600080516020615b2483398151915260448201527f616c3a207769746864726177616c50726f6f662068617320696e636f727265636064820152670e840d8cadccee8d60c31b608482015260a4016106a4565b613acf60046003614ab6565b613ada9060206156e1565b613ae76040850185615378565b905014613b615760405162461bcd60e51b815260206004820152604e6024820152600080516020615b2483398151915260448201527f616c3a20657865637574696f6e5061796c6f616450726f6f662068617320696e60648201526d0c6dee4e4cac6e840d8cadccee8d60931b608482015260a4016106a4565b613b6d600360206156e1565b613b7a6020850185615378565b905014613be85760405162461bcd60e51b81526020600482015260426024820152600080516020615b2483398151915260448201527f616c3a20736c6f7450726f6f662068617320696e636f7272656374206c656e676064820152610e8d60f31b608482015260a4016106a4565b613bf38160206156e1565b613c006060850185615378565b905014613c735760405162461bcd60e51b81526020600482015260476024820152600080516020615b2483398151915260448201527f616c3a2074696d657374616d7050726f6f662068617320696e636f7272656374606482015266040d8cadccee8d60cb1b608482015260a4016106a4565b600d613c8160186001614ab6565b613c8c906005614ab6565b613c97906001614ab6565b613ca19190614ab6565b613cac9060206156e1565b613cb96080850185615378565b905014613d425760405162461bcd60e51b81526020600482015260586024820152600080516020615b2483398151915260448201527f616c3a20686973746f726963616c53756d6d617279426c6f636b526f6f74507260648201527f6f6f662068617320696e636f7272656374206c656e6774680000000000000000608482015260a4016106a4565b6000613d5460c0850160a08601615ac8565b6001600160401b03166000613d6b600d6001614ab6565b613d7b60e0880160c08901615ac8565b6001600160401b0316901b600d613d9460186001614ab6565b613d9f906001614ab6565b613da99190614ab6565b601b901b1717179050613e04613dc26080860186615378565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92505050610100870135846131e0565b613e775760405162461bcd60e51b815260206004820152604a6024820152600080516020615b2483398151915260448201527f616c3a20496e76616c696420686973746f726963616c73756d6d617279206d656064820152693935b63290383937b7b360b11b608482015260a4016106a4565b613ece613e876020860186615378565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201829052506101008a013593506101208a0135925090506131e0565b613f2e5760405162461bcd60e51b815260206004820152603d6024820152600080516020615b2483398151915260448201527f616c3a20496e76616c696420736c6f74206d65726b6c652070726f6f6600000060648201526084016106a4565b6049613f86613f406040870187615378565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250505050610100870135610160880135846131e0565b613ff85760405162461bcd60e51b81526020600482015260496024820152600080516020615b2483398151915260448201527f616c3a20496e76616c696420657865637574696f6e5061796c6f6164206d657260648201526835b63290383937b7b360b91b608482015260a4016106a4565b506140506140096060860186615378565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050505061016086013561014087013560096131e0565b6140bb5760405162461bcd60e51b81526020600482015260426024820152600080516020615b2483398151915260448201527f616c3a20496e76616c69642074696d657374616d70206d65726b6c652070726f60648201526137b360f11b608482015260a4016106a4565b60006140ce610100860160e08701615ac8565b6001600160401b03166140e360046001614ab6565b600e901b17905060006141288888808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152506146d392505050565b90506141786141378780615378565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050505061016088013583856131e0565b6141e45760405162461bcd60e51b81526020600482015260436024820152600080516020615b2483398151915260448201527f616c3a20496e76616c6964207769746864726177616c206d65726b6c6520707260648201526237b7b360e91b608482015260a4016106a4565b505050505050505050565b6000610c648260018151811061324b5761324b6153be565b6000610c648260038151811061324b5761324b6153be565b6000602061358f83610120015161466c565b604080518082019091526000808252602082015260007f00000000000000000000000000000000000000000000000000000007735940006001600160401b0316846001600160401b031611156142a857507f00000000000000000000000000000000000000000000000000000007735940006142ab565b50825b60408051808201909152600080825260208201526142c98286615698565b6001600160401b0390811682526034805484926000916142eb91859116615ae5565b92506101000a8154816001600160401b0302191690836001600160401b0316021790555061431d8285602001516135b1565b602082810191909152600090850152600260608501819052506000888152603660209081526040918290208651815492880151938801516001600160401b03908116600160801b0267ffffffffffffffff60801b19958216600160401b026001600160801b0319909516919092161792909217928316821781556060870151879391929091839160ff60c01b191668ffffffffffffffffff60801b1990911617600160c01b8360028111156143d4576143d4614cd8565b0217905550506040805164ffffffffff8c1681526001600160401b038a8116602083015288168183015290516001600160a01b03891692507fb76a93bb649ece524688f1a01d184e0bbebcda58eae80c28a898bec3fb5a09639181900360600190a298975050505050505050565b60408051808201909152600080825260208201526040805164ffffffffff871681526001600160401b0380871660208301528416918101919091526001600160a01b038416907f8a7335714231dbd551aaba6314f4a97a14c201e53a3e25e1140325cdf67d7a4e9060600160405180910390a2603880548391906000906144d39084906001600160401b0316615ae5565b92506101000a8154816001600160401b0302191690836001600160401b031602179055506040518060400160405280836001600160401b0316815260200160008152509050949350505050565b6000835160001415801561453f57506020845161453d9190615670565b155b6145ce5760405162461bcd60e51b815260206004820152605460248201527f4d65726b6c652e70726f63657373496e636c7573696f6e50726f6f665368613260448201527f35363a2070726f6f66206c656e6774682073686f756c642062652061206e6f6e60648201527316bd32b9379036bab63a34b836329037b310199960611b608482015260a4016106a4565b604080516020808201909252848152905b85518111614662576145f2600285615670565b614625578151600052808601516020526020826040600060026107d05a03fa61461a57600080fd5b600284049350614650565b8086015160005281516020526020826040600060026107d05a03fa61464957600080fd5b6002840493505b61465b602082614ab6565b90506145df565b5051949350505050565b60f881901c60e882901c61ff00161760d882901c62ff0000161760c882901c63ff000000161764ff0000000060b883901c161765ff000000000060a883901c161766ff000000000000609883901c161767ff0000000000000060889290921c919091161790565b600080600283516146e49190615684565b90506000816001600160401b0381111561470057614700614ee5565b604051908082528060200260200182016040528015614729578160200160208202803683370190505b50905060005b828110156148305760028561474483836156e1565b81518110614754576147546153be565b60200260200101518683600261476a91906156e1565b614775906001614ab6565b81518110614785576147856153be565b60200260200101516040516020016147a7929190918252602082015260400190565b60408051601f19818403018152908290526147c19161576a565b602060405180830381855afa1580156147de573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190614801919061535f565b828281518110614813576148136153be565b60209081029190910101528061482881615444565b91505061472f565b5061483c600283615684565b91505b811561495c5760005b828110156149495760028261485d83836156e1565b8151811061486d5761486d6153be565b60200260200101518383600261488391906156e1565b61488e906001614ab6565b8151811061489e5761489e6153be565b60200260200101516040516020016148c0929190918252602082015260400190565b60408051601f19818403018152908290526148da9161576a565b602060405180830381855afa1580156148f7573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061491a919061535f565b82828151811061492c5761492c6153be565b60209081029190910101528061494181615444565b915050614848565b50614955600283615684565b915061483f565b8060008151811061496f5761496f6153be565b602002602001015192505050919050565b60606131f2848460008585600080866001600160a01b031685876040516149a7919061576a565b60006040518083038185875af1925050503d80600081146149e4576040519150601f19603f3d011682016040523d82523d6000602084013e6149e9565b606091505b50915091506149fa87838387614a05565b979650505050505050565b60608315614a71578251614a6a576001600160a01b0385163b614a6a5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064016106a4565b50816131f2565b6131f28383815115614a865781518083602001fd5b8060405162461bcd60e51b81526004016106a49190615b10565b634e487b7160e01b600052601160045260246000fd5b60008219821115614ac957614ac9614aa0565b500190565b6001600160401b0381168114611fb857600080fd5b8035614aee81614ace565b919050565b60008060408385031215614b0657600080fd5b823591506020830135614b1881614ace565b809150509250929050565b600060408284031215614b3557600080fd5b50919050565b60008083601f840112614b4d57600080fd5b5081356001600160401b03811115614b6457600080fd5b6020830191508360208260051b8501011115614b7f57600080fd5b9250929050565b60008060008060008060008060a0898b031215614ba257600080fd5b8835614bad81614ace565b975060208901356001600160401b0380821115614bc957600080fd5b614bd58c838d01614b23565b985060408b0135915080821115614beb57600080fd5b614bf78c838d01614b3b565b909850965060608b0135915080821115614c1057600080fd5b614c1c8c838d01614b3b565b909650945060808b0135915080821115614c3557600080fd5b50614c428b828c01614b3b565b999c989b5096995094979396929594505050565b60008083601f840112614c6857600080fd5b5081356001600160401b03811115614c7f57600080fd5b602083019150836020828501011115614b7f57600080fd5b60008060208385031215614caa57600080fd5b82356001600160401b03811115614cc057600080fd5b614ccc85828601614c56565b90969095509350505050565b634e487b7160e01b600052602160045260246000fd5b60038110614d0c57634e487b7160e01b600052602160045260246000fd5b9052565b60208101610c648284614cee565b600060208284031215614d3057600080fd5b5035919050565b60006080820190506001600160401b03808451168352806020850151166020840152806040850151166040840152506060830151614d786060840182614cee565b5092915050565b600080600080600060608688031215614d9757600080fd5b85356001600160401b0380821115614dae57600080fd5b614dba89838a01614c56565b90975095506020880135915080821115614dd357600080fd5b50614de088828901614c56565b96999598509660400135949350505050565b60008060008060008060008060a0898b031215614e0e57600080fd5b8835614e1981614ace565b975060208901356001600160401b0380821115614e3557600080fd5b614e418c838d01614b3b565b909950975060408b0135915080821115614e5a57600080fd5b614e668c838d01614b23565b965060608b0135915080821115614c1057600080fd5b6001600160a01b0381168114611fb857600080fd5b8035614aee81614e7c565b60008060408385031215614eaf57600080fd5b8235614eba81614e7c565b946020939093013593505050565b600060208284031215614eda57600080fd5b81356135c981614e7c565b634e487b7160e01b600052604160045260246000fd5b60405161018081016001600160401b0381118282101715614f1e57614f1e614ee5565b60405290565b604051601f8201601f191681016001600160401b0381118282101715614f4c57614f4c614ee5565b604052919050565b60006001600160401b03821115614f6d57614f6d614ee5565b5060051b60200190565b600082601f830112614f8857600080fd5b81356020614f9d614f9883614f54565b614f24565b82815260059290921b84018101918181019086841115614fbc57600080fd5b8286015b84811015614fd75780358352918301918301614fc0565b509695505050505050565b600080600060608486031215614ff757600080fd5b83356001600160401b038082111561500e57600080fd5b818601915086601f83011261502257600080fd5b81356020615032614f9883614f54565b82815260059290921b8401810191818101908a84111561505157600080fd5b948201945b8386101561507857853561506981614e7c565b82529482019490820190615056565b9750508701359250508082111561508e57600080fd5b5061509b86828701614f77565b9250506150aa60408501614e91565b90509250925092565b60008060008060008060008060008060c08b8d0312156150d257600080fd5b6150db8b614ae3565b995060208b01356001600160401b03808211156150f757600080fd5b6151038e838f01614b23565b9a5060408d013591508082111561511957600080fd5b6151258e838f01614b3b565b909a50985060608d013591508082111561513e57600080fd5b61514a8e838f01614b3b565b909850965060808d013591508082111561516357600080fd5b61516f8e838f01614b3b565b909650945060a08d013591508082111561518857600080fd5b506151958d828e01614b3b565b915080935050809150509295989b9194979a5092959850565b6000602082840312156151c057600080fd5b815180151581146135c957600080fd5b6020808252603e908201527f456967656e506f642e6f6e6c795768656e4e6f745061757365643a20696e646560408201527f782069732070617573656420696e20456967656e506f644d616e616765720000606082015260800190565b60208082526028908201527f456967656e506f642e6f6e6c79456967656e506f644f776e65723a206e6f74206040820152673837b227bbb732b960c11b606082015260800190565b6020808252602f908201527f456967656e506f642e6861734e6576657252657374616b65643a20726573746160408201526e1ada5b99c81a5cc8195b98589b1959608a1b606082015260800190565b6020808252606f908201527f456967656e506f642e70726f6f664973466f7256616c696454696d657374616d60408201527f703a20626561636f6e20636861696e2070726f6f66206d75737420626520666f60608201527f722074696d657374616d70206166746572206d6f7374526563656e745769746860808201526e064726177616c54696d657374616d7608c1b60a082015260c00190565b60006020828403121561537157600080fd5b5051919050565b6000808335601e1984360301811261538f57600080fd5b8301803591506001600160401b038211156153a957600080fd5b602001915036819003821315614b7f57600080fd5b634e487b7160e01b600052603260045260246000fd5b6000602082840312156153e657600080fd5b813564ffffffffff811681146135c957600080fd5b6000808335601e1984360301811261541257600080fd5b8301803591506001600160401b0382111561542c57600080fd5b6020019150600581901b3603821315614b7f57600080fd5b600060001982141561545857615458614aa0565b5060010190565b60208082526031908201527f456967656e506f642e6f6e6c79456967656e506f644d616e616765723a206e6f6040820152703a1032b4b3b2b72837b226b0b730b3b2b960791b606082015260800190565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b60005b838110156154f45781810151838201526020016154dc565b838111156121435750506000910152565b6000815180845261551d8160208601602086016154d9565b601f01601f19169290920160200192915050565b60808152600061554560808301888a6154b0565b82810360208401526155578188615505565b9050828103604084015261556c8186886154b0565b915050826060830152979650505050505050565b6020815260006131f26020830184866154b0565b600080821280156001600160ff1b03849003851316156155b6576155b6614aa0565b600160ff1b83900384128116156155cf576155cf614aa0565b50500190565b60006001600160ff1b03818413828413808216868404861116156155fb576155fb614aa0565b600160ff1b600087128281168783058912161561561a5761561a614aa0565b6000871292508782058712848416161561563657615636614aa0565b8785058712818416161561564c5761564c614aa0565b505050929093029392505050565b634e487b7160e01b600052601260045260246000fd5b60008261567f5761567f61565a565b500690565b6000826156935761569361565a565b500490565b60006001600160401b03838116908316818110156156b8576156b8614aa0565b039392505050565b6000823561017e198336030181126156d757600080fd5b9190910192915050565b60008160001904831182151516156156fb576156fb614aa0565b500290565b60008282101561571257615712614aa0565b500390565b80516020808301519190811015614b355760001960209190910360031b1b16919050565b6000835161574d8184602088016154d9565b6001600160801b0319939093169190920190815260100192915050565b600082516156d78184602087016154d9565b600082601f83011261578d57600080fd5b81356001600160401b038111156157a6576157a6614ee5565b6157b9601f8201601f1916602001614f24565b8181528460208386010111156157ce57600080fd5b816020850160208301376000918101602001919091529392505050565b600061018082360312156157fe57600080fd5b615806614efb565b82356001600160401b038082111561581d57600080fd5b6158293683870161577c565b8352602085013591508082111561583f57600080fd5b61584b3683870161577c565b6020840152604085013591508082111561586457600080fd5b6158703683870161577c565b6040840152606085013591508082111561588957600080fd5b6158953683870161577c565b606084015260808501359150808211156158ae57600080fd5b506158bb3682860161577c565b6080830152506158cd60a08401614ae3565b60a08201526158de60c08401614ae3565b60c08201526158ef60e08401614ae3565b60e082015261010083810135908201526101208084013590820152610140808401359082015261016092830135928101929092525090565b60006020828403121561593957600080fd5b81516135c981614ace565b600181815b8085111561597f57816000190482111561596557615965614aa0565b8085161561597257918102915b93841c9390800290615949565b509250929050565b60008261599657506001610c64565b816159a357506000610c64565b81600181146159b957600281146159c3576159df565b6001915050610c64565b60ff8411156159d4576159d4614aa0565b50506001821b610c64565b5060208310610133831016604e8410600b8410161715615a02575081810a610c64565b615a0c8383615944565b8060001904821115615a2057615a20614aa0565b029392505050565b60006135c98383615987565b60006001600160401b0380831681851681830481118215151615615a5a57615a5a614aa0565b02949350505050565b60006001600160401b0380841680615a7d57615a7d61565a565b92169190910492915050565b60008083128015600160ff1b850184121615615aa757615aa7614aa0565b6001600160ff1b0384018313811615615ac257615ac2614aa0565b50500390565b600060208284031215615ada57600080fd5b81356135c981614ace565b60006001600160401b03808316818516808303821115615b0757615b07614aa0565b01949350505050565b6020815260006135c9602083018461550556fe426561636f6e436861696e50726f6f66732e7665726966795769746864726177a2646970667358221220ee4b79d40165064464d98297b8cbf7c82c24d69c3c69e735c529c0d6b2c496b464736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0x8ce361602b935680e8dec218b820ff5056beb7af": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x0": "0x1", + "0x33": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x65": "0xa15bb66138824a1c7167f5e85b957d04dd34e468", + "0x66": "0x0", + "0x97": "0xdc385c976b9c09c9f3d60037ddb883ae6a226829befccc1535e616a4fea08a6", + "0x9d": "0x0", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x82c6d3ed4cd33d8ec1e51d0b5cc1d822eaa0c3dc", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + }, + "0x90f79bf6eb2c4f870365e785982e1f101e93b906": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x976ea74026e726554db657fa54763abd0c3a0aa9": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0xa0ee7a142d267c1f36714e4a8f75612f20a79720": { + "nonce": 27, + "balance": "0x21e1888b34963d1a2bb", + "code": "0x", + "storage": {} + }, + "0xa15bb66138824a1c7167f5e85b957d04dd34e468": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b506004361061004c5760003560e01c806346fbf68e146100515780638568520614610089578063ce5484281461009e578063eab66d7a146100b1575b600080fd5b61007461005f366004610313565b60006020819052908152604090205460ff1681565b60405190151581526020015b60405180910390f35b61009c610097366004610335565b6100dc565b005b61009c6100ac366004610313565b61011d565b6001546100c4906001600160a01b031681565b6040516001600160a01b039091168152602001610080565b6001546001600160a01b0316331461010f5760405162461bcd60e51b815260040161010690610371565b60405180910390fd5b6101198282610153565b5050565b6001546001600160a01b031633146101475760405162461bcd60e51b815260040161010690610371565b61015081610220565b50565b6001600160a01b0382166101bf5760405162461bcd60e51b815260206004820152602d60248201527f50617573657252656769737472792e5f7365745061757365723a207a65726f2060448201526c1859191c995cdcc81a5b9c1d5d609a1b6064820152608401610106565b6001600160a01b03821660008181526020818152604091829020805460ff19168515159081179091558251938452908301527f65d3a1fd4c13f05cba164f80d03ce90fb4b5e21946bfc3ab7dbd434c2d0b9152910160405180910390a15050565b6001600160a01b03811661028e5760405162461bcd60e51b815260206004820152602f60248201527f50617573657252656769737472792e5f736574556e7061757365723a207a657260448201526e1bc81859191c995cdcc81a5b9c1d5d608a1b6064820152608401610106565b600154604080516001600160a01b03928316815291831660208301527f06b4167a2528887a1e97a366eefe8549bfbf1ea3e6ac81cb2564a934d20e8892910160405180910390a1600180546001600160a01b0319166001600160a01b0392909216919091179055565b80356001600160a01b038116811461030e57600080fd5b919050565b60006020828403121561032557600080fd5b61032e826102f7565b9392505050565b6000806040838503121561034857600080fd5b610351836102f7565b91506020830135801515811461036657600080fd5b809150509250929050565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b60608201526080019056fea264697066735822122026d0da6bfc317f8d9c6ef8f95a0f798928296740e228ba50c1258f9c7fc9dbd464736f6c634300080c0033", + "storage": { + "0x1": "0x1e9", + "0x8279194eb4b29c3028809787a815b5fcda9c911d6a8c1dab7e4ef293adf594b9": "0x1" + } + }, + "0xb19b36b1456e65e3a6d514d3f715f204bd59f431": { + "nonce": 1, + "balance": "0x0", + "code": "0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b600060405190815260200160405180910390f3fea2646970667358221220dd7e2a31432852d491862c4dbe404a1fc851ccb128c615f36b216c053090772d64736f6c634300080c0033", + "storage": {} + }, + "0xc6b8fbf96cf7bbe45576417ec2163acecfa88ecc": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040526004361061020f5760003560e01c80638ffb5a0d11610118578063c0ccbf10116100a0578063daf12cd41161006f578063daf12cd41461067b578063ea4d3c9b1461069b578063f2fde38b146106cf578063f6848d24146106ef578063fabc1cbc1461072a57600080fd5b8063c0ccbf1014610605578063c1de3aef1461061b578063c2c51c401461063b578063d1c64cc91461065b57600080fd5b8063a38406a3116100e7578063a38406a31461055b578063a6a509be1461057b578063b134427114610591578063beffbb89146105c5578063c052bd61146105e557600080fd5b80638ffb5a0d146104ca5780639104c319146104ea5780639b4e4634146105125780639ba062751461052557600080fd5b8063595c6a671161019b578063715018a61161016a578063715018a61461042e57806374cdd7981461044357806384d8106214610477578063886f11951461048c5780638da5cb5b146104ac57600080fd5b8063595c6a67146103975780635ac86ab7146103ac5780635c975abb146103ec57806360f4062b1461040157600080fd5b8063292b7b2b116101e2578063292b7b2b146102a9578063387b1300146102f557806339b70e381461031557806344e71c8014610349578063463db0381461037757600080fd5b80630cf2686d146102145780630e81073c1461023657806310d67a2f14610269578063136439dd14610289575b600080fd5b34801561022057600080fd5b5061023461022f3660046126aa565b61074a565b005b34801561024257600080fd5b506102566102513660046126d8565b610806565b6040519081526020015b60405180910390f35b34801561027557600080fd5b50610234610284366004612704565b610a3b565b34801561029557600080fd5b506102346102a43660046126aa565b610aeb565b3480156102b557600080fd5b506102dd7f000000000000000000000000196dbcbb54b8ec4958c959d8949ebfe87ac2aaaf81565b6040516001600160a01b039091168152602001610260565b34801561030157600080fd5b50610234610310366004612721565b610c2a565b34801561032157600080fd5b506102dd7f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f4881565b34801561035557600080fd5b5061035e610fc9565b60405167ffffffffffffffff9091168152602001610260565b34801561038357600080fd5b50610234610392366004612762565b610ff2565b3480156103a357600080fd5b5061023461117e565b3480156103b857600080fd5b506103dc6103c736600461278c565b606654600160ff9092169190911b9081161490565b6040519015158152602001610260565b3480156103f857600080fd5b50606654610256565b34801561040d57600080fd5b5061025661041c366004612704565b609b6020526000908152604090205481565b34801561043a57600080fd5b50610234611245565b34801561044f57600080fd5b506102dd7f00000000000000000000000012975173b87f7595ee45dffb2ab812ece596bf8481565b34801561048357600080fd5b506102dd611259565b34801561049857600080fd5b506065546102dd906001600160a01b031681565b3480156104b857600080fd5b506033546001600160a01b03166102dd565b3480156104d657600080fd5b506102346104e53660046126d8565b611343565b3480156104f657600080fd5b506102dd73beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac081565b6102346105203660046127f8565b611437565b34801561053157600080fd5b506102dd610540366004612704565b6098602052600090815260409020546001600160a01b031681565b34801561056757600080fd5b506102dd610576366004612704565b611526565b34801561058757600080fd5b5061025660995481565b34801561059d57600080fd5b506102dd7f0000000000000000000000000c8e79f3534b00d9a3d4a856b665bf4ebc22f2ba81565b3480156105d157600080fd5b506102346105e03660046126d8565b6115f8565b3480156105f157600080fd5b506097546102dd906001600160a01b031681565b34801561061157600080fd5b50610256609a5481565b34801561062757600080fd5b50610234610636366004612704565b61180f565b34801561064757600080fd5b506102346106563660046126d8565b611820565b34801561066757600080fd5b50610256610676366004612762565b611bd3565b34801561068757600080fd5b5061023461069636600461286c565b611cde565b3480156106a757600080fd5b506102dd7f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af81565b3480156106db57600080fd5b506102346106ea366004612704565b611e11565b3480156106fb57600080fd5b506103dc61070a366004612704565b6001600160a01b0390811660009081526098602052604090205416151590565b34801561073657600080fd5b506102346107453660046126aa565b611e87565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561079d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107c191906128c7565b6001600160a01b0316336001600160a01b0316146107fa5760405162461bcd60e51b81526004016107f1906128e4565b60405180910390fd5b61080381611fe3565b50565b6000336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af16146108505760405162461bcd60e51b81526004016107f19061292e565b6001600160a01b0383166108cc5760405162461bcd60e51b815260206004820152603a60248201527f456967656e506f644d616e616765722e6164645368617265733a20706f644f7760448201527f6e65722063616e6e6f74206265207a65726f206164647265737300000000000060648201526084016107f1565b600082121561093a5760405162461bcd60e51b815260206004820152603460248201527f456967656e506f644d616e616765722e6164645368617265733a207368617265604482015273732063616e6e6f74206265206e6567617469766560601b60648201526084016107f1565b610948633b9aca00836129a2565b156109bb5760405162461bcd60e51b815260206004820152603d60248201527f456967656e506f644d616e616765722e6164645368617265733a20736861726560448201527f73206d75737420626520612077686f6c65204777656920616d6f756e7400000060648201526084016107f1565b6001600160a01b0383166000908152609b6020526040812054906109df84836129cc565b6001600160a01b0386166000818152609b602052604090819020839055519192509060008051602061351483398151915290610a1e9087815260200190565b60405180910390a2610a308282612024565b925050505b92915050565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a8e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ab291906128c7565b6001600160a01b0316336001600160a01b031614610ae25760405162461bcd60e51b81526004016107f1906128e4565b61080381612066565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa158015610b33573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b579190612a0d565b610b735760405162461bcd60e51b81526004016107f190612a2f565b60665481811614610bec5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c697479000000000000000060648201526084016107f1565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af1614610c725760405162461bcd60e51b81526004016107f19061292e565b6001600160a01b038316610cec5760405162461bcd60e51b8152602060048201526047602482015260008051602061353483398151915260448201527f546f6b656e733a20706f644f776e65722063616e6e6f74206265207a65726f206064820152666164647265737360c81b608482015260a4016107f1565b6001600160a01b038216610d695760405162461bcd60e51b815260206004820152604a602482015260008051602061353483398151915260448201527f546f6b656e733a2064657374696e6174696f6e2063616e6e6f74206265207a65606482015269726f206164647265737360b01b608482015260a4016107f1565b6000811215610dd85760405162461bcd60e51b8152602060048201526041602482015260008051602061353483398151915260448201527f546f6b656e733a207368617265732063616e6e6f74206265206e6567617469766064820152606560f81b608482015260a4016107f1565b610de6633b9aca00826129a2565b15610e5a5760405162461bcd60e51b815260206004820152604a602482015260008051602061353483398151915260448201527f546f6b656e733a20736861726573206d75737420626520612077686f6c6520476064820152691dd95a48185b5bdd5b9d60b21b608482015260a4016107f1565b6001600160a01b0383166000908152609b602052604081205490811215610f4d576000610e8682612a77565b905080831115610eeb576001600160a01b0385166000908152609b6020526040812055610eb38184612a94565b9250846001600160a01b031660008051602061351483398151915282604051610ede91815260200190565b60405180910390a2610f4b565b6001600160a01b0385166000908152609b602052604081208054859290610f139084906129cc565b90915550506040518381526001600160a01b038616906000805160206135148339815191529060200160405180910390a25050505050565b505b6001600160a01b03848116600090815260986020526040908190205490516362483a2160e11b815285831660048201526024810185905291169063c490744290604401600060405180830381600087803b158015610faa57600080fd5b505af1158015610fbe573d6000803e3d6000fd5b50505050505b505050565b609c5460009067ffffffffffffffff1680610fed5767ffffffffffffffff91505090565b919050565b610ffa61215d565b67ffffffffffffffff811661108c5760405162461bcd60e51b815260206004820152604c60248201527f456967656e506f644d616e616765722e73657444656e6562466f726b54696d6560448201527f7374616d703a2063616e6e6f7420736574206e657744656e6562466f726b546960648201526b06d657374616d7020746f20360a41b608482015260a4016107f1565b609c5467ffffffffffffffff16156111285760405162461bcd60e51b815260206004820152605360248201527f456967656e506f644d616e616765722e73657444656e6562466f726b54696d6560448201527f7374616d703a2063616e6e6f74207365742064656e6562466f726b54696d657360648201527274616d70206d6f7265207468616e206f6e636560681b608482015260a4016107f1565b609c805467ffffffffffffffff191667ffffffffffffffff83169081179091556040519081527f19200b6fdad58f91b2f496b0c444fc4be3eff74a7e24b07770e04a7137bfd9db9060200160405180910390a150565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa1580156111c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111ea9190612a0d565b6112065760405162461bcd60e51b81526004016107f190612a2f565b600019606681905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b61124d61215d565b61125760006121b7565b565b6066546000908190600190811614156112b05760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b60448201526064016107f1565b336000908152609860205260409020546001600160a01b0316156113325760405162461bcd60e51b815260206004820152603360248201527f456967656e506f644d616e616765722e637265617465506f643a2053656e64656044820152721c88185b1c9958591e481a185cc818481c1bd9606a1b60648201526084016107f1565b600061133c612209565b9250505090565b6001600160a01b0382166000908152609b60205260408120549061136783836129cc565b6001600160a01b0385166000908152609b6020526040812082905590915061138f8383612024565b604051631452b9d760e11b81526001600160a01b03878116600483015273beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac06024830152604482018390529192507f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af909116906328a573ae90606401600060405180830381600087803b15801561141857600080fd5b505af115801561142c573d6000803e3d6000fd5b505050505050505050565b6066546000906001908116141561148c5760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b60448201526064016107f1565b336000908152609860205260409020546001600160a01b0316806114b5576114b2612209565b90505b6040516326d3918d60e21b81526001600160a01b03821690639b4e46349034906114eb908b908b908b908b908b90600401612ad4565b6000604051808303818588803b15801561150457600080fd5b505af1158015611518573d6000803e3d6000fd5b505050505050505050505050565b6001600160a01b0380821660009081526098602052604081205490911680610a35576115f1836001600160a01b031660001b60405180610940016040528061090e8152602001612c0661090e9139604080516001600160a01b037f000000000000000000000000196dbcbb54b8ec4958c959d8949ebfe87ac2aaaf166020820152808201919091526000606082015260800160408051601f19818403018152908290526115d69291602001612b49565b604051602081830303815290604052805190602001206123e4565b9392505050565b336001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af16146116405760405162461bcd60e51b81526004016107f19061292e565b60008112156116b75760405162461bcd60e51b815260206004820152603760248201527f456967656e506f644d616e616765722e72656d6f76655368617265733a20736860448201527f617265732063616e6e6f74206265206e6567617469766500000000000000000060648201526084016107f1565b6116c5633b9aca00826129a2565b1561173a576040805162461bcd60e51b81526020600482015260248101919091527f456967656e506f644d616e616765722e72656d6f76655368617265733a20736860448201527f61726573206d75737420626520612077686f6c65204777656920616d6f756e7460648201526084016107f1565b6001600160a01b0382166000908152609b602052604081205461175e908390612b66565b905060008112156117ef5760405162461bcd60e51b815260206004820152604f60248201527f456967656e506f644d616e616765722e72656d6f76655368617265733a20636160448201527f6e6e6f7420726573756c7420696e20706f64206f776e657220686176696e672060648201526e6e656761746976652073686172657360881b608482015260a4016107f1565b6001600160a01b039092166000908152609b602052604090209190915550565b61181761215d565b610803816123f1565b6001600160a01b03808316600090815260986020526040902054839116331461189b5760405162461bcd60e51b815260206004820152602760248201527f456967656e506f644d616e616765722e6f6e6c79456967656e506f643a206e6f6044820152661d0818481c1bd960ca1b60648201526084016107f1565b6118a361243b565b6001600160a01b03831661193a5760405162461bcd60e51b815260206004820152605260248201527f456967656e506f644d616e616765722e7265636f7264426561636f6e4368616960448201527f6e45544842616c616e63655570646174653a20706f644f776e65722063616e6e6064820152716f74206265207a65726f206164647265737360701b608482015260a4016107f1565b611948633b9aca0083612ba5565b156119e15760405162461bcd60e51b815260206004820152605a60248201527f456967656e506f644d616e616765722e7265636f7264426561636f6e4368616960448201527f6e45544842616c616e63655570646174653a2073686172657344656c7461206d60648201527f75737420626520612077686f6c65204777656920616d6f756e74000000000000608482015260a4016107f1565b6001600160a01b0383166000908152609b602052604081205490611a0584836129cc565b6001600160a01b0386166000908152609b60205260408120829055909150611a2d8383612024565b90508015611b95576000811215611af8576001600160a01b037f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af1663132d49678773beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0611a8c85612a77565b6040516001600160e01b031960e086901b1681526001600160a01b0393841660048201529290911660248301526044820152606401600060405180830381600087803b158015611adb57600080fd5b505af1158015611aef573d6000803e3d6000fd5b50505050611b95565b604051631452b9d760e11b81526001600160a01b03878116600483015273beac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac06024830152604482018390527f0000000000000000000000008ce361602b935680e8dec218b820ff5056beb7af16906328a573ae90606401600060405180830381600087803b158015611b7c57600080fd5b505af1158015611b90573d6000803e3d6000fd5b505050505b856001600160a01b031660008051602061351483398151915286604051611bbe91815260200190565b60405180910390a2505050610fc4600160c955565b60975460405163321accf960e11b815267ffffffffffffffff8316600482015260009182916001600160a01b039091169063643599f290602401602060405180830381865afa158015611c2a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611c4e9190612bb9565b905080610a355760405162461bcd60e51b815260206004820152605260248201527f456967656e506f644d616e616765722e676574426c6f636b526f6f744174546960448201527f6d657374616d703a20737461746520726f6f742061742074696d657374616d70606482015271081b9bdd081e595d08199a5b985b1a5e995960721b608482015260a4016107f1565b600054610100900460ff1615808015611cfe5750600054600160ff909116105b80611d185750303b158015611d18575060005460ff166001145b611d7b5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b60648201526084016107f1565b6000805460ff191660011790558015611d9e576000805461ff0019166101001790555b611da786611fe3565b611db0856123f1565b611db9846121b7565b611dc38383612495565b8015611e09576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b505050505050565b611e1961215d565b6001600160a01b038116611e7e5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084016107f1565b610803816121b7565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015611eda573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611efe91906128c7565b6001600160a01b0316336001600160a01b031614611f2e5760405162461bcd60e51b81526004016107f1906128e4565b606654198119606654191614611fac5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c697479000000000000000060648201526084016107f1565b606681905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c90602001610c1f565b609a5460408051918252602082018390527f4e65c41a3597bda732ca64980235cf51494171d5853998763fb05db45afaacb3910160405180910390a1609a55565b6000808313612044576000821361203d57506000610a35565b5080610a35565b6000821361205c5761205583612a77565b9050610a35565b6120558383612b66565b6001600160a01b0381166120f45760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a4016107f1565b606554604080516001600160a01b03928316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1606580546001600160a01b0319166001600160a01b0392909216919091179055565b6033546001600160a01b031633146112575760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016107f1565b603380546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6000609a54609954600161221d9190612bd2565b11156122815760405162461bcd60e51b815260206004820152602d60248201527f456967656e506f644d616e616765722e5f6465706c6f79506f643a20706f642060448201526c1b1a5b5a5d081c995858da1959609a1b60648201526084016107f1565b60996000815461229090612bea565b9091555060408051610940810190915261090e80825260009161232f9183913391612c066020830139604080516001600160a01b037f000000000000000000000000196dbcbb54b8ec4958c959d8949ebfe87ac2aaaf166020820152808201919091526000606082015260800160408051601f198184030181529082905261231b9291602001612b49565b60405160208183030381529060405261257f565b60405163189acdbd60e31b81523360048201529091506001600160a01b0382169063c4d66de890602401600060405180830381600087803b15801561237357600080fd5b505af1158015612387573d6000803e3d6000fd5b50503360008181526098602052604080822080546001600160a01b0319166001600160a01b038816908117909155905192945092507f21c99d0db02213c32fff5b05cf0a718ab5f858802b91498f80d82270289d856a91a3919050565b60006115f1838330612680565b609780546001600160a01b0319166001600160a01b0383169081179091556040517f08f0470754946ccfbb446ff7fd2d6ae6af1bbdae19f85794c0cc5ed5e8ceb4f690600090a250565b600260c954141561248e5760405162461bcd60e51b815260206004820152601f60248201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c0060448201526064016107f1565b600260c955565b6065546001600160a01b03161580156124b657506001600160a01b03821615155b6125385760405162461bcd60e51b815260206004820152604760248201527f5061757361626c652e5f696e697469616c697a655061757365723a205f696e6960448201527f7469616c697a6550617573657228292063616e206f6e6c792062652063616c6c6064820152666564206f6e636560c81b608482015260a4016107f1565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a261257b82612066565b5050565b6000834710156125d15760405162461bcd60e51b815260206004820152601d60248201527f437265617465323a20696e73756666696369656e742062616c616e636500000060448201526064016107f1565b815161261f5760405162461bcd60e51b815260206004820181905260248201527f437265617465323a2062797465636f6465206c656e677468206973207a65726f60448201526064016107f1565b8282516020840186f590506001600160a01b0381166115f15760405162461bcd60e51b815260206004820152601960248201527f437265617465323a204661696c6564206f6e206465706c6f790000000000000060448201526064016107f1565b6000604051836040820152846020820152828152600b8101905060ff815360559020949350505050565b6000602082840312156126bc57600080fd5b5035919050565b6001600160a01b038116811461080357600080fd5b600080604083850312156126eb57600080fd5b82356126f6816126c3565b946020939093013593505050565b60006020828403121561271657600080fd5b81356115f1816126c3565b60008060006060848603121561273657600080fd5b8335612741816126c3565b92506020840135612751816126c3565b929592945050506040919091013590565b60006020828403121561277457600080fd5b813567ffffffffffffffff811681146115f157600080fd5b60006020828403121561279e57600080fd5b813560ff811681146115f157600080fd5b60008083601f8401126127c157600080fd5b50813567ffffffffffffffff8111156127d957600080fd5b6020830191508360208285010111156127f157600080fd5b9250929050565b60008060008060006060868803121561281057600080fd5b853567ffffffffffffffff8082111561282857600080fd5b61283489838a016127af565b9097509550602088013591508082111561284d57600080fd5b5061285a888289016127af565b96999598509660400135949350505050565b600080600080600060a0868803121561288457600080fd5b853594506020860135612896816126c3565b935060408601356128a6816126c3565b925060608601356128b6816126c3565b949793965091946080013592915050565b6000602082840312156128d957600080fd5b81516115f1816126c3565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b602080825260409082018190527f456967656e506f644d616e616765722e6f6e6c7944656c65676174696f6e4d61908201527f6e616765723a206e6f74207468652044656c65676174696f6e4d616e61676572606082015260800190565b634e487b7160e01b600052601260045260246000fd5b6000826129b1576129b161298c565b500690565b634e487b7160e01b600052601160045260246000fd5b600080821280156001600160ff1b03849003851316156129ee576129ee6129b6565b600160ff1b8390038412811615612a0757612a076129b6565b50500190565b600060208284031215612a1f57600080fd5b815180151581146115f157600080fd5b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b606082015260800190565b6000600160ff1b821415612a8d57612a8d6129b6565b5060000390565b600082821015612aa657612aa66129b6565b500390565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b606081526000612ae8606083018789612aab565b8281036020840152612afb818688612aab565b9150508260408301529695505050505050565b6000815160005b81811015612b2f5760208185018101518683015201612b15565b81811115612b3e576000828601525b509290920192915050565b6000612b5e612b588386612b0e565b84612b0e565b949350505050565b60008083128015600160ff1b850184121615612b8457612b846129b6565b6001600160ff1b0384018313811615612b9f57612b9f6129b6565b50500390565b600082612bb457612bb461298c565b500790565b600060208284031215612bcb57600080fd5b5051919050565b60008219821115612be557612be56129b6565b500190565b6000600019821415612bfe57612bfe6129b6565b506001019056fe608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c65644e2b791dedccd9fb30141b088cabf5c14a8912b52f59375c95c010700b8c6193456967656e506f644d616e616765722e77697468647261775368617265734173a264697066735822122077cb0b3b215321776ad0fd7ba21d797178c740d4654ab7a48d0f1d47ac4abace64736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0xd04ff4a75edd737a73e92b2f2274cb887d96e110": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106101f05760003560e01c80637cf72bba1161010f578063d98128c0116100a2578063e921d4fa11610071578063e921d4fa146103c6578063f2fde38b1461044c578063f73b7519146102a9578063fabc1cbc1461045f57600080fd5b8063d98128c014610430578063da16e29b14610322578063df5cf723146102ba578063e58398361461043e57600080fd5b80638da5cb5b116100de5780638da5cb5b146103b5578063a49db732146103c6578063c747075b146103da578063d7b7fa13146103ee57600080fd5b80637cf72bba146103465780638105e04314610354578063855fcc4a1461036b578063886f1195146103a257600080fd5b806339b70e38116101875780636f0c2f74116101565780636f0c2f7414610322578063715018a614610330578063723e59c7146103385780637259a45c1461024257600080fd5b806339b70e38146102ba578063595c6a67146102d55780635ac86ab7146102dd5780635c975abb1461031057600080fd5b80631794bb3c116101c35780631794bb3c1461022f5780631874e5ae14610242578063282670fc1461027257806338c8ee64146102a957600080fd5b80630ffabbce146101f557806310d67a2f14610209578063136439dd1461021c578063175d3205146101f5575b600080fd5b610207610203366004610b25565b5050565b005b610207610217366004610b5a565b610472565b61020761022a366004610b7e565b61052b565b61020761023d366004610b97565b505050565b610258610250366004610b25565b600092915050565b60405163ffffffff90911681526020015b60405180910390f35b610285610280366004610bd8565b61066a565b60408051825163ffffffff9081168252602093840151169281019290925201610269565b6102076102b7366004610b5a565b50565b60005b6040516001600160a01b039091168152602001610269565b610207610685565b6103006102eb366004610c04565b606654600160ff9092169190911b9081161490565b6040519015158152602001610269565b6066545b604051908152602001610269565b610258610250366004610c27565b61020761074c565b610314610250366004610b25565b610207610203366004610c60565b610300610362366004610cd5565b60009392505050565b610385610379366004610c27565b60008060009250925092565b604080519315158452602084019290925290820152606001610269565b6065546102bd906001600160a01b031681565b6033546001600160a01b03166102bd565b6103146103d4366004610b5a565b50600090565b6102076103e8366004610d13565b50505050565b6104016103fc366004610c27565b610760565b60408051825163ffffffff90811682526020808501518216908301529282015190921690820152606001610269565b610300610250366004610c27565b6103006103d4366004610b5a565b61020761045a366004610b5a565b610782565b61020761046d366004610b7e565b6107f8565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156104c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104e99190610d60565b6001600160a01b0316336001600160a01b0316146105225760405162461bcd60e51b815260040161051990610d7d565b60405180910390fd5b6102b781610954565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa158015610573573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105979190610dc7565b6105b35760405162461bcd60e51b815260040161051990610de9565b6066548181161461062c5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c69747900000000000000006064820152608401610519565b606681905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b60408051808201909152600080825260208201525b92915050565b60655460405163237dfb4760e11b81523360048201526001600160a01b03909116906346fbf68e90602401602060405180830381865afa1580156106cd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106f19190610dc7565b61070d5760405162461bcd60e51b815260040161051990610de9565b600019606681905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b610754610a4b565b61075e6000610aa5565b565b604080516060810182526000808252602082018190529181019190915261067f565b61078a610a4b565b6001600160a01b0381166107ef5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b6064820152608401610519565b6102b781610aa5565b606560009054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561084b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061086f9190610d60565b6001600160a01b0316336001600160a01b03161461089f5760405162461bcd60e51b815260040161051990610d7d565b60665419811960665419161461091d5760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c69747900000000000000006064820152608401610519565b606681905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c9060200161065f565b6001600160a01b0381166109e25760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a401610519565b606554604080516001600160a01b03928316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1606580546001600160a01b0319166001600160a01b0392909216919091179055565b6033546001600160a01b0316331461075e5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610519565b603380546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6001600160a01b03811681146102b757600080fd5b803563ffffffff81168114610b2057600080fd5b919050565b60008060408385031215610b3857600080fd5b8235610b4381610af7565b9150610b5160208401610b0c565b90509250929050565b600060208284031215610b6c57600080fd5b8135610b7781610af7565b9392505050565b600060208284031215610b9057600080fd5b5035919050565b600080600060608486031215610bac57600080fd5b8335610bb781610af7565b92506020840135610bc781610af7565b929592945050506040919091013590565b60008060408385031215610beb57600080fd5b8235610bf681610af7565b946020939093013593505050565b600060208284031215610c1657600080fd5b813560ff81168114610b7757600080fd5b60008060408385031215610c3a57600080fd5b8235610c4581610af7565b91506020830135610c5581610af7565b809150509250929050565b60008060208385031215610c7357600080fd5b823567ffffffffffffffff80821115610c8b57600080fd5b818501915085601f830112610c9f57600080fd5b813581811115610cae57600080fd5b8660208260051b8501011115610cc357600080fd5b60209290920196919550909350505050565b600080600060608486031215610cea57600080fd5b8335610cf581610af7565b9250610d0360208501610b0c565b9150604084013590509250925092565b60008060008060808587031215610d2957600080fd5b8435610d3481610af7565b9350610d4260208601610b0c565b9250610d5060408601610b0c565b9396929550929360600135925050565b600060208284031215610d7257600080fd5b8151610b7781610af7565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b600060208284031215610dd957600080fd5b81518015158114610b7757600080fd5b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b60608201526080019056fea2646970667358221220ba817d3c200ab51ada7e33d2c1b7e3ab9d28f58097be69b9312d60e3000623cd64736f6c634300080c0033", + "storage": {} + }, + "0xdbd296711ec8ef9aacb623ee3f1c0922dce0d7b2": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x0": "0xa15bb66138824a1c7167f5e85b957d04dd34e4680001", + "0x1": "0x0", + "0x32": "0x45009dd3abbe29db54fc5d893ceaa98a624882df", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0xf56aa3aceddf88ab12e494d0b96da3c09a5d264e", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + }, + "0xe1aa25618fa0c7a1cfdab5d6b456af611873b629": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x0": "0x1", + "0x33": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x65": "0xa15bb66138824a1c7167f5e85b957d04dd34e468", + "0x66": "0x0", + "0x97": "0xfb1da1faeb15d2e5bcaa2c1b8c84e5bad2599d652a22be70ffa667ddd680b27", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x5b4cb126885fb10464fdd12666feb25e2563b76", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + }, + "0xe1da8919f262ee86f9be05059c9280142cf23f48": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x0": "0x1", + "0x33": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x97": "0xa15bb66138824a1c7167f5e85b957d04dd34e468", + "0x98": "0x0", + "0xc9": "0x32c1f929f27df9d818b4cf4f764cb3eff5216bcff20e0adc6477816696325b0f", + "0xcb": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x2a264f26859166c5bf3868a54593ee716aebc848", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + }, + "0xed1db453c3156ff3155a97ad217b3087d5dc5f6e": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x0": "0x1", + "0x33": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x65": "0xa15bb66138824a1c7167f5e85b957d04dd34e468", + "0x66": "0x0", + "0x97": "0x0", + "0x9a": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0xc6b8fbf96cf7bbe45576417ec2163acecfa88ecc", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + }, + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": { + "nonce": 0, + "balance": "0x21e19e0c9bab2400000", + "code": "0x", + "storage": {} + }, + "0xf56aa3aceddf88ab12e494d0b96da3c09a5d264e": { + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106101375760003560e01c80635c975abb116100b8578063ab5921e11161007c578063ab5921e11461029c578063ce7c2ac2146102b1578063d9caed12146102c4578063e3dae51c146102d7578063f3e73875146102ea578063fabc1cbc146102fd57600080fd5b80635c975abb146102425780637a8b26371461024a578063886f11951461025d5780638c871019146102765780638f6a62401461028957600080fd5b806347e7ef24116100ff57806347e7ef24146101d2578063485cc955146101e5578063553ca5f8146101f8578063595c6a671461020b5780635ac86ab71461021357600080fd5b806310d67a2f1461013c578063136439dd146101515780632495a5991461016457806339b70e38146101945780633a98ef39146101bb575b600080fd5b61014f61014a36600461137b565b610310565b005b61014f61015f36600461139f565b6103cc565b603254610177906001600160a01b031681565b6040516001600160a01b0390911681526020015b60405180910390f35b6101777f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f4881565b6101c460335481565b60405190815260200161018b565b6101c46101e03660046113b8565b610510565b61014f6101f33660046113e4565b6106b4565b6101c461020636600461137b565b6107c9565b61014f6107dd565b61023261022136600461141d565b6001805460ff9092161b9081161490565b604051901515815260200161018b565b6001546101c4565b6101c461025836600461139f565b6108a9565b600054610177906201000090046001600160a01b031681565b6101c461028436600461139f565b6108f4565b6101c461029736600461137b565b6108ff565b6102a461090d565b60405161018b9190611470565b6101c46102bf36600461137b565b61092d565b61014f6102d23660046114a3565b6109c2565b6101c46102e536600461139f565b610b8b565b6101c46102f836600461139f565b610bc4565b61014f61030b36600461139f565b610bcf565b600060029054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061038791906114e4565b6001600160a01b0316336001600160a01b0316146103c05760405162461bcd60e51b81526004016103b790611501565b60405180910390fd5b6103c981610d2b565b50565b60005460405163237dfb4760e11b8152336004820152620100009091046001600160a01b0316906346fbf68e90602401602060405180830381865afa158015610419573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061043d919061154b565b6104595760405162461bcd60e51b81526004016103b79061156d565b600154818116146104d25760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e70617573653a20696e76616c696420617474656d70742060448201527f746f20756e70617573652066756e6374696f6e616c697479000000000000000060648201526084016103b7565b600181905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d906020015b60405180910390a250565b600180546000918291811614156105655760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b60448201526064016103b7565b336001600160a01b037f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f4816146105dd5760405162461bcd60e51b815260206004820181905260248201527f5374726174656779426173652e6f6e6c7953747261746567794d616e6167657260448201526064016103b7565b6105e78484610e30565b60335460006105f86103e8836115cb565b905060006103e8610607610eb0565b61061191906115cb565b9050600061061f87836115e3565b90508061062c84896115fa565b6106369190611619565b95508561069c5760405162461bcd60e51b815260206004820152602e60248201527f5374726174656779426173652e6465706f7369743a206e65775368617265732060448201526d63616e6e6f74206265207a65726f60901b60648201526084016103b7565b6106a686856115cb565b603355505050505092915050565b600054610100900460ff16158080156106d45750600054600160ff909116105b806106ee5750303b1580156106ee575060005460ff166001145b6107515760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201526d191e481a5b9a5d1a585b1a5e995960921b60648201526084016103b7565b6000805460ff191660011790558015610774576000805461ff0019166101001790555b61077e8383610f22565b80156107c4576000805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b505050565b60006107d76102588361092d565b92915050565b60005460405163237dfb4760e11b8152336004820152620100009091046001600160a01b0316906346fbf68e90602401602060405180830381865afa15801561082a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061084e919061154b565b61086a5760405162461bcd60e51b81526004016103b79061156d565b600019600181905560405190815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2565b6000806103e86033546108bc91906115cb565b905060006103e86108cb610eb0565b6108d591906115cb565b9050816108e285836115fa565b6108ec9190611619565b949350505050565b60006107d782610b8b565b60006107d76102f88361092d565b60606040518060800160405280604d8152602001611671604d9139905090565b604051633d3f06c960e11b81526001600160a01b0382811660048301523060248301526000917f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f4890911690637a7e0d9290604401602060405180830381865afa15801561099e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107d7919061163b565b6001805460029081161415610a155760405162461bcd60e51b815260206004820152601960248201527814185d5cd8589b194e881a5b99195e081a5cc81c185d5cd959603a1b60448201526064016103b7565b336001600160a01b037f000000000000000000000000e1da8919f262ee86f9be05059c9280142cf23f481614610a8d5760405162461bcd60e51b815260206004820181905260248201527f5374726174656779426173652e6f6e6c7953747261746567794d616e6167657260448201526064016103b7565b610a98848484610fb3565b60335480831115610b275760405162461bcd60e51b815260206004820152604d60248201527f5374726174656779426173652e77697468647261773a20616d6f756e7453686160448201527f726573206d757374206265206c657373207468616e206f7220657175616c207460648201526c6f20746f74616c53686172657360981b608482015260a4016103b7565b6000610b356103e8836115cb565b905060006103e8610b44610eb0565b610b4e91906115cb565b9050600082610b5d87846115fa565b610b679190611619565b9050610b7386856115e3565b603355610b81888883611036565b5050505050505050565b6000806103e8603354610b9e91906115cb565b905060006103e8610bad610eb0565b610bb791906115cb565b9050806108e283866115fa565b60006107d7826108a9565b600060029054906101000a90046001600160a01b03166001600160a01b031663eab66d7a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610c22573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c4691906114e4565b6001600160a01b0316336001600160a01b031614610c765760405162461bcd60e51b81526004016103b790611501565b600154198119600154191614610cf45760405162461bcd60e51b815260206004820152603860248201527f5061757361626c652e756e70617573653a20696e76616c696420617474656d7060448201527f7420746f2070617573652066756e6374696f6e616c697479000000000000000060648201526084016103b7565b600181905560405181815233907f3582d1828e26bf56bd801502bc021ac0bc8afb57c826e4986b45593c8fad389c90602001610505565b6001600160a01b038116610db95760405162461bcd60e51b815260206004820152604960248201527f5061757361626c652e5f73657450617573657252656769737472793a206e657760448201527f50617573657252656769737472792063616e6e6f7420626520746865207a65726064820152686f206164647265737360b81b608482015260a4016103b7565b600054604080516001600160a01b03620100009093048316815291831660208301527f6e9fcd539896fca60e8b0f01dd580233e48a6b0f7df013b89ba7f565869acdb6910160405180910390a1600080546001600160a01b03909216620100000262010000600160b01b0319909216919091179055565b6032546001600160a01b03838116911614610eac5760405162461bcd60e51b815260206004820152603660248201527f5374726174656779426173652e6465706f7369743a2043616e206f6e6c79206460448201527532b837b9b4ba103ab73232b9363cb4b733aa37b5b2b760511b60648201526084016103b7565b5050565b6032546040516370a0823160e01b81523060048201526000916001600160a01b0316906370a0823190602401602060405180830381865afa158015610ef9573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f1d919061163b565b905090565b600054610100900460ff16610f8d5760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201526a6e697469616c697a696e6760a81b60648201526084016103b7565b603280546001600160a01b0319166001600160a01b038416179055610eac81600061104a565b6032546001600160a01b038381169116146107c45760405162461bcd60e51b815260206004820152603b60248201527f5374726174656779426173652e77697468647261773a2043616e206f6e6c792060448201527f77697468647261772074686520737472617465677920746f6b656e000000000060648201526084016103b7565b6107c46001600160a01b0383168483611136565b6000546201000090046001600160a01b031615801561107157506001600160a01b03821615155b6110f35760405162461bcd60e51b815260206004820152604760248201527f5061757361626c652e5f696e697469616c697a655061757365723a205f696e6960448201527f7469616c697a6550617573657228292063616e206f6e6c792062652063616c6c6064820152666564206f6e636560c81b608482015260a4016103b7565b600181905560405181815233907fab40a374bc51de372200a8bc981af8c9ecdc08dfdaef0bb6e09f88f3c616ef3d9060200160405180910390a2610eac82610d2b565b604080516001600160a01b03848116602483015260448083018590528351808403909101815260649092018352602080830180516001600160e01b031663a9059cbb60e01b17905283518085019094528084527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564908401526107c4928692916000916111c6918516908490611246565b90508051600014806111e75750808060200190518101906111e7919061154b565b6107c45760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b60648201526084016103b7565b60606108ec848460008585600080866001600160a01b0316858760405161126d9190611654565b60006040518083038185875af1925050503d80600081146112aa576040519150601f19603f3d011682016040523d82523d6000602084013e6112af565b606091505b50915091506112c0878383876112cb565b979650505050505050565b60608315611337578251611330576001600160a01b0385163b6113305760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064016103b7565b50816108ec565b6108ec838381511561134c5781518083602001fd5b8060405162461bcd60e51b81526004016103b79190611470565b6001600160a01b03811681146103c957600080fd5b60006020828403121561138d57600080fd5b813561139881611366565b9392505050565b6000602082840312156113b157600080fd5b5035919050565b600080604083850312156113cb57600080fd5b82356113d681611366565b946020939093013593505050565b600080604083850312156113f757600080fd5b823561140281611366565b9150602083013561141281611366565b809150509250929050565b60006020828403121561142f57600080fd5b813560ff8116811461139857600080fd5b60005b8381101561145b578181015183820152602001611443565b8381111561146a576000848401525b50505050565b602081526000825180602084015261148f816040850160208701611440565b601f01601f19169190910160400192915050565b6000806000606084860312156114b857600080fd5b83356114c381611366565b925060208401356114d381611366565b929592945050506040919091013590565b6000602082840312156114f657600080fd5b815161139881611366565b6020808252602a908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526939903ab73830bab9b2b960b11b606082015260800190565b60006020828403121561155d57600080fd5b8151801515811461139857600080fd5b60208082526028908201527f6d73672e73656e646572206973206e6f74207065726d697373696f6e6564206160408201526739903830bab9b2b960c11b606082015260800190565b634e487b7160e01b600052601160045260246000fd5b600082198211156115de576115de6115b5565b500190565b6000828210156115f5576115f56115b5565b500390565b6000816000190483118215151615611614576116146115b5565b500290565b60008261163657634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561164d57600080fd5b5051919050565b60008251611666818460208701611440565b919091019291505056fe4261736520537472617465677920696d706c656d656e746174696f6e20746f20696e68657269742066726f6d20666f72206d6f726520636f6d706c657820696d706c656d656e746174696f6e73a26469706673582212207cdcf9338121603e52694ba2996bd478aa76c1d212140028e6f3354193e88e3764736f6c634300080c0033", + "storage": { + "0x0": "0xff" + } + }, + "0xf7cd8fa9b94db2aa972023b379c7f72c65e4de9d": { + "nonce": 1, + "balance": "0x0", + "code": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212205685c4ad655ee149c77141d53c938af08bb4457fb9509b6f1725a2ee99781c1a64736f6c634300080c0033", + "storage": { + "0x0": "0x1", + "0x33": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720", + "0x97": "0xa15bb66138824a1c7167f5e85b957d04dd34e468", + "0x98": "0x0", + "0xc9": "0xc4e0", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x29a79095352a718b3d7fe84e1f14e9f34a35598e", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x700b6a60ce7eaaea56f065753d8dcb9653dbad35" + } + } + } +} diff --git a/e2e/app/static/static.go b/e2e/app/static/static.go new file mode 100644 index 00000000..5f6aaf52 --- /dev/null +++ b/e2e/app/static/static.go @@ -0,0 +1,12 @@ +package static + +import _ "embed" + +// original: anvil-state.json +// +//go:embed anvil-state.json +var anvilState []byte + +func GetDevnetAnvilState() []byte { + return anvilState +} diff --git a/e2e/app/test.go b/e2e/app/test.go new file mode 100644 index 00000000..58234db0 --- /dev/null +++ b/e2e/app/test.go @@ -0,0 +1,87 @@ +package app + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + + "github.com/cometbft/cometbft/test/e2e/pkg/exec" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +const ( + EnvInfraType = "INFRASTRUCTURE_TYPE" + EnvInfraFile = "INFRASTRUCTURE_FILE" + EnvE2EManifest = "E2E_MANIFEST" + EnvE2ENode = "E2E_NODE" + EnvE2ERPCEndpoints = "E2E_RPC_ENDPOINTS" +) + +// Test runs test cases under tests/. +func Test(ctx context.Context, def Definition, verbose bool) error { + log.Info(ctx, "Running tests in ./test/...") + endpoints := externalEndpoints(def) + + networkDir, err := os.MkdirTemp("", "iliad-e2e") + if err != nil { + return errors.Wrap(err, "creating temp dir") + } + + endpointsFile := filepath.Join(networkDir, "endpoints.json") + if endopintsBytes, err := json.Marshal(endpoints); err != nil { + return errors.Wrap(err, "marshaling endpoints") + } else if err := os.WriteFile(endpointsFile, endopintsBytes, 0644); err != nil { + return errors.Wrap(err, "writing endpoints") + } else if err = os.Setenv(EnvE2ERPCEndpoints, endpointsFile); err != nil { + return errors.Wrap(err, "setting env ar") + } + + manifestFile, err := filepath.Abs(def.Testnet.File) + if err != nil { + return errors.Wrap(err, "absolute manifest path") + } + + if err = os.Setenv(EnvE2EManifest, manifestFile); err != nil { + return errors.Wrap(err, "setting env var") + } + + infd := def.Infra.GetInfrastructureData() + if infd.Path != "" { + infdPath, err := filepath.Abs(infd.Path) + if err != nil { + return errors.Wrap(err, "absolute infrastructure path") + } + err = os.Setenv(EnvInfraFile, infdPath) + if err != nil { + return errors.Wrap(err, "setting env var") + } + } + + if err = os.Setenv(EnvInfraType, infd.Provider); err != nil { + return errors.Wrap(err, "setting env var") + } + + log.Debug(ctx, "Env files", + EnvE2EManifest, manifestFile, + EnvInfraType, infd.Provider, + EnvInfraFile, infd.Path, + EnvE2ERPCEndpoints, endpointsFile, + ) + + args := []string{"go", "test", "-timeout", "60s", "-count", "1"} + if verbose { + args = append(args, "-v") + } + args = append(args, "github.com/piplabs/story/e2e/test") + log.Debug(ctx, "Test command", "args", args) + + err = exec.CommandVerbose(ctx, args...) + if err != nil { + return errors.Wrap(err, "go tests failed") + } + + return nil +} diff --git a/e2e/app/testdata/TestUpdateConfigStateSync.golden b/e2e/app/testdata/TestUpdateConfigStateSync.golden new file mode 100644 index 00000000..d74eb312 --- /dev/null +++ b/e2e/app/testdata/TestUpdateConfigStateSync.golden @@ -0,0 +1,16 @@ + +# Database directory +db_dir = "data" + +# Output level for logging, including package level options +log_level = "info" + +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + +# For Cosmos SDK-based chains, trust_period should usually be about 2/3 of the unbonding time (~2 +# weeks) during which they can be financially punished (slashed) for misbehavior. +rpc_servers = "" +trust_height = 1 +trust_hash = "74657374" +trust_period = "168h0m0s" diff --git a/e2e/app/valupdates.go b/e2e/app/valupdates.go new file mode 100644 index 00000000..642c7ce7 --- /dev/null +++ b/e2e/app/valupdates.go @@ -0,0 +1,238 @@ +package app + +import ( + "context" + "crypto/ecdsa" + "sort" + "time" + + "golang.org/x/sync/errgroup" + + "cosmossdk.io/math" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + + "github.com/piplabs/story/client/genutil/evm/predeploys" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/e2e/app/eoa" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/ethclient/ethbackend" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/txmgr" +) + +// FundValidatorsForTesting funds validators in ephemeral networks: devnet and staging. +// This is required by load generation for periodic validator self-delegation. +func FundValidatorsForTesting(ctx context.Context, def Definition) error { + if !def.Testnet.Network.IsEphemeral() { + // Only fund validators in ephemeral networks, devnet and staging. + return nil + } + + log.Info(ctx, "Funding validators for testing", "count", len(def.Testnet.Nodes)) + + network := networkFromDef(def) + omniEVM, _ := network.IliadEVMChain() + funder := eoa.MustAddress(network.ID, eoa.RoleTester) // Fund validators using tester eoa + _, fundBackend, err := def.Backends().BindOpts(ctx, omniEVM.ID, funder) + if err != nil { + return errors.Wrap(err, "bind opts") + } + + // Iterate over all nodes, since all maybe become validators. + var eg errgroup.Group + for _, node := range def.Testnet.Nodes { + eg.Go(func() error { + addr, _ := k1util.PubKeyToAddress(node.PrivvalKey.PubKey()) + tx, _, err := fundBackend.Send(ctx, funder, txmgr.TxCandidate{ + To: &addr, + GasLimit: 100_000, + Value: math.NewInt(1000).MulRaw(params.Ether).BigInt(), + }) + if err != nil { + return errors.Wrap(err, "send") + } + recp, err := fundBackend.WaitMined(ctx, tx) + if err != nil { + return errors.Wrap(err, "wait mined") + } + + bal, err := fundBackend.EtherBalanceAt(ctx, addr) + if err != nil { + return err + } + + log.Debug(ctx, "Funded validator address", + "node", node.Name, "addr", addr, + "balance", bal, "height", recp.BlockNumber.Uint64()) + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "wait fund") + } + + return nil +} + +type valUpdate struct { + Height int64 + Powers map[*e2e.Node]int64 +} + +func StartValidatorUpdates(ctx context.Context, def Definition) func() error { + errChan := make(chan error, 1) + returnErr := func(err error) { + if err != nil { + log.Error(ctx, "Validator updates failed", err) + } + select { + case errChan <- err: + default: + log.Error(ctx, "Error channel full, dropping error", err) + } + } + + go func() { + // Get all private keys + var privkeys []*ecdsa.PrivateKey + for _, node := range def.Testnet.Nodes { + pk, err := k1util.StdPrivKeyFromComet(node.PrivvalKey) + if err != nil { + returnErr(err) + return + } + + privkeys = append(privkeys, pk) + } + + // Get a sorted list of validator updates + var updates []valUpdate + for height, powers := range def.Testnet.ValidatorUpdates { + updates = append(updates, valUpdate{ + Height: height, + Powers: powers, + }) + } + sort.Slice(updates, func(i, j int) bool { + return updates[i].Height < updates[j].Height + }) + + // Create a backend to trigger deposits from + network := networkFromDef(def) + endpoints := externalEndpoints(def) + iliadEVM, _ := network.IliadEVMChain() + rpc, found := endpoints[iliadEVM.Name] + if !found { + returnErr(errors.New("get rpc")) + return + } + ethCl, err := ethclient.Dial(iliadEVM.Name, rpc) + if err != nil { + returnErr(errors.Wrap(err, "dial")) + return + } + valBackend, err := ethbackend.NewBackend(iliadEVM.Name, iliadEVM.ID, iliadEVM.BlockPeriod, ethCl, privkeys...) + if err != nil { + returnErr(errors.Wrap(err, "new backend")) + return + } + + // Create the IPTokenStaking contract + ipTokenStaking, err := bindings.NewIPTokenStaking(common.HexToAddress(predeploys.IPTokenStaking), valBackend) + if err != nil { + returnErr(errors.Wrap(err, "new iliad stake")) + return + } + + // Wait for each update, then submit self-delegations + for _, update := range updates { + log.Debug(ctx, "Waiting for next validator update", "wait_for_height", update.Height) + _, _, err := waitForHeight(ctx, def.Testnet.Testnet, update.Height) + if err != nil { + returnErr(errors.Wrap(err, "wait for height")) + return + } + + for node, power := range update.Powers { + pubkey := node.PrivvalKey.PubKey() + addr, err := k1util.PubKeyToAddress(pubkey) + if err != nil { + returnErr(errors.Wrap(err, "pubkey to addr")) + return + } + + // Wait until we have enough balance. + // FundValidatorsForTesting should ensure this, but this sometimes fails...? + for range 10 { + height, err := valBackend.BlockNumber(ctx) + if err != nil { + returnErr(errors.Wrap(err, "block height")) + return + } + + balance, err := valBackend.EtherBalanceAt(ctx, addr) + if err != nil { + returnErr(errors.Wrap(err, "balance at")) + return + } + + if balance > float64(power) { + break // We have enough balance + } + + log.Warn(ctx, "Cannot self-delegate, balance to low (will retry)", nil, + "height", height, "balance", balance, "require", power, + "node", node.Name, "addr", addr.Hex()) + time.Sleep(time.Second) + } + + txOpts, err := valBackend.BindOpts(ctx, addr) + if err != nil { + returnErr(errors.Wrap(err, "bind opts")) + return + } + txOpts.Value = math.NewInt(power).MulRaw(params.Ether).BigInt() + + // NOTE: We can use CreateValidator here, rather than Delegate (self-delegation) + // because current e2e manifest validator_udpates are only used to create a new validator, + // and not to self-delegate an existing one. + // TODO: Use CreateValidator + tx, err := ipTokenStaking.CreateValidatorOnBehalf(txOpts, pubkey.Bytes()) + if err != nil { + returnErr(errors.Wrap(err, "deposit", "node", node.Name, "addr", addr.Hex())) + return + } + rec, err := valBackend.WaitMined(ctx, tx) + if err != nil { + returnErr(errors.Wrap(err, "wait minded", "node", node.Name, "addr", addr.Hex())) + return + } + + log.Info(ctx, "Deposited stake", + "validator", node.Name, + "address", addr.Hex(), + "power", power, + "height", rec.BlockNumber.Uint64(), + ) + } + } + + returnErr(nil) + }() + + return func() error { + select { + case err := <-errChan: + return err + case <-ctx.Done(): + return errors.Wrap(ctx.Err(), "timeout") + } + } +} diff --git a/e2e/app/wait.go b/e2e/app/wait.go new file mode 100644 index 00000000..eb5c85d2 --- /dev/null +++ b/e2e/app/wait.go @@ -0,0 +1,38 @@ +package app + +import ( + "context" + "time" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + + "github.com/piplabs/story/lib/log" +) + +// Wait waits for a number of blocks to be produced, and for all nodes to catch +// up with it. +func Wait(ctx context.Context, testnet *e2e.Testnet, blocks int64) error { + block, _, err := waitForHeight(ctx, testnet, 0) + if err != nil { + return err + } + + return WaitUntil(ctx, testnet, block.Height+blocks) +} + +// WaitUntil waits until a given height has been reached. +func WaitUntil(ctx context.Context, testnet *e2e.Testnet, height int64) error { + log.Info(ctx, "Waiting for nodes to reach height", "height", height) + _, err := waitForAllNodes(ctx, testnet, height, waitingTime(len(testnet.Nodes), height)) + if err != nil { + return err + } + + return nil +} + +// waitingTime estimates how long it should take for a node to reach the height. +// More nodes in a network implies we may expect a slower network and may have to wait longer. +func waitingTime(nodes int, height int64) time.Duration { + return time.Duration(20+(int64(nodes)*height)) * time.Second +} diff --git a/e2e/cmd/cmd.go b/e2e/cmd/cmd.go new file mode 100644 index 00000000..349c0c26 --- /dev/null +++ b/e2e/cmd/cmd.go @@ -0,0 +1,273 @@ +package cmd + +import ( + "context" + "log/slog" + "regexp" + + cmtdocker "github.com/cometbft/cometbft/test/e2e/pkg/infra/docker" + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + + "github.com/piplabs/story/e2e/app" + "github.com/piplabs/story/e2e/app/eoa" + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/e2e/types" + libcmd "github.com/piplabs/story/lib/cmd" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" +) + +func New() *cobra.Command { + // E2E app is aimed at devs and CI, so debug level and force colors by default. + logCfg := log.DefaultConfig() + logCfg.Level = slog.LevelDebug.String() + logCfg.Color = log.ColorForce + + defCfg := app.DefaultDefinitionConfig(context.Background()) + + var def app.Definition + + cmd := libcmd.NewRootCmd("e2e", "e2e network generator and test runner") + cmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + if _, err := log.Init(ctx, logCfg); err != nil { + return err + } + + if err := libcmd.LogFlags(ctx, cmd.Flags()); err != nil { + return err + } + + var err error + def, err = app.MakeDefinition(ctx, defCfg, cmd.Use) + if err != nil { + return errors.Wrap(err, "make definition") + } + + // Some commands require networking, this ensures proper errors instead of panics. + if matchAny(cmd.Use, ".*deploy.*", ".*update.*", "e2e") { + if err := def.InitLazyNetwork(); err != nil { + return errors.Wrap(err, "init network") + } + } + + return err + } + + bindDefFlags(cmd.PersistentFlags(), &defCfg) + log.BindFlags(cmd.PersistentFlags(), &logCfg) + + // Root command runs the full E2E test. + e2eTestCfg := app.DefaultE2ETestConfig() + bindE2EFlags(cmd.Flags(), &e2eTestCfg) + cmd.RunE = func(cmd *cobra.Command, _ []string) error { + return app.E2ETest(cmd.Context(), def, e2eTestCfg) + } + + // Add subcommands + cmd.AddCommand( + newCreate3DeployCmd(&def), + newDeployCmd(&def), + newLogsCmd(&def), + newCleanCmd(&def), + newTestCmd(&def), + newUpgradeCmd(&def), + newKeyCreate(&def), + fundAccounts(&def), + ) + + return cmd +} + +func matchAny(str string, patterns ...string) bool { + for _, pattern := range patterns { + if ok, _ := regexp.MatchString(pattern, str); ok { + return true + } + } + + return false +} + +func newDeployCmd(def *app.Definition) *cobra.Command { + return &cobra.Command{ + Use: "deploy", + Short: "Deploys the e2e network", + RunE: func(cmd *cobra.Command, _ []string) error { + err := app.Deploy(cmd.Context(), *def, app.DefaultDeployConfig()) + return err + }, + } +} + +func newLogsCmd(def *app.Definition) *cobra.Command { + return &cobra.Command{ + Use: "logs", + Short: "Prints the infrastructure logs (of a previously preserved network)", + RunE: func(cmd *cobra.Command, _ []string) error { + err := cmtdocker.ExecComposeVerbose(cmd.Context(), def.Testnet.Dir, "logs") + if err != nil { + return errors.Wrap(err, "executing docker-compose logs") + } + + return nil + }, + } +} + +func newCleanCmd(def *app.Definition) *cobra.Command { + return &cobra.Command{ + Use: "clean", + Short: "Cleans (deletes) previously preserved network infrastructure", + RunE: func(cmd *cobra.Command, _ []string) error { + if err := app.CleanInfra(cmd.Context(), *def); err != nil { + return err + } + + return app.CleanupDir(cmd.Context(), def.Testnet.Dir) + }, + } +} + +func newTestCmd(def *app.Definition) *cobra.Command { + return &cobra.Command{ + Use: "test", + Short: "Runs go tests against the a previously preserved network", + RunE: func(cmd *cobra.Command, _ []string) error { + return app.Test(cmd.Context(), *def, true) + }, + } +} + +func newUpgradeCmd(def *app.Definition) *cobra.Command { + cfg := app.DefaultDeployConfig() + upgradeCfg := types.DefaultUpgradeConfig() + + cmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrades docker containers of a previously preserved network", + RunE: func(cmd *cobra.Command, _ []string) error { + return app.Upgrade(cmd.Context(), *def, cfg, upgradeCfg) + }, + } + + bindUpgradeFlags(cmd.Flags(), &upgradeCfg) + + return cmd +} + +func newCreate3DeployCmd(def *app.Definition) *cobra.Command { + cfg := app.Create3DeployConfig{} + + cmd := &cobra.Command{ + Use: "create3-deploy", + Short: "Deploys the Create3 factory", + RunE: func(cmd *cobra.Command, _ []string) error { + return app.Create3Deploy(cmd.Context(), *def, cfg) + }, + } + + bindCreate3DeployFlags(cmd.Flags(), &cfg) + + return cmd +} + +func newKeyCreate(def *app.Definition) *cobra.Command { + cfg := key.UploadConfig{} + + cmd := &cobra.Command{ + Use: "key-create", + Short: "Creates a private key in GCP secret manager for a node in a manifest", + RunE: func(cmd *cobra.Command, _ []string) error { + if def.Testnet.Network == netconf.Simnet || def.Testnet.Network == netconf.Devnet { + return errors.New("cannot create keys for simnet or devnet") + } + + cfg.Network = def.Testnet.Network + + if err := verifyKeyNodeType(*def, cfg); err != nil { + return err + } + + _, err := key.UploadNew(cmd.Context(), cfg) + + return err + }, + } + + bindKeyCreateFlags(cmd, &cfg) + + return cmd +} + +func fundAccounts(def *app.Definition) *cobra.Command { + cmd := &cobra.Command{ + Use: "fund", + Short: "Funds accounts to their target balance, network based on the manifest", + RunE: func(cmd *cobra.Command, _ []string) error { + if def.Testnet.Network == netconf.Simnet || def.Testnet.Network == netconf.Devnet { + return errors.New("cannot fund accounts on simnet or devnet") + } + if err := def.InitLazyNetwork(); err != nil { + return errors.Wrap(err, "init network") + } + + return app.FundEOAAccounts(cmd.Context(), *def, false) + }, + } + + return cmd +} + +// verifyKeyNodeType checks if the node exists in the manifest and if the key type is allowed for the node. +func verifyKeyNodeType(def app.Definition, cfg key.UploadConfig) error { + if err := cfg.Type.Verify(); err != nil { + return err + } + + if cfg.Type == key.EOA { + eoaRole := eoa.Role(cfg.Name) + if err := eoaRole.Verify(); err != nil { + return errors.Wrap(err, "verifying name as eoa type") + } + + account, ok := eoa.AccountForRole(def.Testnet.Network, eoaRole) + if !ok { + return errors.New("eoa account not found", "role", eoaRole) + } + + if account.Type != eoa.TypeSecret { + return errors.New("cannot create eoa key for non secret account") + } + + if account.Address != (common.Address{}) { + return errors.New("cannot create eoa key already defined", "addr", account.Address.Hex()) + } + + return nil + } + + for _, node := range def.Testnet.Nodes { + if node.Name == cfg.Name { + if cfg.Type == key.P2PExecution { + return errors.New("cannot create execution key for iliad node") + } + + return nil + } + } + + for _, evm := range def.Testnet.IliadEVMs { + if evm.InstanceName == cfg.Name { + if cfg.Type != key.P2PExecution { + return errors.New("only execution keys allowed for evm nodes") + } + + return nil + } + } + + return errors.New("node not found", "name", cfg.Name) +} diff --git a/e2e/cmd/flags.go b/e2e/cmd/flags.go new file mode 100644 index 00000000..928b4b7a --- /dev/null +++ b/e2e/cmd/flags.go @@ -0,0 +1,52 @@ +//nolint:lll // Long lines are easier to read for flag descriptions. +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/piplabs/story/e2e/app" + "github.com/piplabs/story/e2e/app/agent" + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/e2e/types" +) + +func bindDefFlags(flags *pflag.FlagSet, cfg *app.DefinitionConfig) { + bindPromFlags(flags, &cfg.AgentSecrets) + flags.StringVarP(&cfg.ManifestFile, "manifest-file", "f", cfg.ManifestFile, "path to manifest file") + flags.StringVar(&cfg.InfraProvider, "infra", cfg.InfraProvider, "infrastructure provider: docker, vmcompose") + flags.StringVar(&cfg.InfraDataFile, "infra-file", cfg.InfraDataFile, "infrastructure data file (not required for docker provider)") + flags.StringVar(&cfg.DeployKeyFile, "deploy-key", cfg.DeployKeyFile, "path to deploy private key file") + flags.StringVar(&cfg.FireAPIKey, "fireblocks-api-key", cfg.FireAPIKey, "FireBlocks api key") + flags.StringVar(&cfg.FireKeyPath, "fireblocks-key-path", cfg.FireKeyPath, "FireBlocks RSA private key path") + flags.StringVar(&cfg.IliadImgTag, "iliad-image-tag", cfg.IliadImgTag, "Iliad docker images tag (iliad). Defaults to working dir git commit.") + flags.StringToStringVar(&cfg.RPCOverrides, "rpc-overrides", cfg.RPCOverrides, "Public chain rpc overrides: '=,'") + flags.StringVar(&cfg.TracingEndpoint, "tracing-endpoint", cfg.TracingEndpoint, "Tracing endpoint") + flags.StringVar(&cfg.TracingHeaders, "tracing-headers", cfg.TracingHeaders, "Tracing headers") +} + +func bindE2EFlags(flags *pflag.FlagSet, cfg *app.E2ETestConfig) { + flags.BoolVar(&cfg.Preserve, "preserve", cfg.Preserve, "preserve infrastructure after test") +} + +func bindPromFlags(flags *pflag.FlagSet, cfg *agent.Secrets) { + flags.StringVar(&cfg.URL, "prom-url", cfg.URL, "prometheus url (only required if prometheus==true)") + flags.StringVar(&cfg.User, "prom-user", cfg.User, "prometheus user") + flags.StringVar(&cfg.Pass, "prom-password", cfg.Pass, "prometheus password") +} + +func bindUpgradeFlags(flags *pflag.FlagSet, cfg *types.UpgradeConfig) { + flags.StringVar(&cfg.ServiceRegexp, "services", cfg.ServiceRegexp, "Regexp applied to services per VM. Any match results in the VM being upgraded (all services on that VM are upgraded, not only matching services)") +} + +func bindCreate3DeployFlags(flags *pflag.FlagSet, cfg *app.Create3DeployConfig) { + flags.Uint64Var(&cfg.ChainID, "chain-id", cfg.ChainID, "chain id of the chain to deploy to") +} + +func bindKeyCreateFlags(cmd *cobra.Command, cfg *key.UploadConfig) { + cmd.Flags().StringVar(&cfg.Name, "name", cfg.Name, "key name: either node name or eoa account type") + cmd.Flags().StringVar((*string)(&cfg.Type), "type", string(cfg.Type), "key type: validator, p2p_execution, p2p_consensus, eoa") + + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("type") +} diff --git a/e2e/docker/compose.yaml.tmpl b/e2e/docker/compose.yaml.tmpl new file mode 100644 index 00000000..45c07066 --- /dev/null +++ b/e2e/docker/compose.yaml.tmpl @@ -0,0 +1,130 @@ +version: '2.4' +networks: + {{ .NetworkName }}: + labels: + e2e: true + driver: bridge + {{- if .Network }} + ipam: + driver: default + config: + - subnet: {{ .NetworkCIDR }} + {{- end }} + +services: +{{- range .Nodes }} + {{ .Name }}: + labels: + e2e: true + container_name: {{ .Name }} + image: {{ .Version }} + init: true + ports: + - {{ if $.BindAll }}26656:{{end}}26656 + - {{ if .ProxyPort }}{{ .ProxyPort }}:{{ end }}26657 +{{- if .PrometheusProxyPort }} + - {{ .PrometheusProxyPort }}:26660 +{{- end }} + - 6060 + volumes: + - ./{{ .Name }}:/iliad + depends_on: + {{ index $.NodeIliadEVMs .Name }}: + condition: service_healthy + networks: + {{ $.NetworkName }}: + {{ if $.Network }}ipv4_address: {{ .InternalIP }}{{ end }} +{{end}} + +{{- range .Anvils }} + # Initialises geth files and folder from provided genesis file. + {{ .Chain.Name }}: + labels: + e2e: true + container_name: {{ .Chain.Name }} + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id={{ .Chain.ChainID }} + - --block-time={{.Chain.BlockPeriod.Seconds}} + - --silent + {{ if .LoadState }}- --load-state=/anvil/state.json{{ end }} + ports: + - {{ if .ProxyPort }}{{ .ProxyPort }}:{{ end }}8545 + networks: + {{ $.NetworkName }}: + {{ if $.Network }}ipv4_address: {{ .InternalIP }}{{ end }} + {{ if .LoadState }} + volumes: + - {{ .LoadState }}:/anvil/state.json + {{ end }} +{{- end}} + + # Use geth as the iliad EVMs. +{{- range .IliadEVMs }} + # Initialises geth files and folder from provided genesis file. + {{ .InstanceName }}-init: + labels: + e2e: true + container_name: {{ .InstanceName }}-init + image: "ethereum/client-go:{{ $.GethTag }}" + command: --state.scheme={{ if .IsArchive }}hash{{ else }}path{{ end }} --datadir=/geth init /geth/genesis.json + volumes: + - ./{{ .InstanceName }}:/geth + networks: + {{ $.NetworkName }}: + + {{ .InstanceName }}: + labels: + e2e: true + container_name: {{ .InstanceName }} + image: "ethereum/client-go:{{ $.GethTag }}" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip:{{ .AdvertisedIP }} + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + {{ if .IsArchive }}- --gcmode=archive{{ end }} + ports: + - {{ if $.BindAll }}8551:{{end}}8551 + - {{ if .ProxyPort }}{{ .ProxyPort }}:{{ end }}8545 + - {{ if $.BindAll }}30303:{{end}}30303 + - 8546 + - 6060 + depends_on: + {{ .InstanceName }}-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./{{ .InstanceName }}:/geth + networks: + {{ $.NetworkName }}: + {{ if $.Network }}ipv4_address: {{ .AdvertisedIP }}{{ end }} +{{end}} + +{{- if .Prometheus }} + prometheus: + labels: + e2e: true + container_name: prometheus + image: prom/prometheus:latest + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.console.libraries=/usr/share/prometheus/console_libraries + - --web.console.templates=/usr/share/prometheus/consoles + - --enable-feature=exemplar-storage + - --enable-feature=agent + restart: unless-stopped + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + {{ $.NetworkName }}: + {{ if $.Network }}ipv4_address: 10.186.73.202{{ end }} +{{ end }} diff --git a/e2e/docker/data.go b/e2e/docker/data.go new file mode 100644 index 00000000..689f5c64 --- /dev/null +++ b/e2e/docker/data.go @@ -0,0 +1,62 @@ +package docker + +import ( + "fmt" + "net" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" +) + +const ( + ipPrefix = "10.186.73." // See github.com/cometbft/cometbft/test/e2e/pkg for reference + startIPSuffix = 100 + startPort = 8000 +) + +var localhost = net.ParseIP("127.0.0.1") //nolint:gochecknoglobals // Static IP + +// NewInfraData returns a new InfrastructureData for the given manifest. +// In addition to normal. +func NewInfraData(manifest types.Manifest) (types.InfrastructureData, error) { + infd, err := e2e.NewDockerInfrastructureData(manifest.Manifest) + if err != nil { + return types.InfrastructureData{}, errors.Wrap(err, "creating docker infrastructure data") + } + + // IP generator + ipSuffix := startIPSuffix + nextInternalIP := func() net.IP { + defer func() { ipSuffix++ }() + return net.ParseIP(fmt.Sprintf(ipPrefix+"%d", ipSuffix)) + } + + // Port generator + port := startPort + nextPort := func() uint32 { + defer func() { port++ }() + return uint32(port) + } + + for name := range manifest.IliadEVMs() { + infd.Instances[name] = e2e.InstanceData{ + IPAddress: nextInternalIP(), + ExtIPAddress: localhost, + Port: nextPort(), + } + } + + for _, name := range manifest.AnvilChains { + infd.Instances[name] = e2e.InstanceData{ + IPAddress: nextInternalIP(), + ExtIPAddress: localhost, + Port: nextPort(), + } + } + + return types.InfrastructureData{ + InfrastructureData: infd, + }, nil +} diff --git a/e2e/docker/docker.go b/e2e/docker/docker.go new file mode 100644 index 00000000..dfbd5258 --- /dev/null +++ b/e2e/docker/docker.go @@ -0,0 +1,228 @@ +package docker + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + "text/template" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/cometbft/cometbft/test/e2e/pkg/exec" + "github.com/cometbft/cometbft/test/e2e/pkg/infra" + cmtdocker "github.com/cometbft/cometbft/test/e2e/pkg/infra/docker" + + "github.com/piplabs/story/e2e/app/geth" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + + _ "embed" +) + +const ProviderName = "docker" + +// composeTmpl is our own custom docker compose template. This differs from cometBFT's. +// +//go:embed compose.yaml.tmpl +var composeTmpl []byte + +var _ types.InfraProvider = (*Provider)(nil) + +// Provider wraps the cometBFT docker provider, writing a different compose file. +type Provider struct { + *cmtdocker.Provider + servicesOnce sync.Once + testnet types.Testnet + iliadTag string +} + +func (*Provider) Clean(ctx context.Context) error { + log.Info(ctx, "Removing docker containers and networks") + + for _, cmd := range CleanCmds(false, runtime.GOOS == "linux") { + err := exec.Command(ctx, "bash", "-c", cmd) + if err != nil { + return errors.Wrap(err, "remove docker containers") + } + } + + return nil +} + +// NewProvider returns a new Provider. +func NewProvider(testnet types.Testnet, infd types.InfrastructureData, imgTag string) *Provider { + return &Provider{ + Provider: &cmtdocker.Provider{ + ProviderData: infra.ProviderData{ + Testnet: testnet.Testnet, + InfrastructureData: infd.InfrastructureData, + }, + }, + testnet: testnet, + iliadTag: imgTag, + } +} + +// Setup generates the docker-compose file and write it to disk, erroring if +// any of these operations fail. +func (p *Provider) Setup() error { + def := ComposeDef{ + Network: true, + NetworkName: p.testnet.Name, + NetworkCIDR: p.testnet.IP.String(), + BindAll: false, + Nodes: p.testnet.Nodes, + IliadEVMs: p.testnet.IliadEVMs, + Anvils: p.testnet.AnvilChains, + Prometheus: p.testnet.Prometheus, + IliadTag: p.iliadTag, + } + + bz, err := GenerateComposeFile(def) + if err != nil { + return errors.Wrap(err, "generate compose file") + } + + err = os.WriteFile(filepath.Join(p.Testnet.Dir, "docker-compose.yml"), bz, 0o644) + if err != nil { + return errors.Wrap(err, "write compose file") + } + + return nil +} + +func (*Provider) Upgrade(context.Context, types.UpgradeConfig) error { + return errors.New("upgrade not supported for docker provider") +} + +func (p *Provider) StartNodes(ctx context.Context, nodes ...*e2e.Node) error { + var err error + p.servicesOnce.Do(func() { + svcs := additionalServices(p.testnet) + log.Info(ctx, "Starting additional services", "names", svcs) + + err = cmtdocker.ExecCompose(ctx, p.Testnet.Dir, "create") // This fails if containers not available. + if err != nil { + err = errors.Wrap(err, "create containers") + return + } + + err = cmtdocker.ExecCompose(ctx, p.Testnet.Dir, append([]string{"up", "-d"}, svcs...)...) + if err != nil { + err = errors.Wrap(err, "start additional services") + return + } + }) + if err != nil { + return err + } + + // if there are no iliad nodes available + if len(nodes) == 0 { + panic("no nodes to start") + } + + // Start all requested nodes (use --no-deps to avoid starting the additional services again). + nodeNames := make([]string, len(nodes)) + for i, n := range nodes { + nodeNames[i] = n.Name + } + err = cmtdocker.ExecCompose(ctx, p.Testnet.Dir, append([]string{"up", "-d", "--no-deps"}, nodeNames...)...) + if err != nil { + return errors.Wrap(err, "start nodes") + } + + return nil +} + +type ComposeDef struct { + Network bool + NetworkName string + NetworkCIDR string + BindAll bool + + Nodes []*e2e.Node + IliadEVMs []types.IliadEVM + Anvils []types.AnvilChain + + IliadTag string + Prometheus bool +} + +func (ComposeDef) GethTag() string { + return geth.Version +} + +// NodeIliadEVMs returns a map of node name to IliadEVM instance name; map[node_name]iliad_evm. +func (c ComposeDef) NodeIliadEVMs() map[string]string { + resp := make(map[string]string) + for i, node := range c.Nodes { + evm := c.IliadEVMs[0].InstanceName + if len(c.IliadEVMs) == len(c.Nodes) { + evm = c.IliadEVMs[i].InstanceName + } + resp[node.Name] = evm + } + + return resp +} + +func GenerateComposeFile(def ComposeDef) ([]byte, error) { + tmpl, err := template.New("compose").Parse(string(composeTmpl)) + if err != nil { + return nil, errors.Wrap(err, "parse template") + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, def) + if err != nil { + return nil, errors.Wrap(err, "execute template") + } + + return buf.Bytes(), nil +} + +// CleanCmds returns generic docker commands to clean up docker containers and networks. +// This bypasses the need to a specific docker-compose context. +func CleanCmds(sudo bool, isLinux bool) []string { + // GNU xargs requires the -r flag to not run when input is empty, macOS + // does this by default. Ugly, but works. + xargsR := "" + if isLinux { + xargsR = "-r" + } + + // Some environments need sudo to run docker commands. + perm := "" + if sudo { + perm = "sudo" + } + + return []string{ + fmt.Sprintf("%s docker container ls -qa --filter label=e2e | xargs %v %s docker container rm -f", + perm, xargsR, perm), + fmt.Sprintf("%s docker network ls -q --filter label=e2e | xargs %v %s docker network rm", + perm, xargsR, perm), + } +} + +// additionalServices returns additional (to iliad) docker-compose services to start. +func additionalServices(testnet types.Testnet) []string { + var resp []string + if testnet.Prometheus { + resp = append(resp, "prometheus") + } + + for _, iliadEVM := range testnet.IliadEVMs { + resp = append(resp, iliadEVM.InstanceName) + } + for _, anvil := range testnet.AnvilChains { + resp = append(resp, anvil.Chain.Name) + } + + return resp +} diff --git a/e2e/docker/docker_test.go b/e2e/docker/docker_test.go new file mode 100644 index 00000000..fbaeccfe --- /dev/null +++ b/e2e/docker/docker_test.go @@ -0,0 +1,115 @@ +package docker_test + +import ( + "net" + "os" + "path/filepath" + "testing" + "time" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/docker" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/evmchain" + "github.com/piplabs/story/lib/netconf" +) + +//go:generate go test . -golden -clean + +func TestComposeTemplate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tag string + isEmpheral bool + }{ + { + name: "main_network", + tag: "main", + isEmpheral: false, + }, + { + name: "empheral_network", + tag: "main", + isEmpheral: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, ipNet, err := net.ParseCIDR("10.186.73.0/24") + require.NoError(t, err) + + key, err := crypto.HexToECDSA("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") + require.NoError(t, err) + en := enode.NewV4(&key.PublicKey, ipNet.IP, 30303, 30303) + + dir := t.TempDir() + testnet := types.Testnet{ + Testnet: &e2e.Testnet{ + Name: "test", + IP: ipNet, + Dir: dir, + Prometheus: true, + Nodes: []*e2e.Node{{ + Name: "node0", + Version: "iliadops/iliad:" + test.tag, + InternalIP: ipNet.IP, + ProxyPort: 8584, + }}, + }, + IliadEVMs: []types.IliadEVM{ + { + Chain: types.IliadEVMByNetwork(netconf.Simnet), + InstanceName: "iliad_evm_0", + AdvertisedIP: ipNet.IP, + ProxyPort: 8000, + NodeKey: key, + Enode: en, + Peers: []*enode.Node{en}, + }, + }, + AnvilChains: []types.AnvilChain{ + { + Chain: types.EVMChain{Metadata: evmchain.Metadata{ + ChainID: 99, + Name: "mock_rollup", + BlockPeriod: time.Second, + }}, + InternalIP: ipNet.IP, + ProxyPort: 9000, + }, + { + Chain: types.EVMChain{Metadata: evmchain.Metadata{ + ChainID: 1, + Name: "mock_l1", + BlockPeriod: time.Hour, + }}, + InternalIP: ipNet.IP, + ProxyPort: 9000, + LoadState: "path/to/anvil/state.json", + }, + }, + } + + // If the network is empheral, we use the devnet configuration. + if test.isEmpheral { + testnet.Network = netconf.Devnet + } + + p := docker.NewProvider(testnet, types.InfrastructureData{}, test.tag) + require.NoError(t, err) + + require.NoError(t, p.Setup()) + + _, err = os.ReadFile(filepath.Join(dir, "docker-compose.yml")) + require.NoError(t, err) + }) + } +} diff --git a/e2e/docker/testdata/TestComposeTemplate_commit_no_explorer.golden b/e2e/docker/testdata/TestComposeTemplate_commit_no_explorer.golden new file mode 100644 index 00000000..82bd6c91 --- /dev/null +++ b/e2e/docker/testdata/TestComposeTemplate_commit_no_explorer.golden @@ -0,0 +1,138 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + ipam: + driver: default + config: + - subnet: 10.186.73.0/24 + +services: + node0: + labels: + e2e: true + container_name: node0 + image: iliadops/iliad:7d1ae53 + init: true + ports: + - 26656 + - 8584:26657 + - 6060 + volumes: + - ./node0:/iliad + depends_on: + iliad_evm_0: + condition: service_healthy + networks: + test: + ipv4_address: 10.186.73.0 + + # Initialises geth files and folder from provided genesis file. + mock_rollup: + labels: + e2e: true + container_name: mock_rollup + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=99 + - --block-time=1 + - --silent + + ports: + - 9000:8545 + networks: + test: + ipv4_address: 10.186.73.0 + + # Initialises geth files and folder from provided genesis file. + mock_l1: + labels: + e2e: true + container_name: mock_l1 + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=1 + - --block-time=3600 + - --silent + - --load-state=/anvil/state.json + ports: + - 9000:8545 + networks: + test: + ipv4_address: 10.186.73.0 + + volumes: + - path/to/anvil/state.json:/anvil/state.json + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + iliad_evm_0-init: + labels: + e2e: true + container_name: iliad_evm_0-init + image: "ethereum/client-go:v1.14.2" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./iliad_evm_0:/geth + networks: + test: + + iliad_evm_0: + labels: + e2e: true + container_name: iliad_evm_0 + image: "ethereum/client-go:v1.14.2" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip:10.186.73.0 + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551 + - 8000:8545 + - 30303 + - 8546 + - 6060 + depends_on: + iliad_evm_0-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./iliad_evm_0:/geth + networks: + test: + ipv4_address: 10.186.73.0 + + prometheus: + labels: + e2e: true + container_name: prometheus + image: prom/prometheus:latest + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.console.libraries=/usr/share/prometheus/console_libraries + - --web.console.templates=/usr/share/prometheus/consoles + - --enable-feature=exemplar-storage + - --enable-feature=agent + restart: unless-stopped + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + test: + ipv4_address: 10.186.73.202 + diff --git a/e2e/docker/testdata/TestComposeTemplate_empheral_network.golden b/e2e/docker/testdata/TestComposeTemplate_empheral_network.golden new file mode 100644 index 00000000..51acee09 --- /dev/null +++ b/e2e/docker/testdata/TestComposeTemplate_empheral_network.golden @@ -0,0 +1,138 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + ipam: + driver: default + config: + - subnet: 10.186.73.0/24 + +services: + node0: + labels: + e2e: true + container_name: node0 + image: iliadops/iliad:main + init: true + ports: + - 26656 + - 8584:26657 + - 6060 + volumes: + - ./node0:/iliad + depends_on: + iliad_evm_0: + condition: service_healthy + networks: + test: + ipv4_address: 10.186.73.0 + + # Initialises geth files and folder from provided genesis file. + mock_rollup: + labels: + e2e: true + container_name: mock_rollup + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=99 + - --block-time=1 + - --silent + + ports: + - 9000:8545 + networks: + test: + ipv4_address: 10.186.73.0 + + # Initialises geth files and folder from provided genesis file. + mock_l1: + labels: + e2e: true + container_name: mock_l1 + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=1 + - --block-time=3600 + - --silent + - --load-state=/anvil/state.json + ports: + - 9000:8545 + networks: + test: + ipv4_address: 10.186.73.0 + + volumes: + - path/to/anvil/state.json:/anvil/state.json + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + iliad_evm_0-init: + labels: + e2e: true + container_name: iliad_evm_0-init + image: "ethereum/client-go:v1.14.2" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./iliad_evm_0:/geth + networks: + test: + + iliad_evm_0: + labels: + e2e: true + container_name: iliad_evm_0 + image: "ethereum/client-go:v1.14.2" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip:10.186.73.0 + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551 + - 8000:8545 + - 30303 + - 8546 + - 6060 + depends_on: + iliad_evm_0-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./iliad_evm_0:/geth + networks: + test: + ipv4_address: 10.186.73.0 + + prometheus: + labels: + e2e: true + container_name: prometheus + image: prom/prometheus:latest + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.console.libraries=/usr/share/prometheus/console_libraries + - --web.console.templates=/usr/share/prometheus/consoles + - --enable-feature=exemplar-storage + - --enable-feature=agent + restart: unless-stopped + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + test: + ipv4_address: 10.186.73.202 + diff --git a/e2e/docker/testdata/TestComposeTemplate_main_explorer.golden b/e2e/docker/testdata/TestComposeTemplate_main_explorer.golden new file mode 100644 index 00000000..5d5a1b12 --- /dev/null +++ b/e2e/docker/testdata/TestComposeTemplate_main_explorer.golden @@ -0,0 +1,137 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + ipam: + driver: default + config: + - subnet: 10.186.73.0/24 + +services: + node0: + labels: + e2e: true + container_name: node0 + image: iliadops/iliad:main + init: true + ports: + - 26656 + - 8584:26657 + - 6060 + volumes: + - ./node0:/iliad + depends_on: + iliad_evm_0: + condition: service_healthy + networks: + test: + ipv4_address: 10.186.73.0 + + # Initialises geth files and folder from provided genesis file. + mock_rollup: + labels: + e2e: true + container_name: mock_rollup + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=99 + - --block-time=1 + - --silent + + ports: + - 9000:8545 + networks: + test: + ipv4_address: 10.186.73.0 + + # Initialises geth files and folder from provided genesis file. + mock_l1: + labels: + e2e: true + container_name: mock_l1 + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=1 + - --block-time=3600 + - --silent + - --load-state=/anvil/state.json + ports: + - 9000:8545 + networks: + test: + ipv4_address: 10.186.73.0 + + volumes: + - path/to/anvil/state.json:/anvil/state.json + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + iliad_evm_0-init: + labels: + e2e: true + container_name: iliad_evm_0-init + image: "ethereum/client-go:v1.14.2" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./iliad_evm_0:/geth + networks: + test: + + iliad_evm_0: + labels: + e2e: true + container_name: iliad_evm_0 + image: "ethereum/client-go:v1.14.2" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip:10.186.73.0 + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551 + - 8000:8545 + - 30303 + - 8546 + - 6060 + depends_on: + iliad_evm_0-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./iliad_evm_0:/geth + networks: + test: + ipv4_address: 10.186.73.0 + + prometheus: + labels: + e2e: true + container_name: prometheus + image: prom/prometheus:latest + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.console.libraries=/usr/share/prometheus/console_libraries + - --web.console.templates=/usr/share/prometheus/consoles + - --enable-feature=exemplar-storage + - --enable-feature=agent + restart: unless-stopped + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + test: + ipv4_address: 10.186.73.202 diff --git a/e2e/main.go b/e2e/main.go new file mode 100644 index 00000000..bc3a0816 --- /dev/null +++ b/e2e/main.go @@ -0,0 +1,10 @@ +package main + +import ( + e2ecmd "github.com/piplabs/story/e2e/cmd" + libcmd "github.com/piplabs/story/lib/cmd" +) + +func main() { + libcmd.Main(e2ecmd.New()) +} diff --git a/e2e/manifests/ci.toml b/e2e/manifests/ci.toml new file mode 100644 index 00000000..b0a0701e --- /dev/null +++ b/e2e/manifests/ci.toml @@ -0,0 +1,23 @@ +network = "devnet" +anvil_chains = ["mock_l2", "mock_l1"] + +multi_iliad_evms = true + +[node.validator01] +[node.validator02] +[node.validator03] +[node.validator04] +start_at = 20 +state_sync = true + +[node.full01] +mode = "full" +perturb = ["restart"] + +# Trigger validator updates at height 10 +[validator_update.10] +full01 = 2 # Add full01 as validator by depositing 2 ether $ILIAD + +# Additional perturbations. +[perturb] +validator02_evm = ["stopstart"] diff --git a/e2e/manifests/devnet0.toml b/e2e/manifests/devnet0.toml new file mode 100644 index 00000000..a17a15bc --- /dev/null +++ b/e2e/manifests/devnet0.toml @@ -0,0 +1,7 @@ +# Devnet0 is a tiny devnet. Only a single validator and two anvils. +# This is aimed at local dev environments. + +network = "devnet" +anvil_chains = ["mock_op", "mock_arb"] + +[node.validator01] diff --git a/e2e/manifests/devnet1.toml b/e2e/manifests/devnet1.toml new file mode 100644 index 00000000..c9271bf9 --- /dev/null +++ b/e2e/manifests/devnet1.toml @@ -0,0 +1,9 @@ +# Devnet1 is the simple multi-validator devnet. It contains 2 validators. +network = "devnet" +anvil_chains = ["mock_l1", "mock_l2"] + +multi_iliad_evms = true +prometheus = true + +[node.validator01] +[node.validator02] diff --git a/e2e/manifests/devnet2.toml b/e2e/manifests/devnet2.toml new file mode 100644 index 00000000..664256ba --- /dev/null +++ b/e2e/manifests/devnet2.toml @@ -0,0 +1,9 @@ +# Devnet2 is the smallest devnet possible. It only a single validator. +network = "devnet" +anvil_chains = ["mock_l1", "mock_l2"] + +multi_iliad_evms = true +prometheus = true + +[node.validator01] +[node.validator02] diff --git a/e2e/manifests/mainnet.toml b/e2e/manifests/mainnet.toml new file mode 100644 index 00000000..230dfae8 --- /dev/null +++ b/e2e/manifests/mainnet.toml @@ -0,0 +1,4 @@ +network = "mainnet" +public_chains = ["ethereum"] + +prometheus = true diff --git a/e2e/manifests/simple.toml b/e2e/manifests/simple.toml new file mode 100644 index 00000000..df85f16c --- /dev/null +++ b/e2e/manifests/simple.toml @@ -0,0 +1,7 @@ +network = "devnet" +anvil_chains = ["mock_l2", "mock_l1"] + +[node.validator01] +[node.validator02] +[node.validator03] +[node.validator04] diff --git a/e2e/manifests/staging.toml b/e2e/manifests/staging.toml new file mode 100644 index 00000000..dbd54106 --- /dev/null +++ b/e2e/manifests/staging.toml @@ -0,0 +1,22 @@ +network = "staging" +public_chains = ["op_sepolia"] +anvil_chains = ["slow_l1"] +multi_iliad_evms = true +prometheus = true + +[node.validator01] +[node.validator02] + +[node.seed01] +mode = "seed" + +[node.fullnode01] +mode = "archive" + +# Testing long-lived keys on staging while testnet doens't exist. Remove this. +[keys.validator01] +validator = "0xD6CD71dF91a6886f69761826A9C4D123178A8d9D" +p2p_consensus = "324F0A865DB7EAFBF53A387FC74E58169EDFA6C4" + +[keys.validator01_evm] +p2p_execution = "0x86e640Ac9c8c693BBFFd2E31f91522b95c1274BF" diff --git a/e2e/manifests/static.go b/e2e/manifests/static.go new file mode 100644 index 00000000..86d713b3 --- /dev/null +++ b/e2e/manifests/static.go @@ -0,0 +1,23 @@ +package manifests + +import ( + _ "embed" +) + +var ( + //go:embed devnet0.toml + devnet0 []byte + + //go:embed testnet.toml + testnet []byte +) + +// Devnet0 returns the devnet0.toml manifest bytes. +func Devnet0() []byte { + return devnet0 +} + +// Testnet returns the testnet.toml manifest bytes. +func Testnet() []byte { + return testnet +} diff --git a/e2e/manifests/testnet.toml b/e2e/manifests/testnet.toml new file mode 100644 index 00000000..54c8d44d --- /dev/null +++ b/e2e/manifests/testnet.toml @@ -0,0 +1,54 @@ +network = "testnet" +public_chains = ["holesky","op_sepolia","arb_sepolia"] + +multi_iliad_evms = true +prometheus = true + +[node.validator01] +[node.validator02] +[node.validator03] +[node.validator04] + +[node.seed01] +mode = "seed" +[node.seed02] +mode = "seed" + +[node.fullnode01] +mode = "full" +[node.fullnode02] +mode = "full" +[node.fullnode03] +mode = "full" +[node.fullnode04] +mode = "full" +[node.fullnode05] +mode = "full" + +[node.archive01] +mode = "archive" + + +[keys.validator01] +validator = "0x5e7B5b243824E15796c33a8829F7807F2C5EeDc0" + +[keys.validator02] +validator = "0xD10A6A65C5586aF6093A4F993C3d609758c2a32b" + +[keys.validator03] +validator = "0xf64deF685fF01761D7b5D58452016D0dC9fa43eF" + +[keys.validator04] +validator = "0x34E0afB2FC3f43D469d9C07436c1A885A0979922" + +[keys.seed01] +p2p_consensus = "9644D22FCC9D3966CFF5789E6C411FDD40B101ED" + +[keys.seed01_evm] +p2p_execution = "0xA49cdBbFfF2fD0a3bD5a5780b5a4eB717A3D2c00" + +[keys.seed02] +p2p_consensus = "A839711D734491E1BA99A69E3FEEEBB965223792" + +[keys.seed02_evm] +p2p_execution = "0xf6d3676305dBf0Ba4BC97ad17833792dE6dC4AB7" diff --git a/e2e/run-multiple.sh b/e2e/run-multiple.sh new file mode 100755 index 00000000..5cf1091c --- /dev/null +++ b/e2e/run-multiple.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# This is a convenience script that takes a list of testnet manifests +# as arguments and runs each one of them sequentially. If a testnet +# fails, the container logs are dumped to stdout along with the testnet +# manifest, but the remaining testnets are still run. +# +# This is mostly used to run generated networks in nightly CI jobs. +# + +set -euo pipefail + +if [[ $# == 0 ]]; then + echo "Usage: $0 [MANIFEST...]" >&2 + exit 1 +fi + +echo "šŸŒŠ==> Running e2e tests:" "$@" + +for MANIFEST in "$@"; do + START=$SECONDS + echo "šŸŒŠ==> Running manifest: $MANIFEST" + + if ! e2e -f "$MANIFEST"; then + echo "šŸŒŠ==> āŒ Testnet $MANIFEST failed, dumping manifest..." + cat "$MANIFEST" + + echo "šŸŒŠ==> Dumping failed container logs to failed-logs.txt..." + e2e -f "$MANIFEST" logs > failed-logs.txt + + echo "šŸŒŠ==> Displaying failed container error and warn logs..." + grep -iE "(panic|erro|warn)" failed-logs.txt || echo "No errors or warns found" + + echo "šŸŒŠ==> Cleaning up failed manifest $MANIFEST..." + e2e -f "$MANIFEST" clean + + echo "šŸŒŠ==> āŒ Manifest $MANIFEST failed..." + exit 1 + fi + + echo "šŸŒŠ==> āœ… Completed manifest $MANIFEST in $(( SECONDS - START ))s" + echo "" +done + +echo "šŸŒŠ==> šŸŽ‰ All manifests successful " diff --git a/e2e/test/app_test.go b/e2e/test/app_test.go new file mode 100644 index 00000000..79f2df97 --- /dev/null +++ b/e2e/test/app_test.go @@ -0,0 +1,47 @@ +package e2e_test + +import ( + "context" + "encoding/hex" + "testing" + "time" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/netconf" +) + +// Tests that the app hash (as reported by the app) matches the last +// block and the node sync status. +func TestApp_Hash(t *testing.T) { + t.Parallel() + ctx := context.Background() + testNode(t, func(t *testing.T, _ netconf.Network, node *e2e.Node) { + t.Helper() + client, err := node.Client() + require.NoError(t, err) + + info, err := client.ABCIInfo(ctx) + require.NoError(t, err) + require.NotEmpty(t, info.Response.LastBlockAppHash, "expected app to return app hash") + + // In next-block execution, the app hash is stored in the next block + requestedHeight := info.Response.LastBlockHeight + 1 + + require.Eventually(t, func() bool { + status, err := client.Status(ctx) + require.NoError(t, err) + require.NotZero(t, status.SyncInfo.LatestBlockHeight) + + return status.SyncInfo.LatestBlockHeight >= requestedHeight + }, 5*time.Second, 500*time.Millisecond) + + block, err := client.Block(ctx, &requestedHeight) + require.NoError(t, err) + require.Equal(t, + hex.EncodeToString(info.Response.LastBlockAppHash), + hex.EncodeToString(block.Block.AppHash.Bytes()), + "app hash does not match last block's app hash") + }) +} diff --git a/e2e/test/block_test.go b/e2e/test/block_test.go new file mode 100644 index 00000000..4346f903 --- /dev/null +++ b/e2e/test/block_test.go @@ -0,0 +1,107 @@ +package e2e_test + +import ( + "context" + "testing" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/netconf" +) + +// Tests that block headers are identical across nodes where present. +func TestBlock_Header(t *testing.T) { + t.Parallel() + ctx := context.Background() + blocks := fetchBlockChain(ctx, t) + + testNode(t, func(t *testing.T, _ netconf.Network, node *e2e.Node) { + t.Helper() + if node.Mode == e2e.ModeSeed { + return + } + + client, err := node.Client() + require.NoError(t, err) + status, err := client.Status(ctx) + require.NoError(t, err) + + first := status.SyncInfo.EarliestBlockHeight + last := status.SyncInfo.LatestBlockHeight + if node.RetainBlocks > 0 { + first++ // avoid race conditions with block pruning + } + + for _, block := range blocks { + if block.Header.Height < first { + continue + } + if block.Header.Height > last { + break + } + resp, err := client.Block(ctx, &block.Header.Height) + require.NoError(t, err) + + require.Equal(t, block, resp.Block, + "block mismatch for height %d", block.Header.Height) + + require.NoError(t, resp.Block.ValidateBasic(), + "block at height %d is invalid", block.Header.Height) + } + }) +} + +// Tests that the node contains the expected block range. +func TestBlock_Range(t *testing.T) { + t.Parallel() + ctx := context.Background() + + testNode(t, func(t *testing.T, _ netconf.Network, node *e2e.Node) { + t.Helper() + if node.Mode == e2e.ModeSeed { + return + } + + client, err := node.Client() + require.NoError(t, err) + status, err := client.Status(ctx) + require.NoError(t, err) + + first := status.SyncInfo.EarliestBlockHeight + last := status.SyncInfo.LatestBlockHeight + + switch { + case node.StateSync: + assert.Greater(t, first, node.Testnet.InitialHeight, + "state synced nodes should not contain network's initial height") + + case node.RetainBlocks > 0 && int64(node.RetainBlocks) < (last-node.Testnet.InitialHeight+1): + // Delta handles race conditions in reading first/last heights. + assert.InDelta(t, node.RetainBlocks, last-first+1, 1, + "node not pruning expected blocks") + + default: + assert.Equal(t, node.Testnet.InitialHeight, first, + "node's first block should be network's initial height") + } + + for h := first; h <= last; h++ { + resp, err := client.Block(ctx, &(h)) + if err != nil && node.RetainBlocks > 0 && h == first { + // Ignore errors in first block if node is pruning blocks due to race conditions. + continue + } + require.NoError(t, ctx.Err(), "Timeout fetching block range: %d", h) + require.NoError(t, err) + assert.Equal(t, h, resp.Block.Height) + } + + for h := node.Testnet.InitialHeight; h < first; h++ { + _, err := client.Block(ctx, &(h)) + require.NoError(t, ctx.Err(), "Timeout fetching initial range: %d", h) + require.Error(t, err) + } + }) +} diff --git a/e2e/test/e2e_test.go b/e2e/test/e2e_test.go new file mode 100644 index 00000000..b396880a --- /dev/null +++ b/e2e/test/e2e_test.go @@ -0,0 +1,233 @@ +package e2e_test + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "sync" + "testing" + + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + rpctypes "github.com/cometbft/cometbft/rpc/core/types" + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/app" + "github.com/piplabs/story/e2e/docker" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/e2e/vmcompose" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/netconf" +) + +//nolint:gochecknoglobals // This was copied from cometbft/test/e2e/test/e2e_test.go +var ( + endpointsCache = map[string]map[string]string{} + networkCache = map[string]netconf.Network{} + testnetCache = map[string]types.Testnet{} + testnetCacheMtx = sync.Mutex{} + blocksCache = map[string][]*cmttypes.Block{} + blocksCacheMtx = sync.Mutex{} +) + +type testFunc struct { + TestNode func(*testing.T, netconf.Network, *e2e.Node) + TestIliadEVM func(*testing.T, ethclient.Client) + TestNetwork func(*testing.T, netconf.Network) +} + +func testNode(t *testing.T, fn func(*testing.T, netconf.Network, *e2e.Node)) { + t.Helper() + test(t, testFunc{TestNode: fn}) +} + +func testIliadEVM(t *testing.T, fn func(*testing.T, ethclient.Client)) { + t.Helper() + test(t, testFunc{TestIliadEVM: fn}) +} + +// test runs tests for testnet nodes. The callback functions are respectively given a +// single node to test, running as a subtest in parallel with other subtests. +// +// The testnet manifest must be given as the envvar E2E_MANIFEST. If not set, +// these tests are skipped so that they're not picked up during normal unit +// test runs. If E2E_NODE is also set, only the specified node is tested, +// otherwise all nodes are tested. +func test(t *testing.T, testFunc testFunc) { + t.Helper() + + testnet, network, endpoints := loadEnv(t) + nodes := testnet.Nodes + + if name := os.Getenv(app.EnvE2ENode); name != "" { + node := testnet.LookupNode(name) + require.NotNil(t, node, "node %q not found in testnet %q", name, testnet.Name) + nodes = []*e2e.Node{node} + } + + log.Info(context.Background(), "Running tests for testnet", + "testnet", testnet.Name, + "nodes", len(nodes), + ) + for _, node := range nodes { + if node.Stateless() { + continue + } else if testFunc.TestNode == nil { + continue + } + + t.Run(node.Name, func(t *testing.T) { + t.Parallel() + testFunc.TestNode(t, network, node) + }) + } + + if testFunc.TestIliadEVM != nil { + for _, chain := range network.Chains { + if !netconf.IsIliadExecution(network.ID, chain.ID) { + continue + } + + rpc, found := endpoints[chain.Name] + require.True(t, found) + + client, err := ethclient.Dial(chain.Name, rpc) + require.NoError(t, err) + + t.Run(chain.Name, func(t *testing.T) { + t.Parallel() + testFunc.TestIliadEVM(t, client) + }) + } + } + + if testFunc.TestNetwork != nil { + t.Run("network", func(t *testing.T) { + t.Parallel() + testFunc.TestNetwork(t, network) + }) + } +} + +// loadEnv loads the testnet and network based on env vars. +// + +func loadEnv(t *testing.T) (types.Testnet, netconf.Network, map[string]string) { + t.Helper() + + manifestFile := os.Getenv(app.EnvE2EManifest) + if manifestFile == "" { + t.Skip(app.EnvE2EManifest + " not set, not an end-to-end test run") + } + if !filepath.IsAbs(manifestFile) { + require.Fail(t, app.EnvE2EManifest+" must be an absolute path", "got", manifestFile) + } + + ifdType := os.Getenv(app.EnvInfraType) + ifdFile := os.Getenv(app.EnvInfraFile) + if ifdType != docker.ProviderName && ifdFile == "" { + require.Fail(t, app.EnvInfraFile+" not set while INFRASTRUCTURE_TYPE="+ifdType) + } else if ifdType != docker.ProviderName && !filepath.IsAbs(ifdFile) { + require.Fail(t, app.EnvInfraFile+" must be an absolute path", "got", ifdFile) + } + + testnetCacheMtx.Lock() + defer testnetCacheMtx.Unlock() + if testnet, ok := testnetCache[manifestFile]; ok { + return testnet, networkCache[manifestFile], endpointsCache[manifestFile] + } + m, err := app.LoadManifest(manifestFile) + require.NoError(t, err) + + var ifd types.InfrastructureData + switch ifdType { + case docker.ProviderName: + ifd, err = docker.NewInfraData(m) + case vmcompose.ProviderName: + ifd, err = vmcompose.LoadData(ifdFile) + default: + require.Fail(t, "unsupported infrastructure type", ifdType) + } + require.NoError(t, err) + + cfg := app.DefinitionConfig{ + ManifestFile: manifestFile, + } + testnet, err := app.TestnetFromManifest(context.Background(), m, ifd, cfg) + require.NoError(t, err) + testnetCache[manifestFile] = testnet + + endpointsFile := os.Getenv(app.EnvE2ERPCEndpoints) + if endpointsFile == "" { + t.Fatalf(app.EnvE2ERPCEndpoints + " not set") + } + bz, err := os.ReadFile(endpointsFile) + require.NoError(t, err) + + endpoints := map[string]string{} + require.NoError(t, json.Unmarshal(bz, &endpoints)) + endpointsCache[manifestFile] = endpoints + + network := netconf.Network{ + ID: testnet.Network, + Chains: []netconf.Chain{}, + } + + return testnet, network, endpoints +} + +// fetchBlockChain fetches a complete, up-to-date block history from +// the freshest testnet archive node. +func fetchBlockChain(ctx context.Context, t *testing.T) []*cmttypes.Block { + t.Helper() + + testnet, _, _ := loadEnv(t) + + // Find the freshest archive node + var ( + client *rpchttp.HTTP + status *rpctypes.ResultStatus + ) + for _, node := range testnet.ArchiveNodes() { + c, err := node.Client() + require.NoError(t, err) + s, err := c.Status(ctx) + require.NoError(t, err) + if status == nil || s.SyncInfo.LatestBlockHeight > status.SyncInfo.LatestBlockHeight { + client = c + status = s + } + } + require.NotNil(t, client, "couldn't find an archive node") + + // Fetch blocks. Look for existing block history in the block cache, and + // extend it with any new blocks that have been produced. + blocksCacheMtx.Lock() + defer blocksCacheMtx.Unlock() + + from := status.SyncInfo.EarliestBlockHeight + to := status.SyncInfo.LatestBlockHeight + blocks, ok := blocksCache[testnet.Name] + if !ok { + blocks = make([]*cmttypes.Block, 0, to-from+1) + } + if len(blocks) > 0 { + from = blocks[len(blocks)-1].Height + 1 + } + + for h := from; h <= to; h++ { + resp, err := client.Block(ctx, &(h)) + require.NoError(t, ctx.Err(), "Timeout fetching all blocks: %d of %d", h, to) + require.NoError(t, err) + require.NotNil(t, resp.Block) + require.Equal(t, h, resp.Block.Height, "unexpected block height %v", resp.Block.Height) + blocks = append(blocks, resp.Block) + } + require.NotEmpty(t, blocks, "blockchain does not contain any blocks") + blocksCache[testnet.Name] = blocks + + return blocks +} diff --git a/e2e/test/geth_test.go b/e2e/test/geth_test.go new file mode 100644 index 00000000..96c6adb1 --- /dev/null +++ b/e2e/test/geth_test.go @@ -0,0 +1,29 @@ +package e2e_test + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/app/geth" + "github.com/piplabs/story/lib/ethclient" +) + +// TestGethConfig ensure that the geth config is setup correctly. +func TestGethConfig(t *testing.T) { + t.Parallel() + testIliadEVM(t, func(t *testing.T, client ethclient.Client) { + t.Helper() + ctx := context.Background() + + cfg := geth.MakeGethConfig(geth.Config{}) + + block, err := client.BlockByNumber(ctx, big.NewInt(1)) + require.NoError(t, err) + + require.EqualValues(t, int(cfg.Eth.Miner.GasCeil), int(block.GasLimit())) + require.Equal(t, big.NewInt(0), block.Difficulty()) + }) +} diff --git a/e2e/types/chain.go b/e2e/types/chain.go new file mode 100644 index 00000000..5f4a0612 --- /dev/null +++ b/e2e/types/chain.go @@ -0,0 +1,94 @@ +package types + +import ( + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/evmchain" + "github.com/piplabs/story/lib/netconf" +) + +//nolint:gochecknoglobals // Static mappings +var ( + chainEthereum = EVMChain{ + Metadata: mustMetadata(evmchain.IDEthereum), + IsPublic: true, + } + + chainHolesky = EVMChain{ + Metadata: mustMetadata(evmchain.IDHolesky), + IsPublic: true, + } + + chainArbSepolia = EVMChain{ + Metadata: mustMetadata(evmchain.IDArbSepolia), + IsPublic: true, + } + + chainOpSepolia = EVMChain{ + Metadata: mustMetadata(evmchain.IDOpSepolia), + IsPublic: true, + } +) + +// IliadEVMByNetwork returns the Iliad evm chain definition by netconf network. +func IliadEVMByNetwork(network netconf.ID) EVMChain { + return EVMChain{ + Metadata: mustMetadata(network.Static().IliadExecutionChainID), + } +} + +// AnvilChainsByNames returns the Anvil evm chain definitions by names. +func AnvilChainsByNames(names []string) ([]EVMChain, error) { + var chains []EVMChain + for _, name := range names { + meta, ok := evmchain.MetadataByName(name) + if !ok { + return nil, errors.New("unknown anvil chain", "name", name) + } + chains = append(chains, EVMChain{ + Metadata: meta, + }) + } + + return chains, nil +} + +// PublicChainByName returns the public chain definition by name. +func PublicChainByName(name string) (EVMChain, error) { + switch name { + case chainHolesky.Name: + return chainHolesky, nil + case chainArbSepolia.Name: + return chainArbSepolia, nil + case chainOpSepolia.Name: + return chainOpSepolia, nil + case chainEthereum.Name: + return chainEthereum, nil + default: + return EVMChain{}, errors.New("unknown chain name") + } +} + +// PublicRPCByName returns the public chain RPC address by name. +func PublicRPCByName(name string) string { + switch name { + case chainHolesky.Name: + return "https://ethereum-holesky-rpc.publicnode.com" + case chainArbSepolia.Name: + return "https://sepolia-rollup.arbitrum.io/rpc" + case chainOpSepolia.Name: + return "https://sepolia.optimism.io" + case chainEthereum.Name: + return "https://ethereum-rpc.publicnode.com" + default: + return "" + } +} + +func mustMetadata(chainID uint64) evmchain.Metadata { + meta, ok := evmchain.MetadataByID(chainID) + if !ok { + panic("unknown chain ID") + } + + return meta +} diff --git a/e2e/types/infra.go b/e2e/types/infra.go new file mode 100644 index 00000000..dbef198d --- /dev/null +++ b/e2e/types/infra.go @@ -0,0 +1,53 @@ +package types + +import ( + "context" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/cometbft/cometbft/test/e2e/pkg/infra" +) + +func DefaultUpgradeConfig() UpgradeConfig { + return UpgradeConfig{ + ServiceRegexp: ".*", + } +} + +type UpgradeConfig struct { + ServiceRegexp string +} + +type InfraProvider interface { + infra.Provider + + Upgrade(ctx context.Context, cfg UpgradeConfig) error + + // Clean deletes all containers, networks, and data on disk. + Clean(ctx context.Context) error +} + +// InfrastructureData wraps e2e.InfrastructureData with additional iliad-specific fields. +type InfrastructureData struct { + e2e.InfrastructureData + + // VMs maps the VM name to its instance data. + // Note this differs from e2e.InfrastructureData.Instances, which maps the service names to its instance data. + VMs map[string]e2e.InstanceData +} + +// ServicesByInstance returns the set of services associated to the instance. +func (d InfrastructureData) ServicesByInstance(data e2e.InstanceData) map[string]bool { + resp := make(map[string]bool) + for serviceName, instance := range d.Instances { + if instancesEqual(data, instance) { + resp[serviceName] = true + } + } + + return resp +} + +// instancesEqual returns true if the two instances are equal, as identified by IPs. +func instancesEqual(a, b e2e.InstanceData) bool { + return a.IPAddress.Equal(b.IPAddress) && a.ExtIPAddress.Equal(b.ExtIPAddress) +} diff --git a/e2e/types/manifest.go b/e2e/types/manifest.go new file mode 100644 index 00000000..8074b5c6 --- /dev/null +++ b/e2e/types/manifest.go @@ -0,0 +1,112 @@ +package types + +import ( + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/lib/netconf" +) + +// Mode defines the iliad consensus node mode. +// Nodes are in general full nodes (light nodes are not supported yet). +// In some cases, additional roles are defined: validator, archive, seed. +// +// Note that the execution clients only have two modes: "default" and "archive". +// +// e2e.Mode is extended so ModeArchive can be added transparently. +type Mode = e2e.Mode + +const ( + // ModeValidator defines a validator node. + // It's validator key has staked tokens and it actively participates in consensus and is subject to rewards and penalties. + // It must always be online, otherwise it will get stashed/jailed. + // [genesis_validator_set=true,pruning=default,consensus=default,special_p2p=false]. + // Note technically a validator node is also a "full node". + ModeValidator = e2e.ModeValidator + + // ModeArchive defines an archive node. + // It stores all historical blocks and state, it doesn't delete anything ever. It will require TBs of disk. + // [genesis_validator_set=false,pruning=none,consensus=default,special_p2p=false]. + // Note technically an archive node is also a "full node". + ModeArchive Mode = "archive" + + // ModeSeed defines a seed node. It must have a long-lived p2p pubkey and address (encoded in repo). + // It acts as notice board for external nodes to learn about the network and connect to publicly available nodes. + // It crawls the network regularly, making it available to new nodes. + // [genesis_validator_set=false,pruning=default,consensus=default,special_p2p=true]. + // Note technically a seed node is also a "full node". + ModeSeed = e2e.ModeSeed + + // ModeFull defines a full node. A full node a normal node without a special role. + // [genesis_validator_set=false,pruning=default,consensus=default,special_p2p=false]. + ModeFull = e2e.ModeFull + + // ModeLight defines a light node. This isn't used yet. + // [genesis_validator_set=false,pruning=no_data,consensus=light,special_p2p=false] + // Only light nodes are not also full nodes. + ModeLight = e2e.ModeLight +) + +// Perturb defines non-cometBFT perturbations of components like iliad_evm. +type Perturb string + +const ( + // PerturbRestart defines a perturbation that restarts a docker container. + PerturbRestart Perturb = "restart" + // PerturbStopStart defines a perturbation that stops and then starts a docker container. + PerturbStopStart Perturb = "stopstart" +) + +// Manifest wraps e2e.Manifest with additional iliad-specific fields. +// + +type Manifest struct { + e2e.Manifest + + Network netconf.ID `toml:"network"` + + // AnvilChains defines the anvil chains to deploy; mock_l1, mock_l2, etc. + AnvilChains []string `toml:"anvil_chains"` + + // PublicChains defines the public chains to connect to; arb_sepolia, etc. + PublicChains []string `toml:"public_chains"` + + // MultiIliadEVMs defines whether to deploy one or multiple Iliad EVMs. + MultiIliadEVMs bool `toml:"multi_iliad_evms"` + + // Keys contains long-lived private keys (address by type) by node name. + Keys map[string]map[key.Type]string `toml:"keys"` + + // Perturb defines additional (non-cometBFT) perturbations by service name. + Perturb map[string][]Perturb `json:"perturb"` +} + +// Seeds returns a map of seed nodes by name. +func (m Manifest) Seeds() map[string]bool { + resp := make(map[string]bool) + for name, node := range m.Nodes { + if Mode(node.Mode) == ModeSeed { + resp[name] = true + } + } + + return resp +} + +// IliadEVMs returns a map of iliad evm instances names by to deploy. +// If only a single Iliad EVM is to be deployed, the name is "iliad_evm". +// Otherwise, the names are "_evm". +func (m Manifest) IliadEVMs() map[string]bool { + if !m.MultiIliadEVMs { + return map[string]bool{ + "iliad_evm": false, + } + } + + resp := make(map[string]bool) + for name, node := range m.Nodes { + resp[name+"_evm"] = Mode(node.Mode) == ModeArchive + } + + return resp +} diff --git a/e2e/types/testnet.go b/e2e/types/testnet.go new file mode 100644 index 00000000..a9b14d45 --- /dev/null +++ b/e2e/types/testnet.go @@ -0,0 +1,181 @@ +package types + +import ( + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "math/big" + "net" + "strings" + "sync/atomic" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enode" + + "github.com/piplabs/story/lib/evmchain" + "github.com/piplabs/story/lib/netconf" +) + +// Testnet wraps e2e.Testnet with additional iliad-specific fields. +type Testnet struct { + *e2e.Testnet + Network netconf.ID + IliadEVMs []IliadEVM + AnvilChains []AnvilChain + PublicChains []PublicChain + Perturb map[string][]Perturb +} + +// RandomIliadAddr returns a random iliad address for cometBFT rpc clients. +// It uses the internal IP address of a random node that isn't delayed or a seed. +func (t Testnet) RandomIliadAddr() string { + var eligible []string + for _, node := range t.Nodes { + if node.StartAt != 0 || node.Mode == ModeSeed { + continue // Skip delayed nodes or seed nodes + } + + eligible = append(eligible, node.AddressRPC()) + } + + if len(eligible) == 0 { + return "" + } + + randIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligible)))) + if err != nil { + return "" + } + + return eligible[randIdx.Uint64()] +} + +// BroadcastIliadEVM returns a Iliad EVM to use for e2e app tx broadcasts. +// It prefers a validator nodes since we have an issue with mempool+p2p+startup where +// txs get stuck in non-validator mempool immediately after startup if not connected to peers yet. +// Also avoid validators that are not started immediately. +func (t Testnet) BroadcastIliadEVM() IliadEVM { + isDelayed := func(evm string) bool { + for _, node := range t.Nodes { + if node.StartAt > 0 && strings.Contains(evm, node.Name) { + return true + } + } + + return false + } + + for _, evm := range t.IliadEVMs { + if strings.Contains(evm.InstanceName, "validator") && !isDelayed(evm.InstanceName) { + return evm + } + } + + return t.IliadEVMs[0] +} + +// BroadcastNode returns a iliad node to use for RPC queries broadcasts. +// It prefers a validator nodes since we have an issue with mempool+p2p+startup where +// txs get stuck in non-validator mempool immediately after startup if not connected to peers yet. +// Also avoid validators that are not started immediately. +func (t Testnet) BroadcastNode() *e2e.Node { + for _, node := range t.Nodes { + if !strings.Contains(node.Name, "validator") { + continue + } + if node.StartAt > 0 { + continue + } + + return node + } + + return t.Nodes[0] +} + +// HasPerturbations returns whether the network has any perturbations. +func (t Testnet) HasPerturbations() bool { + if len(t.Perturb) > 0 { + return true + } + + return t.Testnet.HasPerturbations() +} + +func (t Testnet) HasIliadEVM() bool { + return len(t.IliadEVMs) > 0 +} + +// EVMChain represents a EVM chain in a iliad network. +type EVMChain struct { + evmchain.Metadata + IsPublic bool +} + +// IliadEVM represents a iliad evm instance in a iliad network. Similar to e2e.Node for iliad instances. +type IliadEVM struct { + Chain EVMChain // For netconf (all instances must have the same chain) + InstanceName string // For docker container name + AdvertisedIP net.IP // For setting up NAT on geth bootnode + ProxyPort uint32 // For binding + InternalRPC string // For JSON-RPC queries from client + ExternalRPC string // For JSON-RPC queries from e2e app. + IsArchive bool // Whether this instance is in archive mode + JWTSecret string // JWT secret for authentication + + // P2P networking + NodeKey *ecdsa.PrivateKey // Private key + Enode *enode.Node // Public key + Peers []*enode.Node // Peer public keys +} + +// NodeKeyHex returns the hex-encoded node key. Used for geth's config. +func (o IliadEVM) NodeKeyHex() string { + return hex.EncodeToString(crypto.FromECDSA(o.NodeKey)) +} + +// AnvilChain represents an anvil chain instance in a iliad network. +type AnvilChain struct { + Chain EVMChain // For netconf + InternalIP net.IP // For docker container IP + ProxyPort uint32 // For binding + InternalRPC string // For JSON-RPC queries from client + ExternalRPC string // For JSON-RPC queries from e2e app. + LoadState string // File path to load anvil state from +} + +// PublicChain represents a public chain in a iliad network. +type PublicChain struct { + chain EVMChain // For netconf + rpcAddresses []string // For JSON-RPC queries from client/e2e app. + next *atomic.Int32 // For round-robin RPC address selection +} + +func NewPublicChain(chain EVMChain, rpcAddresses []string) PublicChain { + return PublicChain{ + chain: chain, + rpcAddresses: rpcAddresses, + next: new(atomic.Int32), + } +} + +// Chain returns the EVM chain. +func (c PublicChain) Chain() EVMChain { + return c.chain +} + +// NextRPCAddress returns the next RPC address in the list. +func (c PublicChain) NextRPCAddress() string { + i := c.next.Load() + defer func() { + c.next.Store(i + 1) + }() + + l := len(c.rpcAddresses) + if l == 0 { + return "" + } + + return strings.TrimSpace(c.rpcAddresses[int(i)%l]) +} diff --git a/e2e/types/testnet_test.go b/e2e/types/testnet_test.go new file mode 100644 index 00000000..531418fb --- /dev/null +++ b/e2e/types/testnet_test.go @@ -0,0 +1,19 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/types" +) + +func TestNextRPCAddress(t *testing.T) { + t.Parallel() + c := types.NewPublicChain(types.EVMChain{}, []string{"1 ", " 2", "3"}) + + require.Equal(t, "1", c.NextRPCAddress()) + require.Equal(t, "2", c.NextRPCAddress()) + require.Equal(t, "3", c.NextRPCAddress()) + require.Equal(t, "1", c.NextRPCAddress()) +} diff --git a/e2e/vmcompose/data.go b/e2e/vmcompose/data.go new file mode 100644 index 00000000..d0d602e5 --- /dev/null +++ b/e2e/vmcompose/data.go @@ -0,0 +1,89 @@ +package vmcompose + +import ( + "encoding/json" + "net" + "os" + "regexp" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/evmchain" +) + +var iliadEvmRegx = regexp.MustCompile(".*_evm") + +const ( + evmPort = 8545 + iliadPort = 26657 +) + +type vmJSON struct { + Name string `json:"name"` + IP string `json:"ip"` + ExternalIP string `json:"external_ip,omitempty"` +} +type dataJSON struct { + NetworkCIDR string `json:"network_cidr"` + VMs []vmJSON `json:"vms"` + ServicesByVM map[string]string `json:"services_by_vm"` // map[service_name]vm_name +} + +// LoadData returns the vmcompose infrastructure data from the given path. +func LoadData(path string) (types.InfrastructureData, error) { + bz, err := os.ReadFile(path) + if err != nil { + return types.InfrastructureData{}, errors.Wrap(err, "read file") + } + + var data dataJSON + err = json.Unmarshal(bz, &data) + if err != nil { + return types.InfrastructureData{}, errors.Wrap(err, "unmarshal json") + } + + vmsByName := make(map[string]e2e.InstanceData) + for _, vm := range data.VMs { + ip := net.ParseIP(vm.IP) + externalIP := net.ParseIP(vm.ExternalIP) + + vmsByName[vm.Name] = e2e.InstanceData{ + IPAddress: ip, + ExtIPAddress: externalIP, + } + } + + instances := make(map[string]e2e.InstanceData) + for serviceName, vmName := range data.ServicesByVM { + vm, ok := vmsByName[vmName] + if !ok { + return types.InfrastructureData{}, errors.New("vm not found", "name", vmName) + } + + // Default ports, as VMs don't support overlapping ports. + port := iliadPort + if iliadEvmRegx.MatchString(serviceName) { + port = evmPort + } else if _, ok := evmchain.MetadataByName(serviceName); ok { + port = evmPort + } + + instances[serviceName] = e2e.InstanceData{ + IPAddress: vm.IPAddress, + ExtIPAddress: vm.ExtIPAddress, + Port: uint32(port), + } + } + + return types.InfrastructureData{ + InfrastructureData: e2e.InfrastructureData{ + Path: path, + Provider: ProviderName, + Instances: instances, + Network: data.NetworkCIDR, + }, + VMs: vmsByName, + }, nil +} diff --git a/e2e/vmcompose/provider.go b/e2e/vmcompose/provider.go new file mode 100644 index 00000000..8cb8a7e9 --- /dev/null +++ b/e2e/vmcompose/provider.go @@ -0,0 +1,397 @@ +package vmcompose + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + + "golang.org/x/sync/errgroup" + + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + + "github.com/piplabs/story/e2e/app/agent" + "github.com/piplabs/story/e2e/docker" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/evmchain" + "github.com/piplabs/story/lib/log" +) + +const ProviderName = "vmcompose" + +var _ types.InfraProvider = (*Provider)(nil) + +type Provider struct { + Testnet types.Testnet + Data types.InfrastructureData + once sync.Once + iliadTag string +} + +func NewProvider(testnet types.Testnet, data types.InfrastructureData, imgTag string) *Provider { + return &Provider{ + Testnet: testnet, + Data: data, + iliadTag: imgTag, + } +} + +// Setup generates the docker-compose file for each VM IP. +func (p *Provider) Setup() error { + // Group infra services by VM IP + for vmIP, services := range groupByVM(p.Data.Instances) { + // Get all iliad nodes in this VM + var nodes []*e2e.Node + var iliads []string + for _, node := range p.Testnet.Nodes { + if services[node.Name] { + nodes = append(nodes, node) + iliads = append(iliads, node.Name) + } + } + + var geths []string + for _, iliadEVM := range p.Testnet.IliadEVMs { + if services[iliadEVM.InstanceName] { + geths = append(geths, iliadEVM.InstanceName) + } + } + + // Get all iliadEVMs in this VM + var iliadEVMs []types.IliadEVM + for _, iliadEVM := range p.Testnet.IliadEVMs { + if services[iliadEVM.InstanceName] { + iliadEVMs = append(iliadEVMs, iliadEVM) + } + } + + // Get all anvil chains in this VM + var anvilChains []types.AnvilChain + for _, anvilChain := range p.Testnet.AnvilChains { + if services[anvilChain.Chain.Name] { + anvilChains = append(anvilChains, anvilChain) + } + } + + def := docker.ComposeDef{ + Network: false, + BindAll: true, + NetworkName: p.Testnet.Name, + NetworkCIDR: p.Testnet.IP.String(), + Nodes: nodes, + IliadEVMs: iliadEVMs, + Anvils: anvilChains, + Prometheus: p.Testnet.Prometheus, + IliadTag: p.iliadTag, + } + compose, err := docker.GenerateComposeFile(def) + if err != nil { + return errors.Wrap(err, "generate compose file") + } + + err = os.WriteFile(filepath.Join(p.Testnet.Dir, vmComposeFile(vmIP)), compose, 0o644) + if err != nil { + return errors.Wrap(err, "write compose file") + } + + if !p.Testnet.Prometheus { + continue // No need to generate prometheus config + } + + // Update custom prometheus.yml config for this VM + promCfgFile := filepath.Join(p.Testnet.Dir, "prometheus", "prometheus.yml") + agentCfg, err := os.ReadFile(promCfgFile) + if err != nil { + return errors.Wrap(err, "read prometheus config") + } + + hostname := vmIP // TODO: Add hostnames to infra instances. + agentCfg = agent.ConfigForHost(agentCfg, hostname, iliads, geths) + err = os.WriteFile(filepath.Join(p.Testnet.Dir, vmAgentFile(vmIP)), agentCfg, 0o644) + if err != nil { + return errors.Wrap(err, "write compose file") + } + } + + return nil +} + +// Upgrade copies some of the local e2e generated artifacts to the VMs and starts the docker-compose services. +func (p *Provider) Upgrade(ctx context.Context, cfg types.UpgradeConfig) error { + log.Info(ctx, "Upgrading docker-compose on VMs", "image", p.Testnet.UpgradeVersion) + + filesByService := make(map[string][]string) + addFile := func(service string, paths ...string) { + filesByService[service] = append(filesByService[service], filepath.Join(paths...)) + } + + // Include iliad config + for _, node := range p.Testnet.Nodes { + addFile(node.Name, "config", "iliad.toml") + addFile(node.Name, "config", "config.toml") + addFile(node.Name, "config", "jwtsecret") + addFile(node.Name, "config", "priv_validator_key.json") + addFile(node.Name, "config", "node_key.json") + } + + // Include geth config + for _, iliadEVM := range p.Testnet.IliadEVMs { + addFile(iliadEVM.InstanceName, "config.toml") + addFile(iliadEVM.InstanceName, "geth", "jwtsecret") + addFile(iliadEVM.InstanceName, "geth", "nodekey") + } + + addFile("prometheus", "prometheus.yml") // Prometheus isn't a "service", so not actually copied + + // Do initial sequential ssh to each VM, ensure we can connect. + for vmName, instance := range p.Data.VMs { + if !matchAny(cfg, p.Data.ServicesByInstance(instance)) { + log.Debug(ctx, "Skipping vm upgrade, no matching services", "vm", vmName, "regexp", cfg.ServiceRegexp) + continue + } + + log.Debug(ctx, "Ensuring VM SSH connection", "vm", vmName) + if err := execOnVM(ctx, vmName, "ls"); err != nil { + return errors.Wrap(err, "test exec on vm", "vm", vmName) + } + } + + // Then upgrade VMs in parallel + eg, ctx := errgroup.WithContext(ctx) + for vmName, instance := range p.Data.VMs { + services := p.Data.ServicesByInstance(instance) + if !matchAny(cfg, services) { + continue + } + + eg.Go(func() error { + log.Debug(ctx, "Copying artifacts", "vm", vmName, "count", len(filesByService)) + for service, filePaths := range filesByService { + if !services[service] { + continue + } + for _, filePath := range filePaths { + localPath := filepath.Join(p.Testnet.Dir, service, filePath) + remotePath := filepath.Join("/iliad", p.Testnet.Name, service, filePath) + if err := copyFileToVM(ctx, vmName, localPath, remotePath); err != nil { + return errors.Wrap(err, "copy file", "vm", vmName, "service", service, "file", filePath) + } + } + } + + log.Debug(ctx, "Copying docker-compose.yml", "vm", vmName) + composeFile := vmComposeFile(instance.IPAddress.String()) + localComposePath := filepath.Join(p.Testnet.Dir, composeFile) + remoteComposePath := filepath.Join("/iliad", p.Testnet.Name, composeFile) + if err := copyFileToVM(ctx, vmName, localComposePath, remoteComposePath); err != nil { + return errors.Wrap(err, "copy docker compose", "vm", vmName) + } + + // Figure out whether we need to call "docker compose down" before "docker compose up" + var maybeDown string + if services := p.Data.ServicesByInstance(instance); containsEVM(services) { + if containsAnvil(services) { + return errors.New("cannot upgrade VM with both iliad_evm and anvil containers since iliad_evm needs downing and anvil cannot be restarted", "vm", vmName) + } + maybeDown = "sudo docker compose down && " + } + + startCmd := fmt.Sprintf("cd /iliad/%s && "+ + "sudo mv %s docker-compose.yaml && "+ + maybeDown+ + "sudo docker compose up -d", + p.Testnet.Name, composeFile) + + log.Debug(ctx, "Executing docker-compose up", "vm", vmName) + if err := execOnVM(ctx, vmName, startCmd); err != nil { + return errors.Wrap(err, "compose up", "vm", vmName) + } + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "wait errgroup") + } + + return nil +} + +// matchAny returns true if the pattern matches any of the services in the services map. +// An empty pattern returns true, matching anything. +func matchAny(cfg types.UpgradeConfig, services map[string]bool) bool { + if cfg.ServiceRegexp == "" { + return true + } + + for service := range services { + matched, _ := regexp.MatchString(cfg.ServiceRegexp, service) + if matched { + return true + } + } + + return false +} + +func (p *Provider) StartNodes(ctx context.Context, _ ...*e2e.Node) error { + var onceErr error + p.once.Do(func() { + log.Info(ctx, "Copying artifacts to VMs") + for vmName := range p.Data.VMs { + err := copyToVM(ctx, vmName, p.Testnet.Dir) + if err != nil { + onceErr = errors.Wrap(err, "copy files", "vm", vmName) + return + } + } + + log.Info(ctx, "Starting VM deployments") + // TODO: Only start additional services and then start iliad as per above StartNodes. + for vmName, instance := range p.Data.VMs { + composeFile := vmComposeFile(instance.IPAddress.String()) + agentFile := vmAgentFile(instance.IPAddress.String()) + startCmd := fmt.Sprintf("cd /iliad/%s && "+ + "sudo mv %s docker-compose.yaml && "+ + "sudo mv %s prometheus/prometheus.yml && "+ + "sudo docker compose up -d", + p.Testnet.Name, composeFile, agentFile) + + err := execOnVM(ctx, vmName, startCmd) + if err != nil { + onceErr = errors.Wrap(err, "compose up", "vm", vmName) + return + } + } + }) + + return onceErr +} + +func (p *Provider) Clean(ctx context.Context) error { + log.Info(ctx, "Deleting existing VM deployments including data") + for vmName := range p.Data.VMs { + for _, cmd := range docker.CleanCmds(true, true) { + err := execOnVM(ctx, vmName, cmd) + if err != nil { + return errors.Wrap(err, "clean docker containers", "vm", vmName) + } + + err = execOnVM(ctx, vmName, "sudo rm -rf /iliad/*") + if err != nil { + return errors.Wrap(err, "clean docker containers", "vm", vmName) + } + } + } + + return nil +} + +func (*Provider) StopTestnet(context.Context) error { + return errors.New("not implemented") +} + +func (p *Provider) GetInfrastructureData() *e2e.InfrastructureData { + return &p.Data.InfrastructureData +} + +func groupByVM(instances map[string]e2e.InstanceData) map[string]map[string]bool { + resp := make(map[string]map[string]bool) // map[vm_ip]map[service_name]true + for serviceName, instance := range instances { + ip := instance.IPAddress.String() + m, ok := resp[ip] + if !ok { + m = make(map[string]bool) + } + m[serviceName] = true + resp[ip] = m + } + + return resp +} + +func execOnVM(ctx context.Context, vmName string, cmd string) error { + ssh := fmt.Sprintf("gcloud compute ssh --zone=us-east1-c %s --quiet -- \"%s\"", vmName, cmd) + + out, err := exec.CommandContext(ctx, "bash", "-c", ssh).CombinedOutput() + if err != nil { + return errors.Wrap(err, "exec on VM", "output", string(out), "cmd", ssh) + } + + return nil +} + +func copyToVM(ctx context.Context, vmName string, path string) error { + tarscp := fmt.Sprintf("tar czf - %s | gcloud compute ssh --zone=us-east1-c %s --quiet -- \"cd /iliad && tar xzf -\"", filepath.Base(path), vmName) + + cmd := exec.CommandContext(ctx, "bash", "-c", tarscp) + cmd.Dir = filepath.Dir(path) + if out, err := cmd.CombinedOutput(); err != nil { + return errors.Wrap(err, "copy to VM", "output", string(out)) + } + + return nil +} + +func copyFileToVM(ctx context.Context, vmName string, localPath string, remotePath string) error { + scp := fmt.Sprintf("gcloud compute scp --zone=us-east1-c --quiet %s %s:%s", localPath, vmName, remotePath) + cmd := exec.CommandContext(ctx, "bash", "-c", scp) + if out, err := cmd.CombinedOutput(); err != nil { + return errors.Wrap(err, "copy to VM", "output", string(out), "cmd", scp) + } + + return nil +} + +func vmAgentFile(internalIP string) string { + return "prometheus/" + strings.ReplaceAll(internalIP, ".", "_") + "_prometheus.yml" +} + +func vmComposeFile(internalIP string) string { + return strings.ReplaceAll(internalIP, ".", "_") + "_compose.yaml" +} + +// containsEVM returns true if the services map contains an iliad evm. +// TODO: This isn't very robust. +func containsEVM(services map[string]bool) bool { + for service, ok := range services { + if !ok { + continue + } + if strings.Contains(service, "evm") { + return true + } + } + + return false +} + +// containsAnvil returns true if the services map contains an anvil chain. +func containsAnvil(services map[string]bool) bool { + anvils := map[uint64]bool{ + evmchain.IDMockL1Fast: true, + evmchain.IDMockL1Slow: true, + evmchain.IDMockL2: true, + } + for service, ok := range services { + if !ok { + continue + } + meta, ok := evmchain.MetadataByName(service) + if !ok { + continue + } else if !anvils[meta.ChainID] { + continue + } + + return true + } + + return false +} diff --git a/e2e/vmcompose/provider_internal_test.go b/e2e/vmcompose/provider_internal_test.go new file mode 100644 index 00000000..f651854f --- /dev/null +++ b/e2e/vmcompose/provider_internal_test.go @@ -0,0 +1,73 @@ +package vmcompose + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// SetupDataFixtures returns the test fixture filenames of manifest and infrastructure data files. +// This accesses private types, so it's in the same package as the types. +func SetupDataFixtures(t *testing.T) (string, string) { + t.Helper() + + // Write manifest to disk + manifest := ` +network = "devnet" +anvil_chains = ["mock_l1"] +multi_iliad_evms = true + +[node.validator01] +[node.validator02] + +[node.seed01] +mode = "seed" + +[node.fullnode01] +mode = "full" +` + manifestFile := filepath.Join(t.TempDir(), "test.toml") + err := os.WriteFile(manifestFile, []byte(manifest), 0o644) + require.NoError(t, err) + + const vm1, vm2, vm3, vm4, vm5, vm6 = "vm1", "vm2", "vm3", "vm4", "vm5", "vm6" + + dataJSON := dataJSON{ + NetworkCIDR: "127.0.0.1/24", + VMs: []vmJSON{ + {Name: vm1, IP: "127.0.0.1"}, + {Name: vm2, IP: "127.0.0.2"}, + {Name: vm3, IP: "127.0.0.3"}, + {Name: vm4, IP: "127.0.0.4"}, + {Name: vm5, IP: "127.0.0.5"}, + {Name: vm6, IP: "127.0.0.6"}, + }, + ServicesByVM: map[string]string{ + "validator01": vm1, + "validator01_evm": vm1, + + "validator02": vm2, + "validator02_evm": vm2, + + "mock_l1": vm3, + + "seed01": vm4, + "seed01_evm": vm4, + + "fullnode01": vm5, + "fullnode01_evm": vm5, + }, + } + + // Write raw data json to disk + bz, err := json.Marshal(dataJSON) + require.NoError(t, err) + dataFile := filepath.Join(t.TempDir(), "data.json") + err = os.WriteFile(dataFile, bz, 0o644) + require.NoError(t, err) + + return manifestFile, dataFile +} diff --git a/e2e/vmcompose/provider_test.go b/e2e/vmcompose/provider_test.go new file mode 100644 index 00000000..e1f87eb6 --- /dev/null +++ b/e2e/vmcompose/provider_test.go @@ -0,0 +1,57 @@ +package vmcompose_test + +import ( + "context" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/app" + "github.com/piplabs/story/e2e/vmcompose" + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test . -golden -clean + +func TestSetup(t *testing.T) { + t.Parallel() + manifestFile, dataFile := vmcompose.SetupDataFixtures(t) + + def, err := app.MakeDefinition(context.Background(), app.DefinitionConfig{ + ManifestFile: manifestFile, + InfraProvider: vmcompose.ProviderName, + InfraDataFile: dataFile, + IliadImgTag: "7d1ae53", + }, "") + require.NoError(t, err) + + err = os.MkdirAll(def.Testnet.Dir, 0o755) + require.NoError(t, err) + + err = def.Infra.Setup() + require.NoError(t, err) + + files, err := filepath.Glob(filepath.Join(def.Testnet.Dir, "*compose.yaml")) + require.NoError(t, err) + + for _, file := range files { + t.Run(filepath.Base(file), func(t *testing.T) { + t.Parallel() + bz, err := os.ReadFile(file) + require.NoError(t, err) + + // Replace non-deterministic fields with placeholders + + re1 := regexp.MustCompile(`--nodekeyhex=([0-9a-fA-F]+)`) + bz = re1.ReplaceAll(bz, []byte("--nodekeyhex=")) + + re2 := regexp.MustCompile(`enode://([0-9a-fA-F]+)`) + bz = re2.ReplaceAll(bz, []byte("enode://")) + + tutil.RequireGoldenBytes(t, bz) + }) + } +} diff --git a/e2e/vmcompose/testdata/TestSetup_127_0_0_1_compose.yaml.golden b/e2e/vmcompose/testdata/TestSetup_127_0_0_1_compose.yaml.golden new file mode 100644 index 00000000..3ef432a2 --- /dev/null +++ b/e2e/vmcompose/testdata/TestSetup_127_0_0_1_compose.yaml.golden @@ -0,0 +1,73 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + +services: + validator01: + labels: + e2e: true + container_name: validator01 + image: iliadops/iliad:7d1ae53 + init: true + ports: + - 26656:26656 + - 26657:26657 + - 6060 + volumes: + - ./validator01:/iliad + depends_on: + validator01_evm: + condition: service_healthy + networks: + test: + + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + validator01_evm-init: + labels: + e2e: true + container_name: validator01_evm-init + image: "ethereum/client-go:v1.14.5" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./validator01_evm:/geth + networks: + test: + + validator01_evm: + labels: + e2e: true + container_name: validator01_evm + image: "ethereum/client-go:v1.14.5" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip: + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551:8551 + - 8545:8545 + - 30303:30303 + - 8546 + - 6060 + depends_on: + validator01_evm-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./validator01_evm:/geth + networks: + test: + + diff --git a/e2e/vmcompose/testdata/TestSetup_127_0_0_2_compose.yaml.golden b/e2e/vmcompose/testdata/TestSetup_127_0_0_2_compose.yaml.golden new file mode 100644 index 00000000..86642051 --- /dev/null +++ b/e2e/vmcompose/testdata/TestSetup_127_0_0_2_compose.yaml.golden @@ -0,0 +1,73 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + +services: + validator02: + labels: + e2e: true + container_name: validator02 + image: iliadops/iliad:7d1ae53 + init: true + ports: + - 26656:26656 + - 26657:26657 + - 6060 + volumes: + - ./validator02:/iliad + depends_on: + validator02_evm: + condition: service_healthy + networks: + test: + + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + validator02_evm-init: + labels: + e2e: true + container_name: validator02_evm-init + image: "ethereum/client-go:v1.14.5" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./validator02_evm:/geth + networks: + test: + + validator02_evm: + labels: + e2e: true + container_name: validator02_evm + image: "ethereum/client-go:v1.14.5" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip: + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551:8551 + - 8545:8545 + - 30303:30303 + - 8546 + - 6060 + depends_on: + validator02_evm-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./validator02_evm:/geth + networks: + test: + + diff --git a/e2e/vmcompose/testdata/TestSetup_127_0_0_3_compose.yaml.golden b/e2e/vmcompose/testdata/TestSetup_127_0_0_3_compose.yaml.golden new file mode 100644 index 00000000..bfe4bdc5 --- /dev/null +++ b/e2e/vmcompose/testdata/TestSetup_127_0_0_3_compose.yaml.golden @@ -0,0 +1,33 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + +services: + # Initialises geth files and folder from provided genesis file. + mock_l1: + labels: + e2e: true + container_name: mock_l1 + platform: linux/amd64 + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id=1652 + - --block-time=1 + - --silent + - --load-state=/anvil/state.json + ports: + - 8545:8545 + networks: + test: + + + volumes: + - ./anvil/state.json:/anvil/state.json + + + # Use geth as the iliad EVMs. diff --git a/e2e/vmcompose/testdata/TestSetup_127_0_0_4_compose.yaml.golden b/e2e/vmcompose/testdata/TestSetup_127_0_0_4_compose.yaml.golden new file mode 100644 index 00000000..01a8684d --- /dev/null +++ b/e2e/vmcompose/testdata/TestSetup_127_0_0_4_compose.yaml.golden @@ -0,0 +1,73 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + +services: + seed01: + labels: + e2e: true + container_name: seed01 + image: iliadops/iliad:7d1ae53 + init: true + ports: + - 26656:26656 + - 26657:26657 + - 6060 + volumes: + - ./seed01:/iliad + depends_on: + seed01_evm: + condition: service_healthy + networks: + test: + + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + seed01_evm-init: + labels: + e2e: true + container_name: seed01_evm-init + image: "ethereum/client-go:v1.14.5" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./seed01_evm:/geth + networks: + test: + + seed01_evm: + labels: + e2e: true + container_name: seed01_evm + image: "ethereum/client-go:v1.14.5" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip: + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551:8551 + - 8545:8545 + - 30303:30303 + - 8546 + - 6060 + depends_on: + seed01_evm-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./seed01_evm:/geth + networks: + test: + + diff --git a/e2e/vmcompose/testdata/TestSetup_127_0_0_5_compose.yaml.golden b/e2e/vmcompose/testdata/TestSetup_127_0_0_5_compose.yaml.golden new file mode 100644 index 00000000..11336b49 --- /dev/null +++ b/e2e/vmcompose/testdata/TestSetup_127_0_0_5_compose.yaml.golden @@ -0,0 +1,73 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + +services: + fullnode01: + labels: + e2e: true + container_name: fullnode01 + image: iliadops/iliad:7d1ae53 + init: true + ports: + - 26656:26656 + - 26657:26657 + - 6060 + volumes: + - ./fullnode01:/iliad + depends_on: + fullnode01_evm: + condition: service_healthy + networks: + test: + + + + # Use geth as the iliad EVMs. + # Initialises geth files and folder from provided genesis file. + fullnode01_evm-init: + labels: + e2e: true + container_name: fullnode01_evm-init + image: "ethereum/client-go:v1.14.5" + command: --state.scheme=path --datadir=/geth init /geth/genesis.json + volumes: + - ./fullnode01_evm:/geth + networks: + test: + + fullnode01_evm: + labels: + e2e: true + container_name: fullnode01_evm + image: "ethereum/client-go:v1.14.5" + command: + - --config=/geth/config.toml + # Flags not available via config.toml + - --nat=extip: + - --pprof + - --pprof.addr=0.0.0.0 + - --metrics + + ports: + - 8551:8551 + - 8545:8545 + - 30303:30303 + - 8546 + - 6060 + depends_on: + fullnode01_evm-init: + condition: service_completed_successfully + healthcheck: + test: "nc -z localhost 8545" + interval: 1s + retries: 30 + volumes: + - ./fullnode01_evm:/geth + networks: + test: + + diff --git a/e2e/vmcompose/testdata/TestSetup_127_0_0_6_compose.yaml.golden b/e2e/vmcompose/testdata/TestSetup_127_0_0_6_compose.yaml.golden new file mode 100644 index 00000000..8679bc04 --- /dev/null +++ b/e2e/vmcompose/testdata/TestSetup_127_0_0_6_compose.yaml.golden @@ -0,0 +1,9 @@ +version: '2.4' +networks: + test: + labels: + e2e: true + driver: bridge + +services: + diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..71e2cf16 --- /dev/null +++ b/go.mod @@ -0,0 +1,307 @@ +module github.com/piplabs/story + +go 1.22.0 + +require ( + cosmossdk.io/api v0.7.5 + // TODO(https://github.com/cosmos/rosetta/issues/76): Rosetta requires cosmossdk.io/core v0.12.0 erroneously but + // should use v0.11.0. The Cosmos build fails with types/context.go:65:29: undefined: comet.BlockInfo otherwise. + // (Replace is done below) + cosmossdk.io/core v0.12.0 + cosmossdk.io/depinject v1.0.0-alpha.4 + cosmossdk.io/errors v1.0.1 + cosmossdk.io/log v1.3.1 + cosmossdk.io/math v1.3.0 + cosmossdk.io/orm v1.0.0-beta.3 + cosmossdk.io/store v1.1.0 + cosmossdk.io/x/tx v0.13.3 + github.com/BurntSushi/toml v1.3.2 + github.com/bufbuild/buf v1.31.0 + github.com/charmbracelet/log v0.4.0 + github.com/cometbft/cometbft v0.38.9 + github.com/cosmos/cosmos-db v1.0.2 + github.com/cosmos/cosmos-proto v1.0.0-beta.5 + github.com/cosmos/cosmos-sdk v0.50.7 + github.com/cosmos/gogoproto v1.5.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/ethereum/go-ethereum v1.14.5 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 + github.com/google/uuid v1.6.0 + github.com/grpc-ecosystem/grpc-gateway v1.16.0 + github.com/leodido/go-conventionalcommits v0.12.0 + github.com/muesli/termenv v0.15.2 + github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 + github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + github.com/syndtr/goleveldb v1.0.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 + go.uber.org/goleak v1.3.0 + go.uber.org/mock v0.4.0 + golang.org/x/sync v0.7.0 + golang.org/x/tools v0.21.0 + google.golang.org/grpc v1.64.0 + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0 + google.golang.org/protobuf v1.34.1 + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1 // indirect + connectrpc.com/connect v1.16.1 // indirect + connectrpc.com/otelconnect v0.7.0 // indirect + cosmossdk.io/collections v0.4.0 + filippo.io/edwards25519 v1.0.0 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/DataDog/datadog-go v3.2.0+incompatible // indirect + github.com/DataDog/zstd v1.5.5 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/VictoriaMetrics/fastcache v1.12.2 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect + github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/bufbuild/protocompile v0.9.0 // indirect + github.com/bufbuild/protovalidate-go v0.6.2 // indirect + github.com/bufbuild/protoyaml-go v0.1.9 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v1.1.0 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cometbft/cometbft-db v0.9.1 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gogogateway v1.2.0 // indirect + github.com/cosmos/iavl v1.1.2 // indirect + github.com/cosmos/ics23/go v0.10.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect + github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect + github.com/danieljoos/wincred v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v26.1.0+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v26.1.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.1 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.6.0 // indirect + github.com/emicklei/dot v1.6.1 // indirect + github.com/ethereum/c-kzg-4844 v1.0.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/felixge/fgprof v0.9.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fjl/memsize v0.0.2 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-chi/chi/v5 v5.0.12 // indirect + github.com/go-kit/kit v0.12.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/uuid/v5 v5.1.0 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/glog v1.2.0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/cel-go v0.20.1 // indirect + github.com/google/go-containerregistry v0.19.1 // indirect + github.com/google/orderedcode v0.0.1 // indirect + github.com/google/pprof v0.0.0-20240422182052-72c8669ad3e7 // indirect + github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/hashicorp/go-bexpr v0.1.10 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.3 // indirect + github.com/hashicorp/go-plugin v1.5.2 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hdevalence/ed25519consensus v0.1.0 // indirect + github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/holiman/uint256 v1.2.4 // indirect + github.com/huandu/skiplist v1.2.0 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/improbable-eng/grpc-web v0.15.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jdx/go-netrc v1.0.0 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.7 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/linxGnu/grocksdb v1.8.14 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/minio/highwayhash v1.0.2 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 + github.com/mitchellh/pointerstructure v1.2.0 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/naoina/go-stringutil v0.1.0 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.12 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.52.2 // indirect + github.com/prometheus/procfs v0.13.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rs/cors v1.11.0 // indirect + github.com/rs/zerolog v1.32.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tidwall/btree v1.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/urfave/cli/v2 v2.27.2 // indirect + github.com/vbatts/tar-split v0.11.5 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.etcd.io/bbolt v1.3.8 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/otel/trace v1.27.0 + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gotest.tools/v3 v3.5.1 + nhooyr.io/websocket v1.8.6 // indirect + pgregory.net/rapid v1.1.0 // indirect + rsc.io/tmplfunc v0.0.3 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +require ( + cosmossdk.io/x/upgrade v0.1.4 + github.com/go-playground/validator/v10 v10.11.1 + github.com/joho/godotenv v1.5.1 +) + +require ( + cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.7 // indirect + cloud.google.com/go/storage v1.38.0 // indirect + github.com/aws/aws-sdk-go v1.44.224 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter v1.7.4 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/api v0.171.0 // indirect +) + +replace cosmossdk.io/core v0.12.0 => cosmossdk.io/core v0.11.0 + +// See https://github.com/cosmos/cosmos-sdk/pull/14952 +// Also https://github.com/cosmos/cosmos-db/blob/main/go.mod#L11-L12 +replace github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..1d8b7912 --- /dev/null +++ b/go.sum @@ -0,0 +1,1941 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1 h1:2IGhRovxlsOIQgx2ekZWo4wTPAYpck41+18ICxs37is= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= +cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= +connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= +connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= +connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= +cosmossdk.io/api v0.7.5 h1:eMPTReoNmGUm8DeiQL9DyM8sYDjEhWzL1+nLbI9DqtQ= +cosmossdk.io/api v0.7.5/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= +cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= +cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= +cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= +cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= +cosmossdk.io/depinject v1.0.0-alpha.4 h1:PLNp8ZYAMPTUKyG9IK2hsbciDWqna2z1Wsl98okJopc= +cosmossdk.io/depinject v1.0.0-alpha.4/go.mod h1:HeDk7IkR5ckZ3lMGs/o91AVUc7E596vMaOmslGFM3yU= +cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= +cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= +cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= +cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= +cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= +cosmossdk.io/orm v1.0.0-beta.3 h1:XmffCwsIZE+y0sS4kEfRUfIgvJfGGn3HFKntZ91sWcU= +cosmossdk.io/orm v1.0.0-beta.3/go.mod h1:KSH9lKA+0K++2OKECWwPAasKbUIEtZ7xYG+0ikChiyU= +cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= +cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= +cosmossdk.io/x/tx v0.13.3 h1:Ha4mNaHmxBc6RMun9aKuqul8yHiL78EKJQ8g23Zf73g= +cosmossdk.io/x/tx v0.13.3/go.mod h1:I8xaHv0rhUdIvIdptKIqzYy27+n2+zBVaxO6fscFhys= +cosmossdk.io/x/upgrade v0.1.4 h1:/BWJim24QHoXde8Bc64/2BSEB6W4eTydq0X/2f8+g38= +cosmossdk.io/x/upgrade v0.1.4/go.mod h1:9v0Aj+fs97O+Ztw+tG3/tp5JSlrmT7IcFhAebQHmOPo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= +github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I= +github.com/adlio/schema v1.3.3/go.mod h1:1EsRssiv9/Ce2CMzq5DoL7RiMshhuigQxrR4DMV9fHg= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c= +github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.224 h1:09CiaaF35nRmxrzWZ2uRq5v6Ghg/d2RiPjZnSgtt+RQ= +github.com/aws/aws-sdk-go v1.44.224/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/bufbuild/buf v1.31.0 h1:YHLGIr8bjcLaTCIw0+/bCAvJLiR8u46QTwKvn7miSEg= +github.com/bufbuild/buf v1.31.0/go.mod h1:LlxpG2LF33f1Ixw29BTt0pyLriLzg3rXY1K9XQVHSio= +github.com/bufbuild/protocompile v0.9.0 h1:DI8qLG5PEO0Mu1Oj51YFPqtx6I3qYXUAhJVJ/IzAVl0= +github.com/bufbuild/protocompile v0.9.0/go.mod h1:s89m1O8CqSYpyE/YaSGtg1r1YFMF5nLTwh4vlj6O444= +github.com/bufbuild/protovalidate-go v0.6.2 h1:U/V3CGF0kPlR12v41rjO4DrYZtLcS4ZONLmWN+rJVCQ= +github.com/bufbuild/protovalidate-go v0.6.2/go.mod h1:4BR3rKEJiUiTy+sqsusFn2ladOf0kYmA2Reo6BHSBgQ= +github.com/bufbuild/protoyaml-go v0.1.9 h1:anV5UtF1Mlvkkgp4NWA6U/zOnJFng8Orq4Vf3ZUQHBU= +github.com/bufbuild/protoyaml-go v0.1.9/go.mod h1:KCBItkvZOK/zwGueLdH1Wx1RLyFn5rCH7YjQrdty2Wc= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= +github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= +github.com/cockroachdb/apd/v3 v3.1.0 h1:MK3Ow7LH0W8zkd5GMKA1PvS9qG3bWFI95WaVNfyZJ/w= +github.com/cockroachdb/apd/v3 v3.1.0/go.mod h1:6qgPBMXjATAdD/VefbRP9NoSLKjbB4LCoA7gN4LpHs4= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= +github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/cometbft/cometbft v0.38.9 h1:cJBJBG0mPKz+sqelCi/hlfZjadZQGdDNnu6YQ1ZsUHQ= +github.com/cometbft/cometbft v0.38.9/go.mod h1:xOoGZrtUT+A5izWfHSJgl0gYZUE7lu7Z2XIS1vWG/QQ= +github.com/cometbft/cometbft-db v0.9.1 h1:MIhVX5ja5bXNHF8EYrThkG9F7r9kSfv8BX4LWaxWJ4M= +github.com/cometbft/cometbft-db v0.9.1/go.mod h1:iliyWaoV0mRwBJoizElCwwRA9Tf7jZJOURcRZF9m60U= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= +github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAKs= +github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-sdk v0.50.7 h1:LsBGKxifENR/DN4E1RZaitsyL93HU44x0p8EnMHp4V4= +github.com/cosmos/cosmos-sdk v0.50.7/go.mod h1:84xDDJEHttRT7NDGwBaUOLVOMN0JNE9x7NbsYIxXs1s= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= +github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= +github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU= +github.com/cosmos/gogoproto v1.5.0 h1:SDVwzEqZDDBoslaeZg+dGE55hdzHfgUA40pEanMh52o= +github.com/cosmos/gogoproto v1.5.0/go.mod h1:iUM31aofn3ymidYG6bUR5ZFrk+Om8p5s754eMUcyp8I= +github.com/cosmos/iavl v1.1.2 h1:zL9FK7C4L/P4IF1Dm5fIwz0WXCnn7Bp1M2FxH0ayM7Y= +github.com/cosmos/iavl v1.1.2/go.mod h1:jLeUvm6bGT1YutCaL2fIar/8vGUE8cPZvh/gXEWDaDM= +github.com/cosmos/ics23/go v0.10.0 h1:iXqLLgp2Lp+EdpIuwXTYIQU+AiHj9mOC2X9ab++bZDM= +github.com/cosmos/ics23/go v0.10.0/go.mod h1:ZfJSmng/TBNTBkFemHHHj5YY7VAU/MBU980F4VU1NG0= +github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM= +github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cucumber/common/gherkin/go/v22 v22.0.0 h1:4K8NqptbvdOrjL9DEea6HFjSpbdT9+Q5kgLpmmsHYl0= +github.com/cucumber/common/gherkin/go/v22 v22.0.0/go.mod h1:3mJT10B2GGn3MvVPd3FwR7m2u4tLhSRhWUqJU4KN4Fg= +github.com/cucumber/common/messages/go/v17 v17.1.1 h1:RNqopvIFyLWnKv0LfATh34SWBhXeoFTJnSrgm9cT/Ts= +github.com/cucumber/common/messages/go/v17 v17.1.1/go.mod h1:bpGxb57tDE385Rb2EohgUadLkAbhoC4IyCFi89u/JQI= +github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= +github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v26.1.0+incompatible h1:+nwRy8Ocd8cYNQ60mozDDICICD8aoFGtlPXifX/UQ3Y= +github.com/docker/cli v26.1.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v26.1.0+incompatible h1:W1G9MPNbskA6VZWL7b3ZljTh0pXI68FpINx0GKaOdaM= +github.com/docker/docker v26.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= +github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emicklei/dot v1.6.1 h1:ujpDlBkkwgWUY+qPId5IwapRW/xEoligRSYjioR6DFI= +github.com/emicklei/dot v1.6.1/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.5 h1:szuFzO1MhJmweXjoM5nSAeDvjNUH3vIQoMzzQnfvjpw= +github.com/ethereum/go-ethereum v1.14.5/go.mod h1:VEDGGhSxY7IEjn98hJRFXl/uFvpRgbIIf2PpXiyGGgc= +github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 h1:KrE8I4reeVvf7C1tm8elRjj4BdscTYzz/WAbYyf/JI4= +github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0/go.mod h1:D9AJLVXSyZQXJQVk8oh1EwjISE+sJTn2duYIZC0dy3w= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= +github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA= +github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk= +github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= +github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240422182052-72c8669ad3e7 h1:3q13T5NW3mlTJZM6B5UAsf2N5NYFbYWIyI3W8DlvBDU= +github.com/google/pprof v0.0.0-20240422182052-72c8669ad3e7/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.4 h1:3yQjWuxICvSpYwqSayAdKRFcvBl1y/vogCxczWSmix0= +github.com/hashicorp/go-getter v1.7.4/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= +github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= +github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= +github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= +github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= +github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= +github.com/jhump/protoreflect v1.15.6 h1:WMYJbw2Wo+KOWwZFvgY0jMoVHM6i4XIvRs2RcBj5VmI= +github.com/jhump/protoreflect v1.15.6/go.mod h1:jCHoyYQIJnaabEYnbGwyo9hUqfyUMTbJw/tAut5t97E= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/leodido/go-conventionalcommits v0.12.0 h1:pG01rl8Ze+mxnSSVB2wPdGASXyyU25EGwLUc0bWrmKc= +github.com/leodido/go-conventionalcommits v0.12.0/go.mod h1:DW+n8pQb5w/c7Vba7iGOMS3rkbPqykVlnrDykGjlsJM= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= +github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= +github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= +github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 h1:jik8PHtAIsPlCRJjJzl4udgEf7hawInF9texMeO2jrU= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck= +github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/regen-network/gocuke v0.6.2 h1:pHviZ0kKAq2U2hN2q3smKNxct6hS0mGByFMHGnWA97M= +github.com/regen-network/gocuke v0.6.2/go.mod h1:zYaqIHZobHyd0xOrHGPQjbhGJsuZ1oElx150u2o1xuk= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= +github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k= +go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +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= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/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= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +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= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU= +google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa h1:ePqxpG3LVx+feAUOx8YmR5T7rc0rdzK8DyxM8cQ9zq0= +google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:CnZenrTdRJb7jc+jOm0Rkywq+9wh0QC4U8tyiRbEPPM= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0 h1:9SxA29VM43MF5Z9dQu694wmY5t8E/Gxr7s+RSxiIDmc= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0/go.mod h1:yZOK5zhQMiALmuweVdIVoQPa6eIJyXn2B9g5dJDhqX4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/lib/anvil/accounts.go b/lib/anvil/accounts.go new file mode 100644 index 00000000..268db236 --- /dev/null +++ b/lib/anvil/accounts.go @@ -0,0 +1,152 @@ +package anvil + +import ( + "crypto/ecdsa" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +//nolint:gochecknoglobals // Static keys and addresses +var ( + acc0 = addr("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") + acc1 = addr("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + acc2 = addr("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + acc3 = addr("0x90F79bf6EB2c4f870365E785982E1f101E93b906") + acc4 = addr("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65") + acc5 = addr("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc") + acc6 = addr("0x976EA74026E726554dB657fA54763abd0C3a0aa9") + acc7 = addr("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955") + acc8 = addr("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f") + acc9 = addr("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720") + + pk0 = mustHexToKey("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + pk1 = mustHexToKey("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") + pk2 = mustHexToKey("0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a") + pk3 = mustHexToKey("0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6") + pk4 = mustHexToKey("0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a") + pk5 = mustHexToKey("0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba") + pk6 = mustHexToKey("0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e") + pk7 = mustHexToKey("0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356") + pk8 = mustHexToKey("0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97") + pk9 = mustHexToKey("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6") +) + +// +// Accounts. +// + +func DevAccount0() common.Address { + return acc0 +} + +func DevAccount1() common.Address { + return acc1 +} + +func DevAccount2() common.Address { + return acc2 +} + +func DevAccount3() common.Address { + return acc3 +} + +func DevAccount4() common.Address { + return acc4 +} + +func DevAccount5() common.Address { + return acc5 +} + +func DevAccount6() common.Address { + return acc6 +} + +func DevAccount7() common.Address { + return acc7 +} + +func DevAccount8() common.Address { + return acc8 +} + +func DevAccount9() common.Address { + return acc9 +} + +func IsDevAccount(addr common.Address) bool { + switch addr { + case acc0, acc1, acc2, acc3, acc4, acc5, acc6, acc7, acc8, acc9: + return true + } + + return false +} + +// +// Private keys. +// + +func DevPrivateKey0() *ecdsa.PrivateKey { + return pk0 +} + +func DevPrivateKey1() *ecdsa.PrivateKey { + return pk1 +} + +func DevPrivateKey2() *ecdsa.PrivateKey { + return pk2 +} + +func DevPrivateKey3() *ecdsa.PrivateKey { + return pk3 +} + +func DevPrivateKey4() *ecdsa.PrivateKey { + return pk4 +} + +func DevPrivateKey5() *ecdsa.PrivateKey { + return pk5 +} + +func DevPrivateKey6() *ecdsa.PrivateKey { + return pk6 +} + +func DevPrivateKey7() *ecdsa.PrivateKey { + return pk7 +} + +func DevPrivateKey8() *ecdsa.PrivateKey { + return pk8 +} + +func DevPrivateKey9() *ecdsa.PrivateKey { + return pk9 +} + +func DevPrivateKeys() []*ecdsa.PrivateKey { + return []*ecdsa.PrivateKey{pk0, pk1, pk2, pk3, pk4, pk5, pk6, pk7, pk8, pk9} +} + +// +// Utils. +// + +func addr(hex string) common.Address { + return common.HexToAddress(hex) +} + +func mustHexToKey(privKeyHex string) *ecdsa.PrivateKey { + privKey, err := crypto.HexToECDSA(strings.TrimPrefix(privKeyHex, "0x")) + if err != nil { + panic(err) + } + + return privKey +} diff --git a/lib/anvil/anvil.go b/lib/anvil/anvil.go new file mode 100644 index 00000000..610672fc --- /dev/null +++ b/lib/anvil/anvil.go @@ -0,0 +1,179 @@ +package anvil + +import ( + "bytes" + "context" + "net" + "os" + "os/exec" + "path/filepath" + "text/template" + "time" + + "github.com/piplabs/story/e2e/app/static" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/log" + + _ "embed" +) + +// Start starts a genesis anvil node and returns an ethclient and a stop function or an error. +// The dir parameter is the location of the docker compose. +// If useLogProxy is true, all requests are routed via a reserve proxy that logs all requests, which will be printed +// at stop. +func Start(ctx context.Context, dir string, chainID uint64) (ethclient.Client, string, func(), error) { + ctx, cancel := context.WithTimeout(ctx, time.Minute) // Allow 1 minute for edge case of pulling images. + defer cancel() + if !composeDown(ctx, dir) { + return nil, "", nil, errors.New("failure to clean up previous anvil instance") + } + + // Ensure ports are available + port, err := getAvailablePort() + if err != nil { + return nil, "", nil, errors.Wrap(err, "get available port") + } + + if err := writeComposeFile(dir, chainID, port); err != nil { + return nil, "", nil, errors.Wrap(err, "write compose file") + } + + if err := writeAnvilState(dir); err != nil { + return nil, "", nil, errors.Wrap(err, "write anvil state") + } + + log.Info(ctx, "Starting anvil") + + out, err := execCmd(ctx, dir, "docker", "compose", "up", "-d", "--remove-orphans") + if err != nil { + return nil, "", nil, errors.Wrap(err, "docker compose up: "+out) + } + + endpoint := "http://localhost:" + port + + ethCl, err := ethclient.Dial("anvil", endpoint) + if err != nil { + return nil, "", nil, errors.Wrap(err, "new eth client") + } + + //nolint:contextcheck // Fresh stop context since above context might be canceled. + stop := func() { + stopCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + composeDown(stopCtx, dir) + } + + // Wait up to 10 secs for RPC to be available + const retry = 10 + for i := range retry { + if i == retry-1 { + stop() + return nil, "", nil, errors.New("wait for RPC timed out") + } + + select { + case <-ctx.Done(): + return nil, "", nil, errors.Wrap(ctx.Err(), "timeout") + case <-time.After(time.Millisecond * 500): + } + + _, err := ethCl.BlockNumber(ctx) + if err == nil { + break + } + + log.Warn(ctx, "Anvil: waiting for RPC to be available", err) + } + + log.Info(ctx, "Anvil: RPC is available", "addr", endpoint) + + return ethCl, endpoint, stop, nil +} + +// composeDown runs docker-compose down in the provided directory. +func composeDown(ctx context.Context, dir string) bool { + if _, err := os.Stat(dir + "/compose.yaml"); os.IsNotExist(err) { + return true + } + + out, err := execCmd(ctx, dir, "docker", "compose", "down") + if err != nil { + log.Error(ctx, "Error: docker compose down", err, "out", out) + return false + } + + log.Debug(ctx, "Anvil: docker compose down: ok") + + return true +} + +func execCmd(ctx context.Context, dir string, cmd string, args ...string) (string, error) { + c := exec.CommandContext(ctx, cmd, args...) + c.Dir = dir + + out, err := c.CombinedOutput() + if err != nil { + return string(out), errors.Wrap(err, "exec", "out", string(out)) + } + + return string(out), nil +} + +func writeAnvilState(dir string) error { + anvilStateFile := filepath.Join(dir, "state.json") + if err := os.WriteFile(anvilStateFile, static.GetDevnetAnvilState(), 0o644); err != nil { + return errors.Wrap(err, "write anvil state") + } + + return nil +} + +//go:embed compose.yaml.tmpl +var composeTpl []byte + +func writeComposeFile(dir string, chainID uint64, port string) error { + tpl, err := template.New("").Parse(string(composeTpl)) + if err != nil { + return errors.Wrap(err, "parse compose template") + } + + var buf bytes.Buffer + err = tpl.Execute(&buf, struct { + ChainID uint64 + Port string + }{ + ChainID: chainID, + Port: port, + }) + if err != nil { + return errors.Wrap(err, "execute compose template") + } + + err = os.WriteFile(dir+"/compose.yaml", buf.Bytes(), 0644) + if err != nil { + return errors.Wrap(err, "write compose file") + } + + return nil +} + +func getAvailablePort() (string, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return "", errors.Wrap(err, "resolve addr") + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return "", errors.Wrap(err, "listen") + } + defer l.Close() + + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return "", errors.Wrap(err, "split host port") + } + + return port, nil +} diff --git a/lib/anvil/compose.yaml.tmpl b/lib/anvil/compose.yaml.tmpl new file mode 100644 index 00000000..7ac08ae7 --- /dev/null +++ b/lib/anvil/compose.yaml.tmpl @@ -0,0 +1,17 @@ +version: '2.4' + +services: + anvil: + labels: + anvil: true + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: + - anvil + - --host=0.0.0.0 + - --chain-id={{ .ChainID }} + - --silent + - --load-state=/anvil/state.json + ports: + - {{ .Port }}:8545 + volumes: + - ./state.json:/anvil/state.json diff --git a/lib/anvil/utils.go b/lib/anvil/utils.go new file mode 100644 index 00000000..e7f16411 --- /dev/null +++ b/lib/anvil/utils.go @@ -0,0 +1,31 @@ +package anvil + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/piplabs/story/lib/errors" +) + +// FundAccounts funds the anvil account via the anvil_setBalance RPC method. +func FundAccounts(ctx context.Context, rpc string, amount *big.Int, accounts ...common.Address) error { + client, err := ethclient.Dial(rpc) + if err != nil { + return err + } + defer client.Close() + + for _, account := range accounts { + result := make(map[string]any) + err = client.Client().CallContext(ctx, &result, "anvil_setBalance", account, hexutil.EncodeBig(amount)) + if err != nil { + return errors.Wrap(err, "set balance", "account", account) + } + } + + return nil +} diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go new file mode 100644 index 00000000..aa8aafb9 --- /dev/null +++ b/lib/buildinfo/buildinfo.go @@ -0,0 +1,101 @@ +package buildinfo + +import ( + "context" + "fmt" + "runtime/debug" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/piplabs/story/lib/log" +) + +const ( + VersionMajor = 0 // Major version component of the current release + VersionMinor = 9 // Minor version component of the current release + VersionPatch = 1 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string +) + +// Version returns the version of the whole iliad-monorepo and all binaries built from this git commit. +func Version() string { + return fmt.Sprintf("v%d.%d.%d", VersionMajor, VersionMinor, VersionPatch) +} + +// VersionWithMeta holds the textual version string including the metadata. +func VersionWithMeta() string { + v := Version() + if VersionMeta != "" { + v += "-" + VersionMeta + } + + return v +} + +// Instrument logs the version, git commit hash, and timestamp from the runtime build info. +// It also sets metrics. +func Instrument(ctx context.Context) { + commit, timestamp := get() + + version := Version() + versionWithMeta := VersionWithMeta() + + log.Info(ctx, "Version info", + "version", versionWithMeta, + "git_commit", commit, + "git_timestamp", timestamp, + ) + + versionGauge.WithLabelValues(version).Set(1) + commitGauge.WithLabelValues(commit).Set(1) + + ts, _ := time.Parse(time.RFC3339, timestamp) + timestampGauge.Set(float64(ts.Unix())) +} + +func NewVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version information of this binary", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + commit, timestamp := get() + + var sb strings.Builder + _, _ = sb.WriteString("Version " + VersionWithMeta()) + _, _ = sb.WriteString("\n") + _, _ = sb.WriteString("Git Commit " + commit) + _, _ = sb.WriteString("\n") + _, _ = sb.WriteString("Git Timestamp " + timestamp) + _, _ = sb.WriteString("\n") + + cmd.Print(sb.String()) + }, + } +} + +// get returns the git commit hash and timestamp from the runtime build info. +func get() (hash string, timestamp string) { //nolint:nonamedreturns // Disambiguate identical return types. + hash, timestamp = "unknown", "unknown" + hashLen := 7 + + info, ok := debug.ReadBuildInfo() + if !ok { + return hash, timestamp + } + + for _, s := range info.Settings { + if s.Key == "vcs.revision" { + if len(s.Value) < hashLen { + hashLen = len(s.Value) + } + hash = s.Value[:hashLen] + } else if s.Key == "vcs.time" { + timestamp = s.Value + } + } + + return hash, timestamp +} diff --git a/lib/buildinfo/metrics.go b/lib/buildinfo/metrics.go new file mode 100644 index 00000000..733e5149 --- /dev/null +++ b/lib/buildinfo/metrics.go @@ -0,0 +1,29 @@ +package buildinfo + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + commitGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "lib", + Subsystem: "buildinfo", + Name: "git_commit", + Help: "Constant gauge with label 'commit' set to the build info git commit hash.", + }, []string{"commit"}) + + timestampGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "lib", + Subsystem: "buildinfo", + Name: "git_timestamp_unix", + Help: "Build info git commit timestamp in unix seconds.", + }) + + versionGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "lib", + Subsystem: "buildinfo", + Name: "version", + Help: "Constant gauge with label 'version' set to the build info iliad version", + }, []string{"version"}) +) diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go new file mode 100644 index 00000000..e0ed75e9 --- /dev/null +++ b/lib/cmd/cmd.go @@ -0,0 +1,157 @@ +// Package cmd provides a common utilities and helper function to standarise +// the way iliad apps use cobra and viper to produce consistent cli experience +// for both users and devs. +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +// Main is the main entry point for the command line tool. +// Usage: +// +// func main() { +// libcmd.Main(appcmd.New()) +// } +func Main(cmd *cobra.Command) { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + + SilenceErrUsage(cmd) + + err := cmd.ExecuteContext(ctx) + + cancel() + + if err != nil { + log.Error(ctx, "!! Fatal error occurred, app diedļø unexpectedly !!", err) + + const errExitCode = 1 + os.Exit(errExitCode) //nolint:revive // Deep exit is exactly the point of this helper function. + } +} + +// NewRootCmd returns a new root cobra command that handles our command line tool. +// It sets up the general viper config and binds the cobra flags to the viper flags. +// It also silences the usage printing when commands error during "running". +func NewRootCmd(appName string, appDescription string, subCmds ...*cobra.Command) *cobra.Command { + root := &cobra.Command{ + Use: appName, + Short: appDescription, + Args: cobra.NoArgs, + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + return initializeConfig(appName, cmd) + }, + } + + root.AddCommand(subCmds...) + + return root +} + +// SilenceErrUsage silences the usage and error printing. +func SilenceErrUsage(cmd *cobra.Command) { + cmd.SilenceUsage = true + cmd.SilenceErrors = true + for _, cmd := range cmd.Commands() { + SilenceErrUsage(cmd) + } +} + +// initializeConfig sets up the general viper config and binds the cobra flags to the viper flags. +func initializeConfig(appName string, cmd *cobra.Command) error { + v := viper.New() + + v.SetConfigName(appName) + + // Set config path to /config/ if --home flag is used in this app. + if home := cmd.Flag(homeFlag); home != nil { + v.AddConfigPath(filepath.Join(home.Value.String(), "config")) + } else { + // Otherwise, set config path to current directory + v.AddConfigPath(".") + } + + // Attempt to read the config file, gracefully ignoring errors + // caused by a config file not being found. Return an error + // if we cannot parse the config file. + if err := v.ReadInConfig(); err != nil { + // It's okay if there isn't a config file + var cfgError viper.ConfigFileNotFoundError + if ok := errors.As(err, &cfgError); !ok { + return errors.Wrap(err, "read config") + } + } + + v.SetEnvPrefix(appName) + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + // Bind the current command's flags to viper + return bindFlags(cmd, v) +} + +// bindFlags binds each cobra flag to its associated viper configuration (config file and environment variable). +func bindFlags(cmd *cobra.Command, v *viper.Viper) error { + var lastErr error + + cmd.Flags().VisitAll(func(f *pflag.Flag) { + // Cobra provided flags take priority + if f.Changed { + return + } + + // Define all the viper flag names to check + viperNames := []string{ + f.Name, + strings.Replace(f.Name, "-", ".", 1), // Support 1 tier of TOML groups using first term before "-". + } + + for _, name := range viperNames { + if !v.IsSet(name) { + continue + } + + val := v.Get(name) + + // Special case handling of map[string]string flags. + if f.Value.Type() == "stringToString" { + strMap := v.GetStringMapString(name) + if len(strMap) == 0 { + // There is no way to set an empty value for Cobra's map[string]string flags. + // It must either not be set or be non-empty. + // So skip empty viper maps (as if not set) assuming the default value is empty. + continue + } + + var kvs []string + for k, v := range strMap { + kvs = append(kvs, fmt.Sprintf("%s=%s", k, v)) + } + + val = strings.Join(kvs, ",") + } + + err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) + if err != nil { + lastErr = err + } + + break + } + }) + + return lastErr +} diff --git a/lib/cmd/flags.go b/lib/cmd/flags.go new file mode 100644 index 00000000..39d3230b --- /dev/null +++ b/lib/cmd/flags.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "context" + "net/url" + "strings" + + "github.com/spf13/pflag" + + "github.com/piplabs/story/lib/log" +) + +const homeFlag = "home" + +// BindHomeFlag binds the home flag to the given flag set. +// This is generally only required for apps that require multiple config files or persist data to disk. +// Using this flag will result in the viper config directory to be updated from default "." to "/config". +func BindHomeFlag(flags *pflag.FlagSet, homeDir *string) { + flags.StringVar(homeDir, homeFlag, *homeDir, "The application home directory containing config and data") +} + +// LogFlags logs the configured flags kv pairs. +func LogFlags(ctx context.Context, flags *pflag.FlagSet) error { + skip := map[string]bool{ + "help": true, + } + // Flatten config into key/value pairs for logging. + var fields []any + flags.VisitAll(func(f *pflag.Flag) { + if skip[f.Name] { + return + } + + if mapVal, err := flags.GetStringToString(f.Name); err == nil { // First check if it is a map flag + // Redact each map value + for k, v := range mapVal { + mapVal[k] = redact(f.Name, v) + } + fields = append(fields, f.Name) + fields = append(fields, mapVal) + } else if arrayVal, err := flags.GetStringSlice(f.Name); err == nil { // Then check if it is a slice flag + // Redact each slice element + var vals []string + for _, v := range arrayVal { + vals = append(vals, redact(f.Name, v)) + } + fields = append(fields, f.Name) + fields = append(fields, vals) + } else { + fields = append(fields, f.Name) + fields = append(fields, redact(f.Name, f.Value.String())) + } + }) + + log.Info(ctx, "Parsed config from flags", fields...) + + return nil +} + +// redact returns a redacted version of the given flag value. It currently supports redacting +// passwords in valid URLs as well as flags that contains words like "token", "password", "secret", "db" or "key". +func redact(flag, val string) string { + // Don't redact empty flags ; i.e. show that they are empty. + if val == "" { + return "" + } + + u, err := url.Parse(val) + if err == nil { + return u.Redacted() + } + + // Don't redact --.*path or --.*file flags. + if strings.Contains(flag, "file") || + strings.Contains(flag, "path") { + return val + } + + if strings.Contains(flag, "token") || + strings.Contains(flag, "password") || + strings.Contains(flag, "secret") || + strings.Contains(flag, "db") || + strings.Contains(flag, "header") || + strings.Contains(flag, "key") { + return "xxxxx" + } + + return val +} diff --git a/lib/contracts/address.go b/lib/contracts/address.go new file mode 100644 index 00000000..ad815701 --- /dev/null +++ b/lib/contracts/address.go @@ -0,0 +1,70 @@ +package contracts + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/e2e/app/eoa" + "github.com/piplabs/story/lib/create3" + "github.com/piplabs/story/lib/netconf" +) + +// +// Create3Factory. +// + +func MainnetCreate3Factory() common.Address { + return crypto.CreateAddress(eoa.MustAddress(netconf.Mainnet, eoa.RoleCreate3Deployer), 0) +} + +func TestnetCreate3Factory() common.Address { + return crypto.CreateAddress(eoa.MustAddress(netconf.Testnet, eoa.RoleCreate3Deployer), 0) +} + +func StagingCreate3Factory() common.Address { + return crypto.CreateAddress(eoa.MustAddress(netconf.Staging, eoa.RoleCreate3Deployer), 0) +} + +func DevnetCreate3Factory() common.Address { + return crypto.CreateAddress(eoa.MustAddress(netconf.Devnet, eoa.RoleCreate3Deployer), 0) +} + +// +// IPTokenStaking. +// + +func MainnetIPTokenStaking() common.Address { + return create3.Address(MainnetCreate3Factory(), IPTokenStakingSalt(netconf.Mainnet), eoa.MustAddress(netconf.Mainnet, eoa.RoleDeployer)) +} + +func TestnetIPTokenStaking() common.Address { + return create3.Address(TestnetCreate3Factory(), IPTokenStakingSalt(netconf.Testnet), eoa.MustAddress(netconf.Testnet, eoa.RoleDeployer)) +} + +func StagingIPTokenStaking() common.Address { + return create3.Address(StagingCreate3Factory(), IPTokenStakingSalt(netconf.Staging), eoa.MustAddress(netconf.Staging, eoa.RoleDeployer)) +} + +func DevnetIPTokenStaking() common.Address { + return create3.Address(DevnetCreate3Factory(), IPTokenStakingSalt(netconf.Devnet), eoa.MustAddress(netconf.Devnet, eoa.RoleDeployer)) +} + +// +// Salts. +// + +func IPTokenStakingSalt(network netconf.ID) string { + // only portal salts are versioned + return salt(network, "iptokenstaking-"+network.Version()) +} + +// +// Utils. +// + +// salt generates a salt for a contract deployment. For ephemeral networks, +// the salt includes a random per-run suffix. For persistent networks, the +// sale is static. +func salt(network netconf.ID, contract string) string { + return string(network) + "-" + contract +} diff --git a/lib/contracts/create3/deploy.go b/lib/contracts/create3/deploy.go new file mode 100644 index 00000000..35ef5802 --- /dev/null +++ b/lib/contracts/create3/deploy.go @@ -0,0 +1,174 @@ +package create3 + +import ( + "context" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/e2e/app/eoa" + "github.com/piplabs/story/lib/contracts" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient/ethbackend" + "github.com/piplabs/story/lib/netconf" +) + +type DeploymentConfig struct { + Deployer common.Address +} + +func (cfg DeploymentConfig) Validate() error { + if (cfg.Deployer == common.Address{}) { + return errors.New("deployer is zero") + } + + return nil +} + +func getDeployCfg(chainID uint64, network netconf.ID) (DeploymentConfig, error) { + if network == netconf.Devnet { + return devnetCfg(), nil + } + + if network == netconf.Mainnet { + return mainnetCfg(), nil + } + + if network == netconf.Testnet { + return testnetCfg(), nil + } + + if network == netconf.Staging { + return stagingCfg(), nil + } + + return DeploymentConfig{}, errors.New("unsupported chain for network", "chain_id", chainID, "network", network) +} + +func mainnetCfg() DeploymentConfig { + return DeploymentConfig{ + Deployer: eoa.MustAddress(netconf.Mainnet, eoa.RoleCreate3Deployer), + } +} + +func testnetCfg() DeploymentConfig { + return DeploymentConfig{ + Deployer: eoa.MustAddress(netconf.Testnet, eoa.RoleCreate3Deployer), + } +} + +func stagingCfg() DeploymentConfig { + return DeploymentConfig{ + Deployer: eoa.MustAddress(netconf.Staging, eoa.RoleCreate3Deployer), + } +} + +func devnetCfg() DeploymentConfig { + return DeploymentConfig{ + Deployer: eoa.MustAddress(netconf.Devnet, eoa.RoleCreate3Deployer), + } +} + +func AddrForNetwork(network netconf.ID) (common.Address, bool) { + switch network { + case netconf.Mainnet: + return contracts.MainnetCreate3Factory(), true + case netconf.Testnet: + return contracts.TestnetCreate3Factory(), true + case netconf.Staging: + return contracts.StagingCreate3Factory(), true + case netconf.Devnet: + return contracts.DevnetCreate3Factory(), true + default: + return common.Address{}, false + } +} + +// IsDeployed checks if the Create3 factory contract is deployed to the provided backend +// to its expected network address. +func IsDeployed(ctx context.Context, network netconf.ID, backend *ethbackend.Backend) (bool, common.Address, error) { + addr, ok := AddrForNetwork(network) + if !ok { + return false, addr, errors.New("unsupported network", "network", network) + } + + code, err := backend.CodeAt(ctx, addr, nil) + if err != nil { + return false, addr, errors.Wrap(err, "code at", "address", addr) + } + + if len(code) == 0 { + return false, addr, nil + } + + if hexutil.Encode(code) != bindings.Create3DeployedBytecode { + chain, chainID := backend.Chain() + return false, addr, errors.New("unexpected code at factory", "address", addr, "chain", chain, "chain_id", chainID) + } + + return true, addr, nil +} + +// DeployIfNeeded deploys a new Create3 factory contract if it is not already deployed. +func DeployIfNeeded(ctx context.Context, network netconf.ID, backend *ethbackend.Backend) (common.Address, *ethtypes.Receipt, error) { + deployed, addr, err := IsDeployed(ctx, network, backend) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "is deployed") + } + if deployed { + return addr, nil, nil + } + + return Deploy(ctx, network, backend) +} + +// Deploy deploys a new Create3 factory contract and returns the address and receipt. +// It only allows deployments to explicitly supported chains. +func Deploy(ctx context.Context, network netconf.ID, backend *ethbackend.Backend) (common.Address, *ethtypes.Receipt, error) { + chainID, err := backend.ChainID(ctx) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "chain id") + } + + cfg, err := getDeployCfg(chainID.Uint64(), network) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "get deployment config") + } + + return deploy(ctx, cfg, backend) +} + +func deploy(ctx context.Context, cfg DeploymentConfig, backend *ethbackend.Backend) (common.Address, *ethtypes.Receipt, error) { + if err := cfg.Validate(); err != nil { + return common.Address{}, nil, errors.Wrap(err, "validate config") + } + + txOpts, err := backend.BindOpts(ctx, cfg.Deployer) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "bind opts") + } + + nonce, err := backend.PendingNonceAt(ctx, cfg.Deployer) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "pending nonce") + } else if nonce != 0 { + return common.Address{}, nil, errors.New("nonce not zero") + } + + addr, tx, _, err := bindings.DeployCreate3(txOpts, backend) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "deploy create3") + } + + receipt, err := bind.WaitMined(ctx, backend, tx) + if err != nil { + return common.Address{}, nil, errors.Wrap(err, "wait mined") + } else if receipt.Status != ethtypes.ReceiptStatusSuccessful { + return common.Address{}, nil, errors.New("receipt status failed") + } + + return addr, receipt, nil +} diff --git a/lib/contracts/utils.go b/lib/contracts/utils.go new file mode 100644 index 00000000..7132083f --- /dev/null +++ b/lib/contracts/utils.go @@ -0,0 +1,23 @@ +package contracts + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/piplabs/story/lib/errors" +) + +// PackInitCode packs the init code for a contract deployment. +func PackInitCode(abi *abi.ABI, bytecodeHex string, params ...any) ([]byte, error) { + input, err := abi.Pack("", params...) + if err != nil { + return nil, errors.Wrap(err, "pack init code") + } + + bytecode, err := hexutil.Decode(bytecodeHex) + if err != nil { + return nil, errors.Wrap(err, "decode bytecode") + } + + return append(bytecode, input...), nil +} diff --git a/lib/create3/create3.go b/lib/create3/create3.go new file mode 100644 index 00000000..dfbb2091 --- /dev/null +++ b/lib/create3/create3.go @@ -0,0 +1,42 @@ +package create3 + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +// HashSalt returns the [32]byte hash of the salt string. +func HashSalt(s string) [32]byte { + var h [32]byte + copy(h[:], crypto.Keccak256([]byte(s))) + + return h +} + +// Iliad's Create3 factory namespaces salts by deployer. +func namespacedSalt(deployer common.Address, salt string) [32]byte { + var h [32]byte + copy(h[:], crypto.Keccak256(deployer.Bytes(), crypto.Keccak256([]byte(salt)))) + + return h +} + +//nolint:gochecknoglobals // static hash +var ( + // proxyBytecodeHash is the hash bytecode of the intermediate proxy contract the Create3 factory deploys. + proxyBytecodeHash = crypto.Keccak256(hexutil.MustDecode("0x67363d3d37363d34f03d5260086018f3")) +) + +// Address returns the Create3 address for the given factory, salt, and deployer. +func Address( + factory common.Address, + salt string, + deployer common.Address, +) common.Address { + // First, get the CREATE2 intermediate proxy address + proxyAddr := crypto.CreateAddress2(factory, namespacedSalt(deployer, salt), proxyBytecodeHash) + + // Return the CREATE address the proxy deploys to + return crypto.CreateAddress(proxyAddr, 1) +} diff --git a/lib/create3/create3_test.go b/lib/create3/create3_test.go new file mode 100644 index 00000000..48234895 --- /dev/null +++ b/lib/create3/create3_test.go @@ -0,0 +1,35 @@ +package create3_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/app/eoa" + "github.com/piplabs/story/lib/create3" + "github.com/piplabs/story/lib/netconf" +) + +func TestHashSalt(t *testing.T) { + t.Parallel() + + hash := create3.HashSalt("eip1967.proxy.implementation") + + require.Equal( + t, + // keccak-256 hash of "eip1967.proxy.implementation" + "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbd", + common.Bytes2Hex(hash[:]), + ) +} + +func TestAddress(t *testing.T) { + t.Parallel() + + // test case is Devent proxy admin deployment + + factory := common.HexToAddress("0x5FbDB2315678afecb367f032d93F642f64180aa3") + expected := common.HexToAddress("0xd8dc3f2817F4d87200443FBaEdE5ab8D5d742465") + require.Equal(t, expected, create3.Address(factory, "devnet-proxy-admin", eoa.MustAddress(netconf.Devnet, eoa.RoleDeployer))) +} diff --git a/lib/crypto/ecdsa/utils.go b/lib/crypto/ecdsa/utils.go new file mode 100644 index 00000000..9cb1a130 --- /dev/null +++ b/lib/crypto/ecdsa/utils.go @@ -0,0 +1,68 @@ +// ADAPTED FROM: https://github.com/Layr-Labs/eigensdk-go/blob/dev/crypto/ecdsa/utils.go +package ecdsa + +import ( + "bufio" + "crypto/ecdsa" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/crypto" + "github.com/google/uuid" + + "github.com/piplabs/story/lib/errors" +) + +// WriteKey writes the private key to the given path +// The key is encrypted using the given password +// This function will create the directory if it doesn't exist +// If there's an existing file at the given path, it will be overwritten. +func WriteKey(path string, privateKey *ecdsa.PrivateKey, password string) error { + uuid, err := uuid.NewRandom() + if err != nil { + return errors.Wrap(err, "generating UUID") + } + + // We are using https://github.com/ethereum/go-ethereum/blob/master/accounts/keystore/key.go#L41 + // to store the keys which requires us to have random UUID for encryption + key := &keystore.Key{ + Id: uuid, + Address: crypto.PubkeyToAddress(privateKey.PublicKey), + PrivateKey: privateKey, + } + + encryptedBytes, err := keystore.EncryptKey(key, password, keystore.StandardScryptN, keystore.StandardScryptP) + if err != nil { + return errors.Wrap(err, "encrypting key") + } + + return writeBytesToFile(path, encryptedBytes) +} + +func writeBytesToFile(path string, data []byte) error { + dir := filepath.Dir(path) + + // create the directory if it doesn't exist. If exists, it does nothing + if err := os.MkdirAll(dir, 0755); err != nil { + return errors.Wrap(err, "creating directories") + } + + file, err := os.Create(filepath.Clean(path)) + if err != nil { + return errors.Wrap(err, "creating file") + } + // remember to close the file + defer func() { + cerr := file.Close() + if err == nil { + err = cerr + } + }() + + fileScanner := bufio.NewScanner(file) + fileScanner.Split(bufio.ScanLines) + _, err = file.Write(data) + + return errors.Wrap(err, "writing to file") +} diff --git a/lib/errors/errors.go b/lib/errors/errors.go new file mode 100644 index 00000000..842b1ec2 --- /dev/null +++ b/lib/errors/errors.go @@ -0,0 +1,42 @@ +// Package errors provides a consistent interface for using errors. +// It also supports slog structured logging attributes; i.e. structured errors. +// It is also a drop-in replacement for the standard library errors package. +package errors + +import ( + pkgerrors "github.com/pkg/errors" +) + +// New returns an error that formats as the given text and +// contains the structured (slog) attributes. +func New(msg string, attrs ...any) error { + return structured{ + err: pkgerrors.New(msg), + attrs: attrs, + } +} + +// Wrap returns a new error wrapping the provided with additional +// structured fields. +// +//nolint:inamedparam // This function does custom wrapping and errors. +func Wrap(err error, msg string, attrs ...any) error { + if err == nil { + panic("wrap nil error") + } + + // Support error types that do their own wrapping. + if wrapper, ok := err.(interface{ Wrap(string, ...any) error }); ok { + return wrapper.Wrap(msg, attrs...) + } + + var inner structured + if As(err, &inner) { + attrs = append(attrs, inner.attrs...) // Append inner attributes + } + + return structured{ + err: pkgerrors.Wrap(err, msg), + attrs: attrs, + } +} diff --git a/lib/errors/errors_test.go b/lib/errors/errors_test.go new file mode 100644 index 00000000..68411a98 --- /dev/null +++ b/lib/errors/errors_test.go @@ -0,0 +1,61 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package errors_test + +import ( + "io" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/errors" +) + +func TestComparable(t *testing.T) { + t.Parallel() + require.False(t, reflect.TypeOf(errors.New("x")).Comparable()) +} + +func TestIs(t *testing.T) { + t.Parallel() + errX := errors.New("x") + + err1 := errors.New("1", "1", "1") + err11 := errors.Wrap(err1, "w1") + err111 := errors.Wrap(err11, "w2") + + require.Equal(t, "x", errX.Error()) + require.Equal(t, "1", err1.Error()) + require.Equal(t, "w1: 1", err11.Error()) + require.Equal(t, "w2: w1: 1", err111.Error()) + + require.True(t, errors.Is(err1, err1)) + require.True(t, errors.Is(err11, err1)) + require.True(t, errors.Is(err111, err1)) + require.False(t, errors.Is(err1, err11)) + require.True(t, errors.Is(err11, err11)) + require.True(t, errors.Is(err111, err11)) + require.False(t, errors.Is(err1, err111)) + require.False(t, errors.Is(err11, err111)) + require.True(t, errors.Is(err111, err11)) + + require.False(t, errors.Is(err111, errX)) + + errIO1 := errors.Wrap(io.EOF, "w1") + errIO11 := errors.Wrap(errIO1, "w2") + + require.Equal(t, "w1: EOF", errIO1.Error()) + require.Equal(t, "w2: w1: EOF", errIO11.Error()) + + require.True(t, errors.Is(io.EOF, io.EOF)) + require.True(t, errors.Is(errIO1, io.EOF)) + require.True(t, errors.Is(errIO11, io.EOF)) + require.False(t, errors.Is(io.EOF, errIO1)) + require.True(t, errors.Is(errIO1, errIO1)) + require.True(t, errors.Is(errIO11, errIO1)) + require.False(t, errors.Is(io.EOF, errIO11)) + require.False(t, errors.Is(errIO1, errIO11)) + require.True(t, errors.Is(errIO11, errIO11)) + require.False(t, errors.Is(err111, errX)) +} diff --git a/lib/errors/go113.go b/lib/errors/go113.go new file mode 100644 index 00000000..91e460a6 --- /dev/null +++ b/lib/errors/go113.go @@ -0,0 +1,41 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package errors + +import ( + stderrors "errors" +) + +// This file was copied from github.com/pkg/errors/go113.go. It ensures this package is compatible +// with the stdlib error package. + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err, target error) bool { return stderrors.Is(err, target) } + +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error matches target if the error's concrete value is assignable to the value +// pointed to by target, or if the error has a method As(any) bool such that +// As(target) returns true. In the latter case, the As method is responsible for +// setting target. +// +// As will panic if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. As returns false if err is nil. +func As(err error, target any) bool { return stderrors.As(err, target) } + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + return stderrors.Unwrap(err) +} diff --git a/lib/errors/structured.go b/lib/errors/structured.go new file mode 100644 index 00000000..7230c670 --- /dev/null +++ b/lib/errors/structured.go @@ -0,0 +1,55 @@ +package errors + +import ( + pkgerrors "github.com/pkg/errors" +) + +// structured is the implementation of a structured error. +type structured struct { + err error + attrs []any +} + +// StackTrace implements the pkgerrors.StrackTracer interface. +func (s structured) StackTrace() pkgerrors.StackTrace { + type stackTracer interface { + StackTrace() pkgerrors.StackTrace + } + + tracer, ok := s.err.(stackTracer) + if !ok { + return nil + } + + trace := tracer.StackTrace() + + // Skip the first frame as this is always this package (can't skip it via pkgerrors API). + // Drop the last frame as that is always the runtime package. + return trace[1 : len(trace)-1] +} + +// Error returns the error message and implements the error interface. +func (s structured) Error() string { + return s.err.Error() +} + +// Attrs returns the structured slog attributes. +func (s structured) Attrs() []any { + return s.attrs +} + +// Unwrap returns the underlying error and +// provides compatibility with stdlib errors. +func (s structured) Unwrap() error { + return s.err +} + +// Is returns true if err is equaled to this structured error. +func (s structured) Is(err error) bool { + var other structured + if !pkgerrors.As(err, &other) { + return false + } + + return pkgerrors.Is(s.err, other.err) +} diff --git a/lib/ethclient/client_test.go b/lib/ethclient/client_test.go new file mode 100644 index 00000000..3756c05f --- /dev/null +++ b/lib/ethclient/client_test.go @@ -0,0 +1,217 @@ +package ethclient_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/golang-jwt/jwt/v5" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/ethclient" +) + +func TestGetPayloadV2(t *testing.T) { + t.Parallel() + fuzzer := fuzz.New().NilChance(0) + + var param1 engine.PayloadID + fuzzer.Fuzz(¶m1) + + var resp engine.ExecutionPayloadEnvelope + fuzzer.Fuzz(&resp) + + call := func(ctx context.Context, engineCl ethclient.EngineClient) (any, error) { + return engineCl.GetPayloadV2(ctx, param1) + } + + testEndpoint(t, call, resp, param1) +} + +func TestGetPayloadV3(t *testing.T) { + t.Parallel() + fuzzer := fuzz.New().NilChance(0) + + var param1 engine.PayloadID + fuzzer.Fuzz(¶m1) + + var resp engine.ExecutionPayloadEnvelope + fuzzer.Fuzz(&resp) + + call := func(ctx context.Context, engineCl ethclient.EngineClient) (any, error) { + return engineCl.GetPayloadV3(ctx, param1) + } + + testEndpoint(t, call, resp, param1) +} + +func TestNewPayloadV2(t *testing.T) { + t.Parallel() + fuzzer := fuzz.New().NilChance(0) + + var param1 engine.ExecutableData + fuzzer.Fuzz(¶m1) + + var resp engine.PayloadStatusV1 + fuzzer.Fuzz(&resp) + + call := func(ctx context.Context, engineCl ethclient.EngineClient) (any, error) { + return engineCl.NewPayloadV2(ctx, param1) + } + + testEndpoint(t, call, resp, param1) +} + +func TestNewPayloadV3(t *testing.T) { + t.Parallel() + fuzzer := fuzz.New().NilChance(0) + + var param1 engine.ExecutableData + fuzzer.Fuzz(¶m1) + + var param2 []common.Hash + fuzzer.Fuzz(¶m2) + + var param3 common.Hash + fuzzer.Fuzz(¶m3) + + var resp engine.PayloadStatusV1 + fuzzer.Fuzz(&resp) + + call := func(ctx context.Context, engineCl ethclient.EngineClient) (any, error) { + return engineCl.NewPayloadV3(ctx, param1, param2, ¶m3) + } + + testEndpoint(t, call, resp, param1, param2, param3) +} + +func TestForkchoiceUpdatedV2(t *testing.T) { + t.Parallel() + fuzzer := fuzz.New().NilChance(0) + + var param1 engine.ForkchoiceStateV1 + fuzzer.Fuzz(¶m1) + + var param2 engine.PayloadAttributes + fuzzer.Fuzz(¶m2) + + var resp engine.ForkChoiceResponse + fuzzer.Fuzz(&resp) + + call := func(ctx context.Context, engineCl ethclient.EngineClient) (any, error) { + return engineCl.ForkchoiceUpdatedV2(ctx, param1, ¶m2) + } + + testEndpoint(t, call, resp, param1, param2) +} + +func TestForkchoiceUpdatedV3(t *testing.T) { + t.Parallel() + fuzzer := fuzz.New().NilChance(0) + + var param1 engine.ForkchoiceStateV1 + fuzzer.Fuzz(¶m1) + + var param2 engine.PayloadAttributes + fuzzer.Fuzz(¶m2) + + var resp engine.ForkChoiceResponse + fuzzer.Fuzz(&resp) + + call := func(ctx context.Context, engineCl ethclient.EngineClient) (any, error) { + return engineCl.ForkchoiceUpdatedV3(ctx, param1, ¶m2) + } + + testEndpoint(t, call, resp, param1, param2) +} + +func testEndpoint(t *testing.T, callback func(context.Context, ethclient.EngineClient) (any, error), + resp any, params ...any, +) { + t.Helper() + + const jwtSecret = "secret" + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Parse JWT from Authorization Bearer header. + tokenString := r.Header.Get("Authorization") + assert.NotEmpty(t, tokenString) + tokenString = strings.TrimPrefix(tokenString, "Bearer ") + + _, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { + return []byte(jwtSecret), nil + }) + assert.NoError(t, err) + + assert.Equal(t, "/", r.URL.Path) + + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + var rpcReq jsonRPCRequest + err = json.Unmarshal(body, &rpcReq) + assert.NoError(t, err) + + for i, actualJSON := range rpcReq.Params { + expectJSON, err := json.Marshal(params[i]) + assert.NoError(t, err) + + assert.Equal(t, string(expectJSON), string(actualJSON)) + } + + rpcResp := jsonRPCResponse{ + JSONRPC: "2.0", + ID: rpcReq.ID, + Result: resp, + } + buf, err := json.Marshal(rpcResp) + assert.NoError(t, err) + + _, _ = w.Write(buf) + })) + defer srv.Close() + + ctx := context.Background() + + api, err := ethclient.NewAuthClient(ctx, srv.URL, []byte(jwtSecret)) + require.NoError(t, err) + + got, err := callback(ctx, api) + require.NoError(t, err) + + equalJSON(t, resp, got) +} + +type jsonRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id"` + Method string `json:"method"` + Params []json.RawMessage `json:"params"` +} + +type jsonRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id"` + Result any `json:"result"` +} + +// equalJSON asserts that two values are equal after marshaling to JSON. +func equalJSON(t *testing.T, a, b any) { + t.Helper() + + aa, err := json.Marshal(a) + require.NoError(t, err) + + bb, err := json.Marshal(b) + require.NoError(t, err) + + require.Equal(t, string(aa), string(bb)) +} diff --git a/lib/ethclient/engineclient.go b/lib/ethclient/engineclient.go new file mode 100644 index 00000000..8a3c003e --- /dev/null +++ b/lib/ethclient/engineclient.go @@ -0,0 +1,172 @@ +package ethclient + +import ( + "context" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/piplabs/story/lib/errors" +) + +const ( + defaultRPCHTTPTimeout = time.Second * 30 + + newPayloadV2 = "engine_newPayloadV2" + newPayloadV3 = "engine_newPayloadV3" + + forkchoiceUpdatedV2 = "engine_forkchoiceUpdatedV2" + forkchoiceUpdatedV3 = "engine_forkchoiceUpdatedV3" + + getPayloadV2 = "engine_getPayloadV2" + getPayloadV3 = "engine_getPayloadV3" +) + +// EngineClient defines the Engine API authenticated JSON-RPC endpoints. +// It extends the normal Client interface with the Engine API. +type EngineClient interface { + Client + + // NewPayloadV2 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. + NewPayloadV2(ctx context.Context, params engine.ExecutableData) (engine.PayloadStatusV1, error) + // NewPayloadV3 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. + NewPayloadV3(ctx context.Context, params engine.ExecutableData, versionedHashes []common.Hash, + beaconRoot *common.Hash) (engine.PayloadStatusV1, error) + + // ForkchoiceUpdatedV2 has several responsibilities: + // - It sets the chain the head. + // - And/or it sets the chain's finalized block hash. + // - And/or it starts assembling (async) a block with the payload attributes. + ForkchoiceUpdatedV2(ctx context.Context, update engine.ForkchoiceStateV1, + payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) + + // ForkchoiceUpdatedV3 is equivalent to V2 with the addition of parent beacon block root in the payload attributes. + ForkchoiceUpdatedV3(ctx context.Context, update engine.ForkchoiceStateV1, + payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) + + // GetPayloadV2 returns a cached payload by id. + GetPayloadV2(ctx context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) + // GetPayloadV3 returns a cached payload by id. + GetPayloadV3(ctx context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) +} + +// engineClient implements EngineClient using JSON-RPC. +type engineClient struct { + Wrapper +} + +// NewAuthClient returns a new authenticated JSON-RPc engineClient. +func NewAuthClient(ctx context.Context, urlAddr string, jwtSecret []byte) (EngineClient, error) { + transport := http.DefaultTransport + if len(jwtSecret) > 0 { + transport = newJWTRoundTripper(http.DefaultTransport, jwtSecret) + } + + client := &http.Client{Timeout: defaultRPCHTTPTimeout, Transport: transport} + + rpcClient, err := rpc.DialOptions(ctx, urlAddr, rpc.WithHTTPClient(client)) + if err != nil { + return engineClient{}, errors.Wrap(err, "rpc dial") + } + + return engineClient{ + Wrapper: NewClient(rpcClient, "engine", urlAddr), + }, nil +} + +func (c engineClient) NewPayloadV2(ctx context.Context, params engine.ExecutableData) (engine.PayloadStatusV1, error) { + const endpoint = "new_payload_v2" + defer latency(c.chain, endpoint)() + + var resp engine.PayloadStatusV1 + err := c.cl.Client().CallContext(ctx, &resp, newPayloadV2, params) + if err != nil { + incError(c.chain, endpoint) + return engine.PayloadStatusV1{}, errors.Wrap(err, "rpc new payload v2") + } + + return resp, nil +} + +func (c engineClient) NewPayloadV3(ctx context.Context, params engine.ExecutableData, versionedHashes []common.Hash, + beaconRoot *common.Hash, +) (engine.PayloadStatusV1, error) { + const endpoint = "new_payload_v3" + defer latency(c.chain, endpoint)() + + var resp engine.PayloadStatusV1 + err := c.cl.Client().CallContext(ctx, &resp, newPayloadV3, params, versionedHashes, beaconRoot) + if err != nil { + incError(c.chain, endpoint) + return engine.PayloadStatusV1{}, errors.Wrap(err, "rpc new payload v3") + } + + return resp, nil +} + +func (c engineClient) ForkchoiceUpdatedV2(ctx context.Context, update engine.ForkchoiceStateV1, + payloadAttributes *engine.PayloadAttributes, +) (engine.ForkChoiceResponse, error) { + const endpoint = "forkchoice_updated_v2" + defer latency(c.chain, endpoint)() + + var resp engine.ForkChoiceResponse + err := c.cl.Client().CallContext(ctx, &resp, forkchoiceUpdatedV2, update, payloadAttributes) + if err != nil { + incError(c.chain, endpoint) + return engine.ForkChoiceResponse{}, errors.Wrap(err, "rpc forkchoice updated v2") + } + + return resp, nil +} + +func (c engineClient) ForkchoiceUpdatedV3(ctx context.Context, update engine.ForkchoiceStateV1, + payloadAttributes *engine.PayloadAttributes, +) (engine.ForkChoiceResponse, error) { + const endpoint = "forkchoice_updated_v3" + defer latency(c.chain, endpoint)() + + var resp engine.ForkChoiceResponse + err := c.cl.Client().CallContext(ctx, &resp, forkchoiceUpdatedV3, update, payloadAttributes) + if err != nil { + incError(c.chain, endpoint) + return engine.ForkChoiceResponse{}, errors.Wrap(err, "rpc forkchoice updated v3") + } + + return resp, nil +} + +func (c engineClient) GetPayloadV2(ctx context.Context, payloadID engine.PayloadID) ( + *engine.ExecutionPayloadEnvelope, error, +) { + const endpoint = "get_payload_v2" + defer latency(c.chain, endpoint)() + + var resp engine.ExecutionPayloadEnvelope + err := c.cl.Client().CallContext(ctx, &resp, getPayloadV2, payloadID) + if err != nil { + incError(c.chain, endpoint) + return nil, errors.Wrap(err, "rpc get payload v2") + } + + return &resp, nil +} + +func (c engineClient) GetPayloadV3(ctx context.Context, payloadID engine.PayloadID) ( + *engine.ExecutionPayloadEnvelope, error, +) { + const endpoint = "get_payload_v3" + defer latency(c.chain, endpoint)() + + var resp engine.ExecutionPayloadEnvelope + err := c.cl.Client().CallContext(ctx, &resp, getPayloadV3, payloadID) + if err != nil { + incError(c.chain, endpoint) + return nil, errors.Wrap(err, "rpc get payload v3") + } + + return &resp, nil +} diff --git a/lib/ethclient/enginemock.go b/lib/ethclient/enginemock.go new file mode 100644 index 00000000..e3f26336 --- /dev/null +++ b/lib/ethclient/enginemock.go @@ -0,0 +1,471 @@ +package ethclient + +import ( + "context" + "crypto/sha256" + "math/big" + "math/rand" + "sync" + "testing" + "time" + + "github.com/cometbft/cometbft/crypto" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" + fuzz "github.com/google/gofuzz" + + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" +) + +type payloadArgs struct { + params engine.ExecutableData + beaconRoot *common.Hash +} + +//nolint:gochecknoglobals // This is a static mapping. +var depositEvent = mustGetABI(bindings.IPTokenStakingMetaData).Events["Deposit"] + +var _ EngineClient = (*engineMock)(nil) + +// engineMock mocks the Engine API for testing purposes. +type engineMock struct { + Client + fuzzer *fuzz.Fuzzer + randomErrs float64 + + mu sync.Mutex + head *types.Block + pendingLogs map[common.Address][]types.Log + logs map[common.Hash][]types.Log + payloads map[engine.PayloadID]payloadArgs +} + +// WithMockSelfDelegate returns an option to add a deposit event to the mock. +func WithMockSelfDelegation(pubkey crypto.PubKey, ether int64) func(*engineMock) { + return func(mock *engineMock) { + mock.mu.Lock() + defer mock.mu.Unlock() + + wei := new(big.Int).Mul(big.NewInt(ether), big.NewInt(params.Ether)) + + valAddr, err := k1util.PubKeyToAddress(pubkey) + if err != nil { + panic(errors.Wrap(err, "pubkey to address")) + } + + data, err := depositEvent.Inputs.NonIndexed().Pack(wei) + if err != nil { + panic(errors.Wrap(err, "pack delegate")) + } + + // Staking predeploy addr, copied here to avoid import cycle. + contractAddr := common.HexToAddress("0xcccccc0000000000000000000000000000000001") + eventLog := types.Log{ + Address: contractAddr, + Topics: []common.Hash{ + depositEvent.ID, + common.HexToHash(valAddr.Hex()), // delegator + common.HexToHash(valAddr.Hex()), // validator + }, + Data: data, + } + + mock.pendingLogs[contractAddr] = []types.Log{eventLog} + } +} + +type randomErrKey struct{} + +// WithRandomErr returns a context that results in random engineMock errors. +// This must only be used for testing. +func WithRandomErr(ctx context.Context, _ *testing.T) context.Context { + return context.WithValue(ctx, randomErrKey{}, true) +} + +func hasRandomErr(ctx context.Context) bool { + v, ok := ctx.Value(randomErrKey{}).(bool) + return ok && v +} + +// MockGenesisBlock returns a deterministic genesis block for testing. +func MockGenesisBlock() (*types.Block, error) { + // Deterministic genesis block + var ( + // Deterministic genesis block + height uint64 // 0 + parentHash common.Hash + parentBeaconRoot common.Hash + timestamp = time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).Unix() + fuzzer = NewFuzzer(timestamp) + ) + + genesisPayload, err := makePayload(fuzzer, height, uint64(timestamp), parentHash, common.Address{}, parentHash, &parentBeaconRoot) + if err != nil { + return nil, errors.Wrap(err, "make next payload") + } + genesisBlock, err := engine.ExecutableDataToBlock(genesisPayload, nil, &parentBeaconRoot) + if err != nil { + return nil, errors.Wrap(err, "executable data to block") + } + + return genesisBlock, nil +} + +// NewEngineMock returns a new mock engine API client. +// Note only some methods are implemented, it will panic if you call an unimplemented method. +func NewEngineMock(opts ...func(mock *engineMock)) (EngineClient, error) { + genesisBlock, err := MockGenesisBlock() + if err != nil { + return nil, err + } + + m := &engineMock{ + fuzzer: NewFuzzer(int64(genesisBlock.Time())), + head: genesisBlock, + pendingLogs: make(map[common.Address][]types.Log), + payloads: make(map[engine.PayloadID]payloadArgs), + logs: make(map[common.Hash][]types.Log), + } + for _, opt := range opts { + opt(m) + } + + return m, nil +} + +func (m *engineMock) maybeErr(ctx context.Context) error { + if !hasRandomErr(ctx) { + return nil + } + //nolint:gosec // Test code is fine. + if rand.Float64() < m.randomErrs { + return errors.New("test error") + } + + return nil +} + +func (m *engineMock) FilterLogs(_ context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if q.BlockHash == nil || len(q.Addresses) == 0 { + return nil, nil + } + + addr := q.Addresses[0] + + // Ensure we return the same logs for the same query. + if eventLogs, ok := m.logs[*q.BlockHash]; ok { + var resp []types.Log + for _, eventLog := range eventLogs { + if eventLog.Address == addr { + resp = append(resp, eventLog) + } + } + + return resp, nil + } + + eventLogs, ok := m.pendingLogs[addr] + if !ok { + return nil, nil + } + + m.logs[*q.BlockHash] = eventLogs + delete(m.pendingLogs, addr) + + return eventLogs, nil +} + +func (m *engineMock) BlockNumber(ctx context.Context) (uint64, error) { + if err := m.maybeErr(ctx); err != nil { + return 0, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + return m.head.NumberU64(), nil +} + +func (m *engineMock) HeaderByNumber(ctx context.Context, height *big.Int) (*types.Header, error) { + b, err := m.BlockByNumber(ctx, height) + if err != nil { + return nil, err + } + + return b.Header(), nil +} + +func (m *engineMock) HeaderByType(ctx context.Context, typ HeadType) (*types.Header, error) { + if typ != HeadLatest { + return nil, errors.New("only support latest block") + } + + number, err := m.BlockNumber(ctx) + if err != nil { + return nil, err + } + + return m.HeaderByNumber(ctx, big.NewInt(int64(number))) +} + +func (m *engineMock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + if err := m.maybeErr(ctx); err != nil { + return nil, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + if hash != m.head.Hash() { + return nil, errors.New("only head hash supported") // Only support latest block + } + + return m.head.Header(), nil +} + +func (m *engineMock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + if err := m.maybeErr(ctx); err != nil { + return nil, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + if number == nil { + return m.head, nil + } + + if number.Cmp(m.head.Number()) != 0 { + return nil, errors.New("block not found") // Only support latest block + } + + return m.head, nil +} + +func (m *engineMock) NewPayloadV3(ctx context.Context, params engine.ExecutableData, _ []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { + if err := m.maybeErr(ctx); err != nil { + return engine.PayloadStatusV1{}, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + args := payloadArgs{ + params: params, + beaconRoot: beaconRoot, + } + + id, err := MockPayloadID(args.params, args.beaconRoot) + if err != nil { + return engine.PayloadStatusV1{}, err + } + + m.payloads[id] = args + + log.Debug(ctx, "Engine mock received new payload from proposer", + "height", params.Number, + log.Hex7("hash", params.BlockHash.Bytes()), + ) + + return engine.PayloadStatusV1{ + Status: engine.VALID, + }, nil +} + +func (m *engineMock) ForkchoiceUpdatedV3(ctx context.Context, update engine.ForkchoiceStateV1, + attrs *engine.PayloadAttributes, +) (engine.ForkChoiceResponse, error) { + if err := m.maybeErr(ctx); err != nil { + return engine.ForkChoiceResponse{}, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + resp := engine.ForkChoiceResponse{ + PayloadStatus: engine.PayloadStatusV1{ + Status: engine.VALID, + }, + } + + // Maybe update head + //nolint: nestif // this is a mock it's fine + if m.head.Hash() != update.HeadBlockHash { + var found bool + for _, args := range m.payloads { + block, err := engine.ExecutableDataToBlock(args.params, nil, args.beaconRoot) + if err != nil { + return engine.ForkChoiceResponse{}, errors.Wrap(err, "executable data to block") + } + + if block.Hash() != update.HeadBlockHash { + continue + } + + if err := verifyChild(m.head, block); err != nil { + return engine.ForkChoiceResponse{}, err + } + + m.head = block + found = true + + id, err := MockPayloadID(args.params, args.beaconRoot) + if err != nil { + return engine.ForkChoiceResponse{}, err + } + resp.PayloadID = &id + + break + } + if !found { + return engine.ForkChoiceResponse{}, errors.New("forkchoice block not found", + log.Hex7("forkchoice", m.head.Hash().Bytes())) + } + } + + // If we have payload attributes, make a new payload + if attrs != nil { + payload, err := makePayload(m.fuzzer, m.head.NumberU64()+1, + attrs.Timestamp, update.HeadBlockHash, attrs.SuggestedFeeRecipient, attrs.Random, attrs.BeaconRoot) + if err != nil { + return engine.ForkChoiceResponse{}, err + } + + args := payloadArgs{params: payload, beaconRoot: attrs.BeaconRoot} + + id, err := MockPayloadID(args.params, args.beaconRoot) + if err != nil { + return engine.ForkChoiceResponse{}, err + } + + m.payloads[id] = args + + resp.PayloadID = &id + } + + log.Debug(ctx, "Engine mock forkchoice updated", + "height", m.head.NumberU64(), + log.Hex7("forkchoice", update.HeadBlockHash.Bytes()), + ) + + return resp, nil +} + +func (m *engineMock) GetPayloadV3(ctx context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + if err := m.maybeErr(ctx); err != nil { + return nil, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + args, ok := m.payloads[payloadID] + if !ok { + return nil, errors.New("payload not found") + } + + return &engine.ExecutionPayloadEnvelope{ + ExecutionPayload: &args.params, + }, nil +} + +// TODO(corver): Add support for V3 + +func (*engineMock) NewPayloadV2(context.Context, engine.ExecutableData) (engine.PayloadStatusV1, error) { + panic("implement me") +} + +func (*engineMock) ForkchoiceUpdatedV2(context.Context, engine.ForkchoiceStateV1, *engine.PayloadAttributes, +) (engine.ForkChoiceResponse, error) { + panic("implement me") +} + +func (*engineMock) GetPayloadV2(context.Context, engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + panic("implement me") +} + +// makePayload returns a new fuzzed payload using head as parent if provided. +func makePayload(fuzzer *fuzz.Fuzzer, height uint64, timestamp uint64, parentHash common.Hash, + feeRecipient common.Address, randao common.Hash, beaconRoot *common.Hash) (engine.ExecutableData, error) { + // Build a new header + var header types.Header + fuzzer.Fuzz(&header) + header.Number = big.NewInt(int64(height)) + header.Time = timestamp + header.ParentHash = parentHash + header.MixDigest = randao // this corresponds to Random field in PayloadAttributes + header.Coinbase = feeRecipient // this corresponds to SuggestedFeeRecipient field in PayloadAttributes + header.ParentBeaconRoot = beaconRoot + + // Convert header to block + block := types.NewBlock(&header, nil, nil, trie.NewStackTrie(nil)) + + // Convert block to payload + env := engine.BlockToExecutableData(block, big.NewInt(0), nil) + payload := *env.ExecutionPayload + + // Ensure the block is valid + _, err := engine.ExecutableDataToBlock(payload, nil, beaconRoot) + if err != nil { + return engine.ExecutableData{}, errors.Wrap(err, "executable data to block") + } + + return payload, nil +} + +// MockPayloadID returns a deterministic payload id for the given payload. +func MockPayloadID(params engine.ExecutableData, beaconRoot *common.Hash) (engine.PayloadID, error) { + bz, err := params.MarshalJSON() + if err != nil { + return engine.PayloadID{}, errors.Wrap(err, "marshal payload") + } + + h := sha256.New() + _, _ = h.Write(bz) + _, _ = h.Write(beaconRoot.Bytes()) + hash := h.Sum(nil) + + return engine.PayloadID(hash[:8]), nil +} + +// verifyChild returns an error if child is not a valid child of parent. +func verifyChild(parent *types.Block, child *types.Block) error { + if parent.NumberU64()+1 != child.NumberU64() { + return errors.New("forkchoice height not following head", + "head", parent.NumberU64(), + "forkchoice", child.NumberU64(), + ) + } + + if parent.Hash() != child.ParentHash() { + return errors.New("forkchoice parent hash not head", + log.Hex7("head", parent.Hash().Bytes()), + log.Hex7("forkchoice", child.Hash().Bytes()), + ) + } + + return nil +} + +// mustGetABI returns the metadata's ABI as an abi.ABI type. +// It panics on error. +func mustGetABI(metadata *bind.MetaData) *abi.ABI { + abi, err := metadata.GetAbi() + if err != nil { + panic(err) + } + + return abi +} diff --git a/lib/ethclient/ethbackend/backend.go b/lib/ethclient/ethbackend/backend.go new file mode 100644 index 00000000..933e5431 --- /dev/null +++ b/lib/ethclient/ethbackend/backend.go @@ -0,0 +1,311 @@ +package ethbackend + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "math/big" + "time" + + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/anvil" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/fireblocks" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/txmgr" +) + +type account struct { + from common.Address + privateKey *ecdsa.PrivateKey // Either local private key is set, + fireCl fireblocks.Client // or, Fireblocks is used + txMgr txmgr.TxManager +} + +type Backend struct { + ethclient.Client + + accounts map[common.Address]account + chainName string + chainID uint64 + blockPeriod time.Duration +} + +// NewFireBackend returns a backend that supports all accounts supported by the configured fireblocks client. +// Note that private keys can still be added via AddAccount. +func NewFireBackend(ctx context.Context, chainName string, chainID uint64, blockPeriod time.Duration, ethCl ethclient.Client, fireCl fireblocks.Client) (*Backend, error) { + accs, err := fireCl.Accounts(ctx) + if err != nil { + return nil, errors.Wrap(err, "fireblocks accounts") + } + + accounts := make(map[common.Address]account) + for addr := range accs { + txMgr, err := newFireblocksTxMgr(ethCl, chainName, chainID, blockPeriod, addr, fireCl) + if err != nil { + return nil, errors.Wrap(err, "new txmgr") + } + + accounts[addr] = account{ + from: addr, + fireCl: fireCl, + txMgr: txMgr, + } + } + + return &Backend{ + Client: ethCl, + accounts: accounts, + chainName: chainName, + chainID: chainID, + blockPeriod: blockPeriod, + }, nil +} + +// NewAnvilBackend returns a backend with all pre-funded anvil dev accounts. +func NewAnvilBackend(chainName string, chainID uint64, blockPeriod time.Duration, ethCl ethclient.Client) (*Backend, error) { + return NewBackend(chainName, chainID, blockPeriod, ethCl, anvil.DevPrivateKeys()...) +} + +// NewBackend returns a new backend backed by in-memory private keys. +func NewBackend(chainName string, chainID uint64, blockPeriod time.Duration, ethCl ethclient.Client, privateKeys ...*ecdsa.PrivateKey) (*Backend, error) { + accounts := make(map[common.Address]account) + for _, pk := range privateKeys { + txMgr, err := newTxMgr(ethCl, chainName, chainID, blockPeriod, pk) + if err != nil { + return nil, errors.Wrap(err, "new txmgr") + } + + addr := crypto.PubkeyToAddress(pk.PublicKey) + accounts[addr] = account{ + from: addr, + privateKey: pk, + txMgr: txMgr, + } + } + + return &Backend{ + Client: ethCl, + accounts: accounts, + chainName: chainName, + chainID: chainID, + blockPeriod: blockPeriod, + }, nil +} + +// AddAccount adds a in-memory private key account to the backend. +// Note this can be called even if other accounts are fireblocks based. +func (b *Backend) AddAccount(privkey *ecdsa.PrivateKey) (common.Address, error) { + txMgr, err := newTxMgr(b.Client, b.chainName, b.chainID, b.blockPeriod, privkey) + if err != nil { + return common.Address{}, errors.Wrap(err, "new txmgr") + } + + addr := crypto.PubkeyToAddress(privkey.PublicKey) + + b.accounts[addr] = account{ + from: addr, + privateKey: privkey, + txMgr: txMgr, + } + + return addr, nil +} + +func (b *Backend) Chain() (string, uint64) { + return b.chainName, b.chainID +} + +func (b *Backend) Sign(ctx context.Context, from common.Address, input [32]byte) ([65]byte, error) { + acc, ok := b.accounts[from] + if !ok { + return [65]byte{}, errors.New("unknown from address", "from", from) + } else if acc.privateKey == nil { + return acc.fireCl.Sign(ctx, input, from) + } + + pk := k1.PrivKey(crypto.FromECDSA(acc.privateKey)) + + return k1util.Sign(pk, input) +} + +func (b *Backend) PublicKey(from common.Address) (*ecdsa.PublicKey, error) { + acc, ok := b.accounts[from] + if !ok { + return nil, errors.New("unknown from address", "from", from) + } + + return &acc.privateKey.PublicKey, nil +} + +func (b *Backend) Send(ctx context.Context, from common.Address, candidate txmgr.TxCandidate) (*ethtypes.Transaction, *ethtypes.Receipt, error) { + acc, ok := b.accounts[from] + if !ok { + return nil, nil, errors.New("unknown from address", "from", from) + } + + return acc.txMgr.Send(ctx, candidate) +} + +// WaitMined waits for the transaction to be mined and asserts the receipt is successful. +func (b *Backend) WaitMined(ctx context.Context, tx *ethtypes.Transaction) (*ethtypes.Receipt, error) { + rec, err := bind.WaitMined(ctx, b, tx) + if err != nil { + return nil, errors.Wrap(err, "wait mined", "chain", b.chainName) + } else if rec.Status != ethtypes.ReceiptStatusSuccessful { + return rec, errors.New("receipt status unsuccessful", "status", rec.Status) + } + + return rec, nil +} + +// BindOpts returns a new TransactOpts for interacting with bindings based contracts for the provided account. +// The TransactOpts are partially stubbed, since txmgr handles nonces and signing. +// +// Do not cache or store the TransactOpts, as they are not safe for concurrent use (pointer). +// Rather create a new TransactOpts for each transaction. +func (b *Backend) BindOpts(ctx context.Context, from common.Address) (*bind.TransactOpts, error) { + if header, err := b.HeaderByNumber(ctx, nil); err != nil { + return nil, errors.Wrap(err, "header by number") + } else if header.BaseFee == nil { + return nil, errors.New("only dynamic transaction Backends supported") + } + + _, ok := b.accounts[from] + if !ok { + return nil, errors.New("unknown from address", "from", from) + } + + // Stub nonce and signer since txmgr will handle this. + // Bindings will estimate gas. + return &bind.TransactOpts{ + From: from, + Nonce: big.NewInt(1), + Signer: func(from common.Address, tx *ethtypes.Transaction) (*ethtypes.Transaction, error) { + resp, err := tx.WithSignature(backendStubSigner{}, from[:]) + if err != nil { + return nil, errors.Wrap(err, "with signature") + } + + return resp, nil + }, + Context: log.WithCtx(ctx, "dest_chain", b.chainName, "from_addr", from.Hex()[:10]), + }, nil +} + +// SendTransaction intercepts the tx that bindings generates, extracts the from address (assuming the +// backendStubSigner was used), the strips fields, and passes it to the txmgr for reliable broadcasting. +func (b *Backend) SendTransaction(ctx context.Context, in *ethtypes.Transaction) error { + from, err := backendStubSigner{}.Sender(in) + if err != nil { + return errors.Wrap(err, "from signer sender") + } + + acc, ok := b.accounts[from] + if !ok { + return errors.New("unknown from address", "from", from) + } + + candidate := txmgr.TxCandidate{ + TxData: in.Data(), + To: in.To(), + GasLimit: in.Gas(), + Value: in.Value(), + } + + ctx = log.WithCtx(ctx, "req_id", randomHex7(), "chain", b.chainName) + + out, resp, err := acc.txMgr.Send(ctx, candidate) + if err != nil { + return errors.Wrap(err, "txmgr send tx") + } + + // TODO: Maybe remove this as it is very noisy. + log.Debug(ctx, "Backend sent tx", + "nonce", out.Nonce(), + "gas_used", resp.GasUsed, + "status", resp.Status, + "height", resp.BlockNumber.Uint64(), + ) + + *in = *out //nolint:govet // Copy lock (caches) isn't a problem since we are overwriting the object. + + return nil +} + +func randomHex7() string { + bytes := make([]byte, 4) + _, _ = rand.Read(bytes) + hexString := hex.EncodeToString(bytes) + + // Trim the string to 7 characters + if len(hexString) > 7 { + hexString = hexString[:7] + } + + return hexString +} + +var _ ethtypes.Signer = backendStubSigner{} + +// backendStubSigner is a stub signer that is used by bindings to sign transactions for a Backend. +// It just encodes the from address into the signature, since there is no other way to pass the from +// address from TxOpts to the Backend. The Backend then extracts the from address, +// gets the account txmgr, and sends the transaction. +type backendStubSigner struct { + ethtypes.Signer +} + +func (backendStubSigner) ChainID() *big.Int { + return new(big.Int) +} + +func (backendStubSigner) Sender(tx *ethtypes.Transaction) (common.Address, error) { + // Convert R,S,V into a 20 byte signature into: + // R: 8 bytes uint64 + // S: 8 bytes uint64 + // V: 4 bytes uint32 + + v, r, s := tx.RawSignatureValues() + + addrLen := len(common.Address{}) + + if len(r.Bytes()) > addrLen { + return common.Address{}, errors.New("invalid r length", "length", len(r.Bytes())) + } + if s.Uint64() != 0 { + return common.Address{}, errors.New("non-empty s [BUG]", "length", len(s.Bytes())) + } + if v.Uint64() != 0 { + return common.Address{}, errors.New("non-empty v [BUG]", "length", len(v.Bytes())) + } + + addr := make([]byte, addrLen) + // big.Int.Bytes() truncates leading zeros, so we need to left pad the address to 20 bytes. + pad := addrLen - len(r.Bytes()) + copy(addr[pad:], r.Bytes()) + + return common.Address(addr), nil +} + +//nolint:nonamedreturns // Ambiguous return values +func (backendStubSigner) SignatureValues(_ *ethtypes.Transaction, sig []byte) (r, s, v *big.Int, err error) { + if len(sig) != len(common.Address{}) { + return nil, nil, nil, errors.New("invalid from signature length", "length", len(sig)) + } + + // Set the 20 byte signature (from address) as R + r = new(big.Int).SetBytes(sig) + s = new(big.Int) // 0 + v = new(big.Int) // 0 + + return r, s, v, nil +} diff --git a/lib/ethclient/ethbackend/backend_internal_test.go b/lib/ethclient/ethbackend/backend_internal_test.go new file mode 100644 index 00000000..d49e810e --- /dev/null +++ b/lib/ethclient/ethbackend/backend_internal_test.go @@ -0,0 +1,59 @@ +package ethbackend + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" +) + +func Test(t *testing.T) { + t.Parallel() + tests := []struct { + name string + txData ethtypes.TxData + fromHex string + }{ + { + name: "legacy tx", + txData: ðtypes.LegacyTx{}, + }, + { + name: "dynamic fee tx", + txData: ðtypes.DynamicFeeTx{}, + }, + { + name: "legacy tx with zero prefix", + txData: ðtypes.LegacyTx{}, + fromHex: "0x002985c832a67c0b31a05e909f443b641624da52", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + f := fuzz.New().NilChance(0).NumElements(1, 8) + + var from common.Address + f.Fuzz(&from) + f.Fuzz(test.txData) + + if test.fromHex != "" { + from = common.HexToAddress(test.fromHex) + } + + signer := backendStubSigner{} + + tx := ethtypes.NewTx(test.txData) + tx2, err := tx.WithSignature(signer, from.Bytes()) + require.NoError(t, err) + + from2, err := signer.Sender(tx2) + require.NoError(t, err) + + require.Equal(t, from, from2) + }) + } +} diff --git a/lib/ethclient/ethbackend/backends.go b/lib/ethclient/ethbackend/backends.go new file mode 100644 index 00000000..4d0200ca --- /dev/null +++ b/lib/ethclient/ethbackend/backends.go @@ -0,0 +1,238 @@ +package ethbackend + +import ( + "context" + "crypto/ecdsa" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/fireblocks" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/txmgr" +) + +const ( + interval = 3 +) + +// Backends is a wrapper around a set of Backends, one for each chain. +// At this point, it only supports "a single account for all Backends". +// +// See Backends godoc for more information. +type Backends struct { + backends map[uint64]*Backend +} + +// NewFireBackends returns a multi-backends backed by fireblocks keys that supports configured all chains. +func NewFireBackends(ctx context.Context, testnet types.Testnet, fireCl fireblocks.Client) (Backends, error) { + inner := make(map[uint64]*Backend) + + // Configure iliad EVM Backend + if testnet.HasIliadEVM() { + chain := testnet.BroadcastIliadEVM() + ethCl, err := ethclient.Dial(chain.Chain.Name, chain.ExternalRPC) + if err != nil { + return Backends{}, errors.Wrap(err, "dial") + } + + inner[chain.Chain.ChainID], err = NewFireBackend(ctx, chain.Chain.Name, chain.Chain.ChainID, chain.Chain.BlockPeriod, ethCl, fireCl) + if err != nil { + return Backends{}, errors.Wrap(err, "new iliad Backend") + } + } + + // Configure anvil EVM Backends + for _, chain := range testnet.AnvilChains { + ethCl, err := ethclient.Dial(chain.Chain.Name, chain.ExternalRPC) + if err != nil { + return Backends{}, errors.Wrap(err, "dial") + } + + inner[chain.Chain.ChainID], err = NewFireBackend(ctx, chain.Chain.Name, chain.Chain.ChainID, chain.Chain.BlockPeriod, ethCl, fireCl) + if err != nil { + return Backends{}, errors.Wrap(err, "new anvil Backend") + } + } + + // Configure public EVM Backends + for _, chain := range testnet.PublicChains { + ethCl, err := ethclient.Dial(chain.Chain().Name, chain.NextRPCAddress()) + if err != nil { + return Backends{}, errors.Wrap(err, "dial") + } + + inner[chain.Chain().ChainID], err = NewFireBackend(ctx, chain.Chain().Name, chain.Chain().ChainID, chain.Chain().BlockPeriod, ethCl, fireCl) + if err != nil { + return Backends{}, errors.Wrap(err, "new public Backend") + } + } + + return Backends{ + backends: inner, + }, nil +} + +// NewBackends returns a multi-backends backed by in-memory keys that supports configured all chains. +func NewBackends(testnet types.Testnet, deployKeyFile string) (Backends, error) { + var err error + + var publicDeployKey *ecdsa.PrivateKey + if testnet.Network == netconf.Devnet { + if deployKeyFile != "" { + return Backends{}, errors.New("deploy key not supported in devnet") + } + } else if testnet.Network == netconf.Staging { + publicDeployKey, err = crypto.LoadECDSA(deployKeyFile) + } else { + return Backends{}, errors.New("unknown network") + } + if err != nil { + return Backends{}, errors.Wrap(err, "load deploy key") + } + + inner := make(map[uint64]*Backend) + + // Configure iliad EVM Backend + { + chain := testnet.BroadcastIliadEVM() + ethCl, err := ethclient.Dial(chain.Chain.Name, chain.ExternalRPC) + if err != nil { + return Backends{}, errors.Wrap(err, "dial") + } + + // dev iliad evm uses same dev accounts as anvil + // TODO: do not use dev anvil backend for prod iliad evms + backend, err := NewAnvilBackend(chain.Chain.Name, chain.Chain.ChainID, chain.Chain.BlockPeriod, ethCl) + if err != nil { + return Backends{}, errors.Wrap(err, "new iliad Backend") + } + + inner[chain.Chain.ChainID] = backend + } + + // Configure anvil EVM Backends + for _, chain := range testnet.AnvilChains { + ethCl, err := ethclient.Dial(chain.Chain.Name, chain.ExternalRPC) + if err != nil { + return Backends{}, errors.Wrap(err, "dial") + } + + backend, err := NewAnvilBackend(chain.Chain.Name, chain.Chain.ChainID, chain.Chain.BlockPeriod, ethCl) + if err != nil { + return Backends{}, errors.Wrap(err, "new anvil Backend") + } + + inner[chain.Chain.ChainID] = backend + } + + // Configure public EVM Backends + for _, chain := range testnet.PublicChains { + if publicDeployKey == nil { + return Backends{}, errors.New("public deploy key required") + } + ethCl, err := ethclient.Dial(chain.Chain().Name, chain.NextRPCAddress()) + if err != nil { + return Backends{}, errors.Wrap(err, "dial") + } + + inner[chain.Chain().ChainID], err = NewBackend(chain.Chain().Name, chain.Chain().ChainID, chain.Chain().BlockPeriod, ethCl, publicDeployKey) + if err != nil { + return Backends{}, errors.Wrap(err, "new public Backend") + } + } + + return Backends{ + backends: inner, + }, nil +} + +func (b Backends) All() map[uint64]*Backend { + return b.backends +} + +func (b Backends) Backend(sourceChainID uint64) (*Backend, error) { + backend, ok := b.backends[sourceChainID] + if !ok { + return nil, errors.New("unknown chain", "chain", sourceChainID) + } + + return backend, nil +} + +// BindOpts is a convenience function that an accounts' bind.TransactOpts and Backend for a given chain. +func (b Backends) BindOpts(ctx context.Context, sourceChainID uint64, addr common.Address) (*bind.TransactOpts, *Backend, error) { + backend, ok := b.backends[sourceChainID] + if !ok { + return nil, nil, errors.New("unknown chain", "chain", sourceChainID) + } + + opts, err := backend.BindOpts(ctx, addr) + if err != nil { + return nil, nil, errors.Wrap(err, "bind opts") + } + + return opts, backend, nil +} + +func (b Backends) RPCClients() map[uint64]ethclient.Client { + clients := make(map[uint64]ethclient.Client) + for chainID, backend := range b.backends { + clients[chainID] = backend.Client + } + + return clients +} + +func newFireblocksTxMgr(ethCl ethclient.Client, chainName string, chainID uint64, blockPeriod time.Duration, from common.Address, fireCl fireblocks.Client) (txmgr.TxManager, error) { + // creates our new CLI config for our tx manager + defaults := txmgr.DefaultSenderFlagValues + defaults.NetworkTimeout = time.Minute * 5 + cliConfig := txmgr.NewCLIConfig( + chainID, + blockPeriod/interval, + defaults, + ) + + // get the config for our tx manager + cfg, err := txmgr.NewConfigWithSigner(cliConfig, fireCl.Sign, from, ethCl) + if err != nil { + return nil, errors.Wrap(err, "new config") + } + + // create a simple tx manager from our config + txMgr, err := txmgr.NewSimple(chainName, cfg) + if err != nil { + return nil, errors.Wrap(err, "new simple") + } + + return txMgr, nil +} + +func newTxMgr(ethCl ethclient.Client, chainName string, chainID uint64, blockPeriod time.Duration, privateKey *ecdsa.PrivateKey) (txmgr.TxManager, error) { + // creates our new CLI config for our tx manager + cliConfig := txmgr.NewCLIConfig( + chainID, + blockPeriod/interval, + txmgr.DefaultSenderFlagValues, + ) + + // get the config for our tx manager + cfg, err := txmgr.NewConfig(cliConfig, privateKey, ethCl) + if err != nil { + return nil, errors.Wrap(err, "new config") + } + + // create a simple tx manager from our config + txMgr, err := txmgr.NewSimple(chainName, cfg) + if err != nil { + return nil, errors.Wrap(err, "new simple") + } + + return txMgr, nil +} diff --git a/lib/ethclient/ethbackend/wait.go b/lib/ethclient/ethbackend/wait.go new file mode 100644 index 00000000..d31e6e04 --- /dev/null +++ b/lib/ethclient/ethbackend/wait.go @@ -0,0 +1,66 @@ +package ethbackend + +import ( + "context" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/lib/errors" +) + +// NewWaiter returns a new Waiter. +func (b Backends) NewWaiter() *Waiter { + return &Waiter{ + b: b, + waiting: make(chan struct{}), + txs: make(map[uint64][]*ethtypes.Transaction), + } +} + +// Waiter is a convenience struct to easily wait for multiple transactions to be mined. +// Adding is thread safe, but it panics if Add is called after Wait. +type Waiter struct { + b Backends + mu sync.Mutex + waiting chan struct{} + txs map[uint64][]*ethtypes.Transaction +} + +func (w *Waiter) Add(chainID uint64, tx *ethtypes.Transaction) { + timer := time.NewTicker(time.Millisecond) + defer timer.Stop() + var locked bool + for !locked { + select { + case <-timer.C: + locked = w.mu.TryLock() + case <-w.waiting: + panic("waiting for a transaction already in progress") + } + } + defer w.mu.Unlock() + + w.txs[chainID] = append(w.txs[chainID], tx) +} + +func (w *Waiter) Wait(ctx context.Context) error { + w.mu.Lock() + defer w.mu.Unlock() + close(w.waiting) + + for chainID, txs := range w.txs { + for _, tx := range txs { + rec, err := bind.WaitMined(ctx, w.b.backends[chainID], tx) + if err != nil { + return errors.Wrap(err, "wait mined", "chain_id", chainID) + } else if rec.Status != ethtypes.ReceiptStatusSuccessful { + return errors.New("tx status unsuccessful", "chain_id", chainID) + } + } + } + + return nil +} diff --git a/lib/ethclient/ethclient.go b/lib/ethclient/ethclient.go new file mode 100644 index 00000000..37008d3e --- /dev/null +++ b/lib/ethclient/ethclient.go @@ -0,0 +1,162 @@ +package ethclient + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/piplabs/story/lib/errors" +) + +//go:generate go run genwrap/genwrap.go + +var _ Client = Wrapper{} + +type HeadType string + +func (h HeadType) String() string { + return string(h) +} + +func (h HeadType) Verify() error { + if !allHeadTypes[h] { + return errors.New("invalid head type", "head", h) + } + + return nil +} + +//nolint:gochecknoglobals // Static mappings +var allHeadTypes = map[HeadType]bool{ + HeadLatest: true, + HeadEarliest: true, + HeadPending: true, + HeadSafe: true, + HeadFinalized: true, +} + +const ( + HeadLatest HeadType = "latest" + HeadEarliest HeadType = "earliest" + HeadPending HeadType = "pending" + HeadSafe HeadType = "safe" + HeadFinalized HeadType = "finalized" +) + +// Wrapper wraps an ethclient.Client adding metrics and wrapped errors. +type Wrapper struct { + cl *ethclient.Client + chain string + address string +} + +// NewClient wraps an *rpc.Client adding metrics and wrapped errors. +func NewClient(cl *rpc.Client, chain, address string) Wrapper { + return Wrapper{ + cl: ethclient.NewClient(cl), + chain: chain, + address: address, + } +} + +// Dial connects a client to the given URL. +// +// Note if the URL is http(s), it doesn't return an error if it cannot connect to the URL. +// It will retry connecting on every call to a wrapped method. It will only return an error if the +// url is invalid. +func Dial(chainName string, url string) (Wrapper, error) { + cl, err := ethclient.Dial(url) + if err != nil { + return Wrapper{}, errors.Wrap(err, "dial", "chain", chainName, "url", url) + } + + return Wrapper{ + cl: cl, + chain: chainName, + address: url, + }, nil +} + +// Close closes the underlying RPC connection. +func (w Wrapper) Close() { + w.cl.Close() +} + +// Address returns the underlying RPC address. +func (w Wrapper) Address() string { + return w.address +} + +// HeaderByType returns the block header for the given head type. +func (w Wrapper) HeaderByType(ctx context.Context, typ HeadType) (*types.Header, error) { + const endpoint = "header_by_type" + defer latency(w.chain, endpoint)() + + var header *types.Header + err := w.cl.Client().CallContext( + ctx, + &header, + "eth_getBlockByNumber", + typ.String(), + false, + ) + if err != nil { + incError(w.chain, endpoint) + return nil, errors.Wrap(err, "get block") + } + + return header, nil +} + +// SetHead sets the current head of the local chain by block number. +// Note, this is a destructive action and may severely damage your chain. +// Use with extreme caution. +func (w Wrapper) SetHead(ctx context.Context, height uint64) error { + const endpoint = "set_head" + defer latency(w.chain, endpoint)() + + err := w.cl.Client().CallContext( + ctx, + nil, + "debug_setHead", + height, + ) + if err != nil { + incError(w.chain, endpoint) + return errors.Wrap(err, "set head") + } + + return nil +} + +// PeerCount returns the number of p2p peers as reported by the net_peerCount method. +func (w Wrapper) PeerCount(ctx context.Context) (uint64, error) { + const endpoint = "peer_count" + defer latency(w.chain, endpoint)() + + resp, err := w.cl.PeerCount(ctx) + if err != nil { + incError(w.chain, endpoint) + return 0, errors.Wrap(err, "rpc get payload v3") + } + + return resp, nil +} + +// EtherBalanceAt returns the current balance in ether of the provided account. +// Note this converts big.Int to float64 so IS NOT accurate. +// Only use if accuracy is not required, i.e., for display/metrics purposes. +func (w Wrapper) EtherBalanceAt(ctx context.Context, addr common.Address) (float64, error) { + b, err := w.BalanceAt(ctx, addr, nil) + if err != nil { + return 0, err + } + + bf, _ := b.Float64() + + return bf / params.Ether, nil +} diff --git a/lib/ethclient/ethclient_gen.go b/lib/ethclient/ethclient_gen.go new file mode 100644 index 00000000..b3b9d1dd --- /dev/null +++ b/lib/ethclient/ethclient_gen.go @@ -0,0 +1,497 @@ +package ethclient + +// Code generated by genwrap.go. DO NOT EDIT. + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/tracer" +) + +// Client defines all ethereum interfaces used in iliad. +type Client interface { + ethereum.BlockNumberReader + ethereum.ChainIDReader + ethereum.ChainReader + ethereum.ChainStateReader + ethereum.ChainSyncReader + ethereum.ContractCaller + ethereum.GasEstimator + ethereum.GasPricer + ethereum.GasPricer1559 + ethereum.LogFilterer + ethereum.PendingStateReader + ethereum.TransactionReader + ethereum.TransactionSender + HeaderByType(ctx context.Context, typ HeadType) (*types.Header, error) + EtherBalanceAt(ctx context.Context, addr common.Address) (float64, error) + PeerCount(ctx context.Context) (uint64, error) + SetHead(ctx context.Context, height uint64) error + Address() string + Close() +} + +func (w Wrapper) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + const endpoint = "block_by_hash" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.BlockByHash(ctx, hash) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + const endpoint = "block_by_number" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.BlockByNumber(ctx, number) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + const endpoint = "header_by_hash" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.HeaderByHash(ctx, hash) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + const endpoint = "header_by_number" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.HeaderByNumber(ctx, number) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) { + const endpoint = "transaction_count" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.TransactionCount(ctx, blockHash) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) { + const endpoint = "transaction_in_block" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.TransactionInBlock(ctx, blockHash, index) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +// This method subscribes to notifications about changes of the head block of +// the canonical chain. + +func (w Wrapper) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { + const endpoint = "subscribe_new_head" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.SubscribeNewHead(ctx, ch) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +// TransactionByHash checks the pool of pending transactions in addition to the +// blockchain. The isPending return value indicates whether the transaction has been +// mined yet. Note that the transaction may not be part of the canonical chain even if +// it's not pending. + +func (w Wrapper) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { + const endpoint = "transaction_by_hash" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, res1, err := w.cl.TransactionByHash(ctx, txHash) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, res1, err +} + +// TransactionReceipt returns the receipt of a mined transaction. Note that the +// transaction may not be included in the current canonical chain even if a receipt +// exists. + +func (w Wrapper) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + const endpoint = "transaction_receipt" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.TransactionReceipt(ctx, txHash) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { + const endpoint = "balance_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.BalanceAt(ctx, account, blockNumber) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { + const endpoint = "storage_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.StorageAt(ctx, account, key, blockNumber) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { + const endpoint = "code_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.CodeAt(ctx, account, blockNumber) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + const endpoint = "nonce_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.NonceAt(ctx, account, blockNumber) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) SyncProgress(ctx context.Context) (*ethereum.SyncProgress, error) { + const endpoint = "sync_progress" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.SyncProgress(ctx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + const endpoint = "call_contract" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.CallContract(ctx, call, blockNumber) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + const endpoint = "filter_logs" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.FilterLogs(ctx, q) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + const endpoint = "subscribe_filter_logs" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.SubscribeFilterLogs(ctx, q, ch) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) SendTransaction(ctx context.Context, tx *types.Transaction) error { + const endpoint = "send_transaction" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + err := w.cl.SendTransaction(ctx, tx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return err +} + +func (w Wrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + const endpoint = "suggest_gas_price" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.SuggestGasPrice(ctx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + const endpoint = "suggest_gas_tip_cap" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.SuggestGasTipCap(ctx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) PendingBalanceAt(ctx context.Context, account common.Address) (*big.Int, error) { + const endpoint = "pending_balance_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.PendingBalanceAt(ctx, account) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) PendingStorageAt(ctx context.Context, account common.Address, key common.Hash) ([]byte, error) { + const endpoint = "pending_storage_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.PendingStorageAt(ctx, account, key) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { + const endpoint = "pending_code_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.PendingCodeAt(ctx, account) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + const endpoint = "pending_nonce_at" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.PendingNonceAt(ctx, account) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) PendingTransactionCount(ctx context.Context) (uint, error) { + const endpoint = "pending_transaction_count" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.PendingTransactionCount(ctx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + const endpoint = "estimate_gas" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.EstimateGas(ctx, call) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) BlockNumber(ctx context.Context) (uint64, error) { + const endpoint = "block_number" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.BlockNumber(ctx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} + +func (w Wrapper) ChainID(ctx context.Context) (*big.Int, error) { + const endpoint = "chain_id" + defer latency(w.chain, endpoint)() + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + res0, err := w.cl.ChainID(ctx) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return res0, err +} diff --git a/lib/ethclient/fuzz.go b/lib/ethclient/fuzz.go new file mode 100644 index 00000000..7091a41b --- /dev/null +++ b/lib/ethclient/fuzz.go @@ -0,0 +1,50 @@ +package ethclient + +import ( + "math/big" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + fuzz "github.com/google/gofuzz" +) + +// NewFuzzer returns a new fuzzer for valid ethereum types. +// If seed is zero, it uses current nano time as the seed. +func NewFuzzer(seed int64) *fuzz.Fuzzer { + if seed == 0 { + seed = time.Now().UnixNano() + } + + f := fuzz.NewWithSeed(seed).NilChance(0) + f.Funcs( + func(h *types.Header, c fuzz.Continue) { + c.FuzzNoCustom(h) + h.Difficulty = common.Big0 + h.UncleHash = types.EmptyUncleHash + h.WithdrawalsHash = nil + h.ParentBeaconRoot = nil + h.Nonce = types.BlockNonce{} + }, + func(b *types.Block, c fuzz.Continue) { + var header types.Header + c.Fuzz(&header) + + block := types.NewBlock(&header, nil, nil, trie.NewStackTrie(nil)) + + *b = *block //nolint:govet // Copy lock isn't a problem since we are creating a new object. + }, + func(b *engine.ExecutableData, c fuzz.Continue) { + block := new(types.Block) + c.Fuzz(block) + + env := engine.BlockToExecutableData(block, big.NewInt(0), nil) + + *b = *env.ExecutionPayload + }, + ) + + return f +} diff --git a/lib/ethclient/fuzz_test.go b/lib/ethclient/fuzz_test.go new file mode 100644 index 00000000..0bc5cd94 --- /dev/null +++ b/lib/ethclient/fuzz_test.go @@ -0,0 +1,22 @@ +package ethclient_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/ethclient" +) + +func TestFuzzer(t *testing.T) { + t.Parallel() + f := ethclient.NewFuzzer(0) + + var payload engine.ExecutableData + f.Fuzz(&payload) + + // Ensure the fuzzed payload is valid by converting it to a block. + _, err := engine.ExecutableDataToBlock(payload, nil, nil) + require.NoError(t, err) +} diff --git a/lib/ethclient/genwrap/genwrap.go b/lib/ethclient/genwrap/genwrap.go new file mode 100644 index 00000000..7a1f4826 --- /dev/null +++ b/lib/ethclient/genwrap/genwrap.go @@ -0,0 +1,415 @@ +// Command genwrap provides a code generator for ethclient.Client wrapper +// that adds prometheus metrics and error wrapping. +// +// This code was mostly copied from Obol's Charon repo. +// https://github.com/ObolNetwork/charon/blob/main/app/eth2wrap/genwrap/genwrap.go +// +//nolint:gochecknoglobals,prealloc,gocognit // Some static config is required. Prealloc not worth it. +package main + +import ( + "bytes" + "context" + "fmt" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "go/ast" + "go/printer" + "go/token" + "golang.org/x/tools/imports" + "os" + "regexp" + "sort" + "strings" + "text/template" + + _ "github.com/ethereum/go-ethereum" + "golang.org/x/tools/go/packages" +) + +var ( + tpl = `package ethclient + +// Code generated by genwrap.go. DO NOT EDIT. + +import ( + "github.com/piplabs/story/lib/errors" + "github.com/ethereum/go-ethereum" +{{- range .Imports}} + {{.}} +{{- end}} +) + +// Client defines all ethereum interfaces used in iliad. +type Client interface { + {{range .Providers}} ethereum.{{.}} + {{end -}} + HeaderByType(ctx context.Context, typ HeadType) (*types.Header, error) + EtherBalanceAt(ctx context.Context, addr common.Address) (float64, error) + PeerCount(ctx context.Context) (uint64, error) + SetHead(ctx context.Context, height uint64) error + Address() string + Close() +} + +{{range .Methods}} + {{.Doc}} + func (w Wrapper) {{.Name}}({{.Params}}) ({{.ResultTypes}}) { + const endpoint = "{{.Label}}" + {{if .Latency}}defer latency(w.chain, endpoint)() {{end}} + + ctx, span := tracer.Start(ctx, spanName(endpoint)) + defer span.End() + + {{.ResultNames}} := w.cl.{{.Name}}({{.ParamNames}}) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return {{.ResultNames}} + } +{{end}} +` + + // interfaces defines all the interfaces to implement. + interfaces = map[string]bool{ + "ChainReader": true, + "TransactionReader": true, + "ChainStateReader": true, + "ChainSyncReader": true, + "ContractCaller": true, + "LogFilterer": true, + "TransactionSender": true, + "GasPricer": true, + "GasPricer1559": true, + "PendingStateReader": true, + "GasEstimator": true, + "BlockNumberReader": true, + "ChainIDReader": true, + } + + // addImport indicates which types need hardcoded imports. + addImport = map[string]string{ + "CallMsg": "ethereum", + "FilterQuery": "ethereum", + "*SyncProgress": "ethereum", + "Subscription": "ethereum", + } + + // successFuncs indicates which endpoints have custom success functions. + successFuncs = map[string]string{} + + skipImport = map[string]bool{ + "\"errors\"": true, + } +) + +type Method struct { + Name string + Doc string + Latency bool + DoFunc string + SuccessFunc string + params []Field + results []Field +} + +func (m Method) Label() string { + return toSnakeCase(m.Name) +} + +func (m Method) Params() string { + var resp []string + for _, param := range m.params { + resp = append(resp, fmt.Sprintf("%s %s", param.Name, param.Type)) + } + + return strings.Join(resp, ", ") +} + +func (m Method) Results() string { + var resp []string + for _, result := range m.results { + resp = append(resp, fmt.Sprintf("%s %s", result.Name, result.Type)) + } + + return strings.Join(resp, ", ") +} + +func (m Method) ParamNames() string { + var resp []string + for _, param := range m.params { + resp = append(resp, param.Name) + } + + return strings.Join(resp, ", ") +} + +func (m Method) NamedResults() string { + var resp []string + for _, result := range m.results { + resp = append(resp, fmt.Sprintf("%s %s", result.Name, result.Type)) + } + + return strings.Join(resp, ", ") +} + +func (m Method) ResultNames() string { + var resp []string + for _, result := range m.results { + resp = append(resp, result.Name) + } + + return strings.Join(resp, ", ") +} + +func (m Method) ResultTypes() string { + var resp []string + for _, result := range m.results { + resp = append(resp, result.Type) + } + + return strings.Join(resp, ", ") +} + +type Field struct { + Name string + Type string +} + +func main() { + ctx := context.Background() + err := run(ctx) + if err != nil { + log.Error(ctx, "āŒ Fatal error", err) + } +} + +func run(_ context.Context) error { + pkgs, err := packages.Load( + &packages.Config{ + Mode: packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedTypes, + }, + "github.com/ethereum/go-ethereum", + ) + if err != nil { + return errors.Wrap(err, "load package") + } + + methods, providers, err := parseEthMethods(pkgs[0]) + if err != nil { + return err + } + + imprts, err := parseImports(pkgs[0]) + if err != nil { + return err + } + + return writeTemplate(methods, providers, imprts) +} + +func parseImports(pkg *packages.Package) ([]string, error) { + var ( + dups = make(map[string]bool) + resp []string + ) + + for _, file := range pkg.Syntax { + for _, imprt := range file.Imports { + var b bytes.Buffer + err := printer.Fprint(&b, pkg.Fset, imprt) + if err != nil { + return nil, errors.Wrap(err, "printf") + } + + name := b.String() + if skipImport[name] { + continue + } + + dups[name] = true + resp = append(resp, name) + } + } + + return resp, nil +} + +func writeTemplate(methods []Method, providers []string, imprts []string) error { + t, err := template.New("").Parse(tpl) + if err != nil { + return errors.Wrap(err, "parse template") + } + + sort.Strings(providers) + + var b bytes.Buffer + err = t.Execute(&b, struct { + Providers []string + Methods []Method + Imports []string + }{ + Providers: providers, + Methods: methods, + Imports: imprts, + }) + if err != nil { + return errors.Wrap(err, "exec template") + } + + filename := "ethclient_gen.go" + out, err := imports.Process(filename, b.Bytes(), nil) + if err != nil { + return errors.Wrap(err, "format") + } + + err = os.WriteFile(filename, out, 0o644) + if err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + +func parseEthMethods(pkg *packages.Package) ([]Method, []string, error) { + var ( + methods []Method + providers []string + ) + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + gendecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + + if gendecl.Tok != token.TYPE { + continue + } + + for _, spec := range gendecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + iface, ok := typeSpec.Type.(*ast.InterfaceType) + if !ok { + continue + } + + latency, add := interfaces[typeSpec.Name.Name] + if !add { + continue + } + + providers = append(providers, typeSpec.Name.Name) + + for _, method := range iface.Methods.List { + fnType, ok := method.Type.(*ast.FuncType) + if !ok { + continue + } + + name := method.Names[0].Name + + var params []Field + for _, param := range fnType.Params.List { + var b bytes.Buffer + err := printer.Fprint(&b, pkg.Fset, param.Type) + if err != nil { + return nil, nil, errors.Wrap(err, "printf") + } + + typ := b.String() + if imprt, ok := addImport[typ]; ok { + typ = imprt + "." + typ + } + + field := Field{ + Name: param.Names[0].Name, + Type: typ, + } + + params = append(params, field) + } + + var results []Field + for i, result := range fnType.Results.List { + var b bytes.Buffer + err := printer.Fprint(&b, pkg.Fset, result.Type) + if err != nil { + return nil, nil, errors.Wrap(err, "printf") + } + + typ := b.String() + if imprt, ok := addImport[typ]; ok { + prefix := "" + if strings.HasPrefix(typ, "*") { + prefix = "*" + typ = strings.TrimPrefix(typ, "*") + } + typ = prefix + imprt + "." + typ + } + + name := fmt.Sprintf("res%d", i) + if i == fnType.Results.NumFields()-1 { + name = "err" + } + + field := Field{ + Name: name, + Type: typ, + } + + results = append(results, field) + } + + var doc string + if method.Doc != nil { + for _, line := range strings.Split(strings.TrimSpace(method.Doc.Text()), "\n") { + doc += "// " + line + "\n" + } + } + + successFunc := "nil," + if fn, ok := successFuncs[name]; ok { + successFunc = fn + "," + } + + dofunc := "provide" + if len(results) == 1 { + dofunc = "submit" + successFunc = "" + } + + methods = append(methods, Method{ + Name: name, + Doc: doc, + Latency: latency, + DoFunc: dofunc, + SuccessFunc: successFunc, + params: params, + results: results, + }) + } + } + } + } + + return methods, providers, nil +} + +var ( + matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") + matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") +) + +func toSnakeCase(str string) string { + snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + + return strings.ToLower(snake) +} diff --git a/lib/ethclient/jwt.go b/lib/ethclient/jwt.go new file mode 100644 index 00000000..9ef8b9b1 --- /dev/null +++ b/lib/ethclient/jwt.go @@ -0,0 +1,63 @@ +package ethclient + +import ( + "bytes" + "encoding/hex" + "net/http" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + + "github.com/piplabs/story/lib/errors" +) + +type jwtRoundTripper struct { + underlyingTransport http.RoundTripper + jwtSecret []byte +} + +func newJWTRoundTripper(transport http.RoundTripper, jwtSecret []byte) *jwtRoundTripper { + return &jwtRoundTripper{ + underlyingTransport: transport, + jwtSecret: jwtSecret, + } +} + +func (t *jwtRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iat": time.Now().Unix(), + }) + + tokenString, err := token.SignedString(t.jwtSecret) + if err != nil { + return nil, errors.Wrap(err, "jwt token string") + } + + req.Header.Set("Authorization", "Bearer "+tokenString) + + resp, err := t.underlyingTransport.RoundTrip(req) + if err != nil { + return nil, errors.Wrap(err, "round trip") + } + + return resp, nil +} + +// LoadJWTHexFile loads a hex encoded JWT secret from the provided file. +func LoadJWTHexFile(file string) ([]byte, error) { + jwtHex, err := os.ReadFile(file) + if err != nil { + return nil, errors.Wrap(err, "read jwt file") + } + + jwtHex = bytes.TrimSpace(jwtHex) + jwtHex = bytes.TrimPrefix(jwtHex, []byte("0x")) + + jwtBytes, err := hex.DecodeString(string(jwtHex)) + if err != nil { + return nil, errors.Wrap(err, "decode jwt file") + } + + return jwtBytes, nil +} diff --git a/lib/ethclient/metrics.go b/lib/ethclient/metrics.go new file mode 100644 index 00000000..d0c1d3c4 --- /dev/null +++ b/lib/ethclient/metrics.go @@ -0,0 +1,41 @@ +package ethclient + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + latencyHist = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "lib", + Subsystem: "ethclient", + Name: "latency_seconds", + Help: "Latency in seconds for ethereum JSON-RPC requests by chain and endpoint", + }, []string{"chain", "endpoint"}) + + errorCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "lib", + Subsystem: "ethclient", + Name: "errors_total", + Help: "Total number of errors returned by a Ethereum JSON-RPC by chain and endpoint", + }, []string{"chain", "endpoint"}) +) + +// latency returns a function that records the latency of an RPC call. +func latency(chain string, endpoint string) func() { + start := time.Now() + return func() { + latencyHist.WithLabelValues(chain, endpoint).Observe(time.Since(start).Seconds()) + } +} + +// incError increments the error count for a given chain and endpoint. +func incError(chain, endpoint string) { + errorCount.WithLabelValues(chain, endpoint).Inc() +} + +func spanName(endpoint string) string { + return "ethclient/" + endpoint +} diff --git a/lib/ethclient/mock/generate.go b/lib/ethclient/mock/generate.go new file mode 100644 index 00000000..e8360b79 --- /dev/null +++ b/lib/ethclient/mock/generate.go @@ -0,0 +1,2 @@ +//go:generate mockgen -source ../ethclient_gen.go -package mock -destination ./mock_interfaces.go +package mock diff --git a/lib/ethclient/mock/mock_interfaces.go b/lib/ethclient/mock/mock_interfaces.go new file mode 100644 index 00000000..62bde389 --- /dev/null +++ b/lib/ethclient/mock/mock_interfaces.go @@ -0,0 +1,550 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../ethclient_gen.go +// +// Generated by this command: +// +// mockgen -source ../ethclient_gen.go -package mock -destination ./mock_interfaces.go +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + big "math/big" + reflect "reflect" + + ethereum "github.com/ethereum/go-ethereum" + common "github.com/ethereum/go-ethereum/common" + types "github.com/ethereum/go-ethereum/core/types" + ethclient "github.com/piplabs/story/lib/ethclient" + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Address mocks base method. +func (m *MockClient) Address() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Address") + ret0, _ := ret[0].(string) + return ret0 +} + +// Address indicates an expected call of Address. +func (mr *MockClientMockRecorder) Address() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockClient)(nil).Address)) +} + +// BalanceAt mocks base method. +func (m *MockClient) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BalanceAt", ctx, account, blockNumber) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BalanceAt indicates an expected call of BalanceAt. +func (mr *MockClientMockRecorder) BalanceAt(ctx, account, blockNumber any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BalanceAt", reflect.TypeOf((*MockClient)(nil).BalanceAt), ctx, account, blockNumber) +} + +// BlockByHash mocks base method. +func (m *MockClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByHash", ctx, hash) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByHash indicates an expected call of BlockByHash. +func (mr *MockClientMockRecorder) BlockByHash(ctx, hash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByHash", reflect.TypeOf((*MockClient)(nil).BlockByHash), ctx, hash) +} + +// BlockByNumber mocks base method. +func (m *MockClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByNumber", ctx, number) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByNumber indicates an expected call of BlockByNumber. +func (mr *MockClientMockRecorder) BlockByNumber(ctx, number any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockClient)(nil).BlockByNumber), ctx, number) +} + +// BlockNumber mocks base method. +func (m *MockClient) BlockNumber(ctx context.Context) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockHeight", ctx) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockNumber indicates an expected call of BlockNumber. +func (mr *MockClientMockRecorder) BlockNumber(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockHeight", reflect.TypeOf((*MockClient)(nil).BlockNumber), ctx) +} + +// CallContract mocks base method. +func (m *MockClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CallContract", ctx, call, blockNumber) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallContract indicates an expected call of CallContract. +func (mr *MockClientMockRecorder) CallContract(ctx, call, blockNumber any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallContract", reflect.TypeOf((*MockClient)(nil).CallContract), ctx, call, blockNumber) +} + +// ChainID mocks base method. +func (m *MockClient) ChainID(ctx context.Context) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChainID", ctx) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChainID indicates an expected call of ChainID. +func (mr *MockClientMockRecorder) ChainID(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainID", reflect.TypeOf((*MockClient)(nil).ChainID), ctx) +} + +// Close mocks base method. +func (m *MockClient) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockClientMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close)) +} + +// CodeAt mocks base method. +func (m *MockClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CodeAt", ctx, account, blockNumber) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CodeAt indicates an expected call of CodeAt. +func (mr *MockClientMockRecorder) CodeAt(ctx, account, blockNumber any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeAt", reflect.TypeOf((*MockClient)(nil).CodeAt), ctx, account, blockNumber) +} + +// EstimateGas mocks base method. +func (m *MockClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EstimateGas", ctx, call) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EstimateGas indicates an expected call of EstimateGas. +func (mr *MockClientMockRecorder) EstimateGas(ctx, call any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateGas", reflect.TypeOf((*MockClient)(nil).EstimateGas), ctx, call) +} + +// EtherBalanceAt mocks base method. +func (m *MockClient) EtherBalanceAt(ctx context.Context, addr common.Address) (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EtherBalanceAt", ctx, addr) + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EtherBalanceAt indicates an expected call of EtherBalanceAt. +func (mr *MockClientMockRecorder) EtherBalanceAt(ctx, addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EtherBalanceAt", reflect.TypeOf((*MockClient)(nil).EtherBalanceAt), ctx, addr) +} + +// FilterLogs mocks base method. +func (m *MockClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FilterLogs", ctx, q) + ret0, _ := ret[0].([]types.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FilterLogs indicates an expected call of FilterLogs. +func (mr *MockClientMockRecorder) FilterLogs(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterLogs", reflect.TypeOf((*MockClient)(nil).FilterLogs), ctx, q) +} + +// HeaderByHash mocks base method. +func (m *MockClient) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByHash", ctx, hash) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByHash indicates an expected call of HeaderByHash. +func (mr *MockClientMockRecorder) HeaderByHash(ctx, hash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByHash", reflect.TypeOf((*MockClient)(nil).HeaderByHash), ctx, hash) +} + +// HeaderByNumber mocks base method. +func (m *MockClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByNumber", ctx, number) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByNumber indicates an expected call of HeaderByNumber. +func (mr *MockClientMockRecorder) HeaderByNumber(ctx, number any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByNumber", reflect.TypeOf((*MockClient)(nil).HeaderByNumber), ctx, number) +} + +// HeaderByType mocks base method. +func (m *MockClient) HeaderByType(ctx context.Context, typ ethclient.HeadType) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByType", ctx, typ) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByType indicates an expected call of HeaderByType. +func (mr *MockClientMockRecorder) HeaderByType(ctx, typ any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByType", reflect.TypeOf((*MockClient)(nil).HeaderByType), ctx, typ) +} + +// NonceAt mocks base method. +func (m *MockClient) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NonceAt", ctx, account, blockNumber) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NonceAt indicates an expected call of NonceAt. +func (mr *MockClientMockRecorder) NonceAt(ctx, account, blockNumber any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NonceAt", reflect.TypeOf((*MockClient)(nil).NonceAt), ctx, account, blockNumber) +} + +// PeerCount mocks base method. +func (m *MockClient) PeerCount(ctx context.Context) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeerCount", ctx) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PeerCount indicates an expected call of PeerCount. +func (mr *MockClientMockRecorder) PeerCount(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeerCount", reflect.TypeOf((*MockClient)(nil).PeerCount), ctx) +} + +// PendingBalanceAt mocks base method. +func (m *MockClient) PendingBalanceAt(ctx context.Context, account common.Address) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PendingBalanceAt", ctx, account) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PendingBalanceAt indicates an expected call of PendingBalanceAt. +func (mr *MockClientMockRecorder) PendingBalanceAt(ctx, account any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PendingBalanceAt", reflect.TypeOf((*MockClient)(nil).PendingBalanceAt), ctx, account) +} + +// PendingCodeAt mocks base method. +func (m *MockClient) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PendingCodeAt", ctx, account) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PendingCodeAt indicates an expected call of PendingCodeAt. +func (mr *MockClientMockRecorder) PendingCodeAt(ctx, account any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PendingCodeAt", reflect.TypeOf((*MockClient)(nil).PendingCodeAt), ctx, account) +} + +// PendingNonceAt mocks base method. +func (m *MockClient) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PendingNonceAt", ctx, account) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PendingNonceAt indicates an expected call of PendingNonceAt. +func (mr *MockClientMockRecorder) PendingNonceAt(ctx, account any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PendingNonceAt", reflect.TypeOf((*MockClient)(nil).PendingNonceAt), ctx, account) +} + +// PendingStorageAt mocks base method. +func (m *MockClient) PendingStorageAt(ctx context.Context, account common.Address, key common.Hash) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PendingStorageAt", ctx, account, key) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PendingStorageAt indicates an expected call of PendingStorageAt. +func (mr *MockClientMockRecorder) PendingStorageAt(ctx, account, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PendingStorageAt", reflect.TypeOf((*MockClient)(nil).PendingStorageAt), ctx, account, key) +} + +// PendingTransactionCount mocks base method. +func (m *MockClient) PendingTransactionCount(ctx context.Context) (uint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PendingTransactionCount", ctx) + ret0, _ := ret[0].(uint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PendingTransactionCount indicates an expected call of PendingTransactionCount. +func (mr *MockClientMockRecorder) PendingTransactionCount(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PendingTransactionCount", reflect.TypeOf((*MockClient)(nil).PendingTransactionCount), ctx) +} + +// SendTransaction mocks base method. +func (m *MockClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendTransaction", ctx, tx) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendTransaction indicates an expected call of SendTransaction. +func (mr *MockClientMockRecorder) SendTransaction(ctx, tx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendTransaction", reflect.TypeOf((*MockClient)(nil).SendTransaction), ctx, tx) +} + +// SetHead mocks base method. +func (m *MockClient) SetHead(ctx context.Context, height uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetHead", ctx, height) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetHead indicates an expected call of SetHead. +func (mr *MockClientMockRecorder) SetHead(ctx, height any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHead", reflect.TypeOf((*MockClient)(nil).SetHead), ctx, height) +} + +// StorageAt mocks base method. +func (m *MockClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StorageAt", ctx, account, key, blockNumber) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StorageAt indicates an expected call of StorageAt. +func (mr *MockClientMockRecorder) StorageAt(ctx, account, key, blockNumber any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorageAt", reflect.TypeOf((*MockClient)(nil).StorageAt), ctx, account, key, blockNumber) +} + +// SubscribeFilterLogs mocks base method. +func (m *MockClient) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeFilterLogs", ctx, q, ch) + ret0, _ := ret[0].(ethereum.Subscription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeFilterLogs indicates an expected call of SubscribeFilterLogs. +func (mr *MockClientMockRecorder) SubscribeFilterLogs(ctx, q, ch any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeFilterLogs", reflect.TypeOf((*MockClient)(nil).SubscribeFilterLogs), ctx, q, ch) +} + +// SubscribeNewHead mocks base method. +func (m *MockClient) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeNewHead", ctx, ch) + ret0, _ := ret[0].(ethereum.Subscription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeNewHead indicates an expected call of SubscribeNewHead. +func (mr *MockClientMockRecorder) SubscribeNewHead(ctx, ch any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeNewHead", reflect.TypeOf((*MockClient)(nil).SubscribeNewHead), ctx, ch) +} + +// SuggestGasPrice mocks base method. +func (m *MockClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SuggestGasPrice", ctx) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SuggestGasPrice indicates an expected call of SuggestGasPrice. +func (mr *MockClientMockRecorder) SuggestGasPrice(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SuggestGasPrice", reflect.TypeOf((*MockClient)(nil).SuggestGasPrice), ctx) +} + +// SuggestGasTipCap mocks base method. +func (m *MockClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SuggestGasTipCap", ctx) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SuggestGasTipCap indicates an expected call of SuggestGasTipCap. +func (mr *MockClientMockRecorder) SuggestGasTipCap(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SuggestGasTipCap", reflect.TypeOf((*MockClient)(nil).SuggestGasTipCap), ctx) +} + +// SyncProgress mocks base method. +func (m *MockClient) SyncProgress(ctx context.Context) (*ethereum.SyncProgress, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SyncProgress", ctx) + ret0, _ := ret[0].(*ethereum.SyncProgress) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SyncProgress indicates an expected call of SyncProgress. +func (mr *MockClientMockRecorder) SyncProgress(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncProgress", reflect.TypeOf((*MockClient)(nil).SyncProgress), ctx) +} + +// TransactionByHash mocks base method. +func (m *MockClient) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionByHash", ctx, txHash) + ret0, _ := ret[0].(*types.Transaction) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// TransactionByHash indicates an expected call of TransactionByHash. +func (mr *MockClientMockRecorder) TransactionByHash(ctx, txHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionByHash", reflect.TypeOf((*MockClient)(nil).TransactionByHash), ctx, txHash) +} + +// TransactionCount mocks base method. +func (m *MockClient) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionCount", ctx, blockHash) + ret0, _ := ret[0].(uint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionCount indicates an expected call of TransactionCount. +func (mr *MockClientMockRecorder) TransactionCount(ctx, blockHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionCount", reflect.TypeOf((*MockClient)(nil).TransactionCount), ctx, blockHash) +} + +// TransactionInBlock mocks base method. +func (m *MockClient) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionInBlock", ctx, blockHash, index) + ret0, _ := ret[0].(*types.Transaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionInBlock indicates an expected call of TransactionInBlock. +func (mr *MockClientMockRecorder) TransactionInBlock(ctx, blockHash, index any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionInBlock", reflect.TypeOf((*MockClient)(nil).TransactionInBlock), ctx, blockHash, index) +} + +// TransactionReceipt mocks base method. +func (m *MockClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionReceipt", ctx, txHash) + ret0, _ := ret[0].(*types.Receipt) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionReceipt indicates an expected call of TransactionReceipt. +func (mr *MockClientMockRecorder) TransactionReceipt(ctx, txHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionReceipt", reflect.TypeOf((*MockClient)(nil).TransactionReceipt), ctx, txHash) +} diff --git a/lib/evmchain/evmchain.go b/lib/evmchain/evmchain.go new file mode 100644 index 00000000..3ee74931 --- /dev/null +++ b/lib/evmchain/evmchain.go @@ -0,0 +1,138 @@ +// Package evmchain provides static metadata about supported evm chains. +package evmchain + +import ( + "time" + + "github.com/piplabs/story/lib/tokens" +) + +const ( + // Mainnets. + IDEthereum uint64 = 1 + IDIliadMainnet uint64 = 1514 + + // Local Testet. + IDLocal uint64 = 1511 + + // Testnets. + IDIliadTestnet uint64 = 1511 + IDIliad uint64 = 1513 + IDHolesky uint64 = 17000 + IDArbSepolia uint64 = 421614 + IDOpSepolia uint64 = 11155420 + + // Ephemeral. + IDIliadEphemeral uint64 = 1651 + IDMockL1Fast uint64 = 1652 + IDMockL1Slow uint64 = 1653 + IDMockL2 uint64 = 1654 + IDMockOp uint64 = 1655 + IDMockArb uint64 = 1656 + + iliadEVMName = "iliad_evm" + iliadEVMBlockPeriod = time.Second * 2 +) + +type Metadata struct { + ChainID uint64 + Name string + BlockPeriod time.Duration + NativeToken tokens.Token +} + +func MetadataByID(chainID uint64) (Metadata, bool) { + resp, ok := static[chainID] + return resp, ok +} + +func MetadataByName(name string) (Metadata, bool) { + for _, metadata := range static { + if metadata.Name == name { + return metadata, true + } + } + + return Metadata{}, false +} + +var static = map[uint64]Metadata{ + IDEthereum: { + ChainID: IDEthereum, + Name: "ethereum", + BlockPeriod: 12 * time.Second, + NativeToken: tokens.ETH, + }, + IDIliadMainnet: { + ChainID: IDIliadMainnet, + Name: iliadEVMName, + BlockPeriod: iliadEVMBlockPeriod, + NativeToken: tokens.ILIAD, + }, + IDIliadTestnet: { + ChainID: IDIliadTestnet, + Name: iliadEVMName, + BlockPeriod: iliadEVMBlockPeriod, + NativeToken: tokens.ILIAD, + }, + IDIliad: { + ChainID: IDIliad, + Name: iliadEVMName, + BlockPeriod: iliadEVMBlockPeriod, + NativeToken: tokens.ILIAD, + }, + IDHolesky: { + ChainID: IDHolesky, + Name: "holesky", + BlockPeriod: 12 * time.Second, + NativeToken: tokens.ETH, + }, + IDArbSepolia: { + ChainID: IDArbSepolia, + Name: "arb_sepolia", + BlockPeriod: 300 * time.Millisecond, + NativeToken: tokens.ETH, + }, + IDOpSepolia: { + ChainID: IDOpSepolia, + Name: "op_sepolia", + BlockPeriod: 2 * time.Second, + NativeToken: tokens.ETH, + }, + IDIliadEphemeral: { + ChainID: IDIliadEphemeral, + Name: iliadEVMName, + BlockPeriod: iliadEVMBlockPeriod, + NativeToken: tokens.ILIAD, + }, + IDMockL1Fast: { + ChainID: IDMockL1Fast, + Name: "mock_l1", + BlockPeriod: time.Second, + NativeToken: tokens.ETH, + }, + IDMockL1Slow: { + ChainID: IDMockL1Slow, + Name: "slow_l1", + BlockPeriod: time.Second * 12, + NativeToken: tokens.ETH, + }, + IDMockL2: { + ChainID: IDMockL2, + Name: "mock_l2", + BlockPeriod: time.Second, + NativeToken: tokens.ETH, + }, + IDMockOp: { + ChainID: IDMockOp, + Name: "mock_op", + BlockPeriod: time.Second * 2, + NativeToken: tokens.ETH, + }, + IDMockArb: { + ChainID: IDMockArb, + Name: "mock_arb", + BlockPeriod: time.Second / 4, + NativeToken: tokens.ETH, + }, +} diff --git a/lib/evmchain/evmchain_internal_test.go b/lib/evmchain/evmchain_internal_test.go new file mode 100644 index 00000000..5b054e38 --- /dev/null +++ b/lib/evmchain/evmchain_internal_test.go @@ -0,0 +1,26 @@ +package evmchain + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVerify(t *testing.T) { + t.Parallel() + + uniqNames := make(map[string]bool) + uniqChainIDs := make(map[uint64]bool) + + for chainID, metadata := range static { + require.Equal(t, chainID, metadata.ChainID) + require.NotEmpty(t, metadata.BlockPeriod) + + if metadata.Name != iliadEVMName { + require.False(t, uniqNames[metadata.Name]) + } + require.False(t, uniqChainIDs[metadata.ChainID]) + uniqNames[metadata.Name] = true + uniqChainIDs[metadata.ChainID] = true + } +} diff --git a/lib/expbackoff/expbackoff.go b/lib/expbackoff/expbackoff.go new file mode 100644 index 00000000..aee65a0b --- /dev/null +++ b/lib/expbackoff/expbackoff.go @@ -0,0 +1,237 @@ +// Package expbackoff implements exponential backoff. +// +// It was copied from the GPL version of Obol which was +// originally copied from google.golang.org/grpc. +// +// See: +// - https://github.com/grpc/grpc-go/tree/master/backoff +// - https://github.com/ObolNetwork/charon/tree/v0.14.0/app/expbackoff +// +//nolint:gochecknoglobals // Default config and test-alias globals are ok. +package expbackoff + +import ( + "context" + "math/rand" + "testing" + "time" +) + +// Config defines the configuration options for backoff. +type Config struct { + // BaseDelay is the amount of time to backoff after the first failure. + BaseDelay time.Duration + // Multiplier is the factor with which to multiply backoffs after a + // failed retry. Should ideally be greater than 1. + Multiplier float64 + // Jitter is the factor with which backoffs are randomized. + Jitter float64 + // MaxDelay is the upper bound of backoff delay. + MaxDelay time.Duration +} + +// DefaultConfig is a backoff configuration with the default values specified +// at https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md. +// +// This should be useful for callers who want to configure backoff with +// non-default values only for a subset of the options. +// +// Copied from google.golang.org/grpc@v1.48.0/backoff/backoff.go. +var DefaultConfig = Config{ + BaseDelay: 1.0 * time.Second, + Multiplier: 1.6, + Jitter: 0.2, + MaxDelay: 120 * time.Second, +} + +// FastConfig is a common configuration for fast backoff. +var FastConfig = Config{ + BaseDelay: 100 * time.Millisecond, + Multiplier: 1.6, + Jitter: 0.2, + MaxDelay: 5 * time.Second, +} + +// WithPeriodicConfig configures the backoff with periodic backoff. +func WithPeriodicConfig(period time.Duration) func(*Config) { + return func(config *Config) { + config.BaseDelay = period + config.Multiplier = 1 + } +} + +// WithFastConfig configures the backoff with FastConfig. +func WithFastConfig() func(*Config) { + return func(config *Config) { + *config = FastConfig + } +} + +// With configures the backoff with the provided config. +func With(c Config) func(*Config) { + return func(config *Config) { + *config = c + } +} + +// New returns a backoff function configured via functional options applied to DefaultConfig. +// The backoff function will exponentially sleep longer each time it is called. +// The backoff function returns immediately after the context is canceled. +// +// Usage: +// +// backoff := expbackoff.New(ctx) +// for ctx.Err() == nil { +// resp, err := doThing(ctx) +// if err != nil { +// backoff() +// continue +// } else { +// return resp +// } +// } +// +//nolint:nonamedreturns // Named returns used for clear code. +func New(ctx context.Context, opts ...func(*Config)) (backoff func()) { + backoff, _ = NewWithReset(ctx, opts...) + return backoff +} + +// NewWithReset returns a backoff and a reset function configured via functional options applied to DefaultConfig. +// The backoff function will exponentially sleep longer each time it is called. +// Calling the reset function will reset the backoff sleep duration to Config.BaseDelay. +// The backoff function returns immediately after the context is canceled. +// +// Usage: +// +// backoff, reset := expbackoff.NewWithReset(ctx) +// for ctx.Err() == nil { +// resp, err := doThing(ctx) +// if err != nil { +// backoff() +// continue +// } else { +// reset() +// // Do something with the response. +// } +// } +// +//nolint:nonamedreturns // Named returns used for clear code. +func NewWithReset(ctx context.Context, opts ...func(*Config)) (backoff func(), reset func()) { + conf := DefaultConfig + for _, opt := range opts { + opt(&conf) + } + + var retries int + + backoff = func() { + if ctx.Err() != nil { + return + } + + select { + case <-ctx.Done(): + case <-after(Backoff(conf, retries)): + } + retries++ + } + + reset = func() { + retries = 0 + } + + return backoff, reset +} + +// NewWithAutoReset returns a backoff function configured via functional options applied to DefaultConfig. +// The backoff function will exponentially sleep longer each time it is called. +// The backoff function is automatically reset if sufficient time has passed since the last backoff. +// +// This "sufficient delay" is the next backoff duration, so if the next backoff duration is 1s, +// the backoff will reset to initial duration after 1s of not being called. +// +//nolint:nonamedreturns // Named returns used for clear code. +func NewWithAutoReset(ctx context.Context, opts ...func(*Config)) (backoff func()) { + conf := DefaultConfig + for _, opt := range opts { + opt(&conf) + } + + var retries int + + lastBackoff := time.Now() + backoff = func() { + if ctx.Err() != nil { + return + } + + autoResetAt := lastBackoff.Add(Backoff(conf, retries)) + if time.Now().After(autoResetAt) { + retries = 0 + } + + select { + case <-ctx.Done(): + case <-after(Backoff(conf, retries)): + } + retries++ + lastBackoff = time.Now() + } + + return backoff +} + +// Backoff returns the amount of time to wait before the next retry given the +// number of retries. +// Copied from google.golang.org/grpc@v1.48.0/internal/backoff/backoff.go. +func Backoff(config Config, retries int) time.Duration { + if retries == 0 { + return config.BaseDelay + } + + backoff := float64(config.BaseDelay) + maxDelay := float64(config.MaxDelay) + + for backoff < maxDelay && retries > 0 { + backoff *= config.Multiplier + retries-- + } + if backoff > maxDelay { + backoff = maxDelay + } + // Randomize backoff delays so that if a cluster of requests start at + // the same time, they won't operate in lockstep. + backoff *= 1 + config.Jitter*(randFloat()*2-1) + if backoff < 0 { + return 0 + } + + return time.Duration(backoff) +} + +// after is aliased for testing. +var after = time.After + +// SetAfterForT sets the after internal function for testing. +func SetAfterForT(t *testing.T, fn func(d time.Duration) <-chan time.Time) { + t.Helper() + cached := after + after = fn + t.Cleanup(func() { + after = cached + }) +} + +// randFloat is aliased for testing. +var randFloat = rand.Float64 + +// SetRandFloatForT sets the random float internal function for testing. +func SetRandFloatForT(t *testing.T, fn func() float64) { + t.Helper() + cached := randFloat + randFloat = fn + t.Cleanup(func() { + randFloat = cached + }) +} diff --git a/lib/expbackoff/expbackoff_test.go b/lib/expbackoff/expbackoff_test.go new file mode 100644 index 00000000..7716f533 --- /dev/null +++ b/lib/expbackoff/expbackoff_test.go @@ -0,0 +1,137 @@ +//nolint:paralleltest // Parallel tests not supported since test-alias globals are used. +package expbackoff_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/expbackoff" +) + +func TestConfigs(t *testing.T) { + tests := []struct { + name string + config expbackoff.Config + backoffs []string + jitter float64 + }{ + { + name: "default", + config: expbackoff.DefaultConfig, + jitter: 0.5, + backoffs: []string{ + "1s", + "1.6s", + "2.56s", + "4.09s", + "6.55s", + "10.48s", + "16.77s", + "26.84s", + "42.94s", + "1m8.71s", + "1m49.95s", + "2m0s", + "2m0s", + }, + }, + { + name: "default max jitter", + config: expbackoff.DefaultConfig, + jitter: 1, + backoffs: []string{ + "1s", + "1.92s", + "3.07s", + "4.91s", + "7.86s", + "12.58s", + "20.13s", + "32.21s", + "51.53s", + "1m22.46s", + "2m11.94s", + "2m24s", + "2m24s", + }, + }, + { + name: "fast", + config: expbackoff.FastConfig, + jitter: 0.5, + backoffs: []string{ + "100ms", + "160ms", + "250ms", + "400ms", + "650ms", + "1.04s", + "1.67s", + "2.68s", + "4.29s", + "5s", + "5s", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expbackoff.SetRandFloatForT(t, func() float64 { + return test.jitter + }) + + var resps []string + for i := range test.backoffs { + resp := expbackoff.Backoff(test.config, i) + resps = append(resps, resp.Truncate(time.Millisecond*10).String()) + } + require.Equal(t, test.backoffs, resps) + }) + } +} + +func TestNewWithReset(t *testing.T) { + t0 := time.Now() + now := t0 + expbackoff.SetAfterForT(t, func(d time.Duration) <-chan time.Time { + now = now.Add(d) + ch := make(chan time.Time, 1) + ch <- now + + return ch + }) + + ctx, cancel := context.WithCancel(context.Background()) + + backoff, reset := expbackoff.NewWithReset(ctx, expbackoff.With(expbackoff.Config{ + BaseDelay: time.Second, + Multiplier: 2, + Jitter: 0, + MaxDelay: time.Hour, + })) + + elapsed := func(t *testing.T, expect string) { + t.Helper() + require.Equal(t, expect, now.Sub(t0).Truncate(time.Millisecond*10).String()) + } + + backoff() + elapsed(t, "1s") // +1s + backoff() + elapsed(t, "3s") // +2s + backoff() + elapsed(t, "7s") // +4s + backoff() + elapsed(t, "15s") // +8s + + reset() + backoff() + elapsed(t, "16s") // +1s + + cancel() + backoff() + elapsed(t, "16s") // +0s +} diff --git a/lib/fireblocks/README.md b/lib/fireblocks/README.md new file mode 100644 index 00000000..3d440a8b --- /dev/null +++ b/lib/fireblocks/README.md @@ -0,0 +1,14 @@ +# Fireblocks +https://developers.fireblocks.com/reference/api-overview + +## Transactions + +This is going to be our primary API call. We use raw signing for all of our create transaction requests. This is because we cannot guranteee fireblocks is integrated with the chains we are deploying on. + + +## Request Signing: + +https://developers.fireblocks.com/reference/signing-a-request-jwt-structure + +### Note: +A deployment transaction is no different than a normal transaction, we build a transaction payload (the payload just specifies to deploy), we send the txn payload to fireblocks to be signed, we get it back and then send it to the chain. diff --git a/lib/fireblocks/account.go b/lib/fireblocks/account.go new file mode 100644 index 00000000..fe8f466c --- /dev/null +++ b/lib/fireblocks/account.go @@ -0,0 +1,149 @@ +package fireblocks + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/hex" + "net/http" + "strconv" + "text/template" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" +) + +// Accounts returns all the vault accounts from the account cache, populating it if empty. +func (c Client) Accounts(ctx context.Context) (map[common.Address]uint64, error) { + if err := c.cache.MaybePopulate(ctx, c.queryAccounts); err != nil { + return nil, errors.Wrap(err, "populating account cache") + } + + return c.cache.Clone(), nil +} + +// getAccount returns the Fireblocks account ID for the given address from the account cache. +// It populates the cache if the account is not found. +func (c Client) getAccount(ctx context.Context, addr common.Address) (uint64, error) { + accounts, err := c.Accounts(ctx) + if err != nil { + return 0, err + } + + account, ok := accounts[addr] + if !ok { + return 0, errors.New("account not found") + } + + return account, nil +} + +// queryAccounts returns all the vault accounts from the Fireblocks API. +func (c Client) queryAccounts(ctx context.Context) (map[common.Address]uint64, error) { + header, err := c.authHeaders(endpointVaults, nil) + if err != nil { + return nil, err + } + + var resp vaultsResponse + var errResp errorResponse + ok, err := c.jsonHTTP.Send( + ctx, + endpointVaults, + http.MethodGet, + nil, + header, + &resp, + &errResp, + ) + if err != nil { + return nil, err + } else if !ok { + return nil, errors.New("failed to get vaults", "resp_msg", errResp.Message, "resp_code", errResp.Code) + } else if resp.Paging.After != "" { + return nil, errors.New("paging not implemented") + } + + accounts := make(map[common.Address]uint64, len(resp.Accounts)) + for _, account := range resp.Accounts { + id, err := strconv.ParseUint(account.ID, 10, 64) + if err != nil { + return nil, errors.Wrap(err, "parsing account ID") + } + + pubkey, err := c.GetPublicKey(ctx, id) + if err != nil { + return nil, errors.Wrap(err, "getting public key") + } + + accounts[crypto.PubkeyToAddress(*pubkey)] = id + } + + return accounts, nil +} + +// GetPublicKey returns the public key for the given vault account. +func (c Client) GetPublicKey(ctx context.Context, account uint64) (*ecdsa.PublicKey, error) { + endpoint, err := c.pubkeyEndpoint(account) + if err != nil { + return nil, errors.Wrap(err, "getting pubkey endpoint") + } + + headers, err := c.authHeaders(endpoint, nil) + if err != nil { + return nil, err + } + + var res pubkeyResponse + var errRes errorResponse + ok, err := c.jsonHTTP.Send( + ctx, + endpoint, + http.MethodGet, + nil, + headers, + &res, + &errRes, + ) + if err != nil { + return nil, err + } else if !ok { + return nil, errors.New("failed to get public key", "resp_msg", errRes.Message, "resp_code", errRes.Code) + } + + pk, err := hex.DecodeString(res.PublicKey) + if err != nil { + return nil, errors.Wrap(err, "decoding public key") + } + + resp, err := crypto.DecompressPubkey(pk) + if err != nil { + return nil, errors.Wrap(err, "decompressing public key") + } + + return resp, nil +} + +// pubkeyEndpoint returns the public key endpoint by populating the template. +func (c Client) pubkeyEndpoint(account uint64) (string, error) { + tmpl, err := template.New("").Parse(endpointPubkeyTmpl) + if err != nil { + return "", errors.Wrap(err, "parsing pubkey endpoint template") + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, struct { + VaultAccountID string + AssetID string + }{ + VaultAccountID: strconv.FormatUint(account, 10), + AssetID: c.getAssetID(), + }) + if err != nil { + return "", errors.Wrap(err, "executing pubkey endpoint template") + } + + return buf.String(), nil +} diff --git a/lib/fireblocks/client.go b/lib/fireblocks/client.go new file mode 100644 index 00000000..91238cac --- /dev/null +++ b/lib/fireblocks/client.go @@ -0,0 +1,138 @@ +package fireblocks + +import ( + "context" + "crypto/rsa" + "maps" + "sync" + + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/netconf" +) + +const ( + endpointTransactions = "/v1/transactions" + endpointAssets = "/v1/supported_assets" + endpointVaults = "/v1/vault/accounts_paged" + endpointPubkeyTmpl = "/v1/vault/accounts/{{.VaultAccountID}}/{{.AssetID}}/0/0/public_key_info?compressed" + + assetHolesky = "ETH_TEST6" + assetSepolia = "ETH_TEST5" + assetMainnet = "ETH" + + hostProd = "https://api.fireblocks.io" + hostSandbox = "https://sandbox-api.fireblocks.io" +) + +// Client is a JSON HTTP client for the FireBlocks API. +type Client struct { + opts options + apiKey string + network netconf.ID + privateKey *rsa.PrivateKey + jsonHTTP jsonHTTP + cache *accountCache +} + +// New creates a new FireBlocks client. +func New(network netconf.ID, apiKey string, privateKey *rsa.PrivateKey, opts ...func(*options)) (Client, error) { + if apiKey == "" { + return Client{}, errors.New("apiKey is required") + } + if privateKey == nil { + return Client{}, errors.New("privateKey is required") + } + + o := defaultOptions() + for _, opt := range opts { + opt(&o) + } + if err := o.check(); err != nil { + return Client{}, errors.Wrap(err, "options check") + } + + return Client{ + apiKey: apiKey, + privateKey: privateKey, + jsonHTTP: newJSONHTTP(o.Host, apiKey), + opts: o, + cache: newAccountCache(o.TestAccounts), + network: network, + }, nil +} + +// authHeaders returns the authentication headers for the FireBlocks API. +func (c Client) authHeaders(endpoint string, request any) (map[string]string, error) { + token, err := c.token(endpoint, request) + if err != nil { + return nil, errors.Wrap(err, "generating token") + } + + return map[string]string{ + "X-API-KEY": c.apiKey, + "Authorization": "Bearer " + token, + }, nil +} + +func (c Client) getAssetID() string { + switch c.network { + case netconf.Mainnet: + return assetMainnet + default: + return assetHolesky + } +} + +func newAccountCache(init map[common.Address]uint64) *accountCache { + return &accountCache{ + accountsByAddress: init, + } +} + +type accountCache struct { + sync.Mutex + accountsByAddress map[common.Address]uint64 +} + +func (c *accountCache) MaybePopulate(ctx context.Context, fn func(context.Context) (map[common.Address]uint64, error)) error { + c.Lock() + defer c.Unlock() + + if len(c.accountsByAddress) > 0 { + return nil + } + + accounts, err := fn(ctx) + if err != nil { + return err + } + + c.accountsByAddress = accounts + + return nil +} + +func (c *accountCache) Get(addr common.Address) (uint64, bool) { + c.Lock() + defer c.Unlock() + + acc, ok := c.accountsByAddress[addr] + + return acc, ok +} + +func (c *accountCache) Set(addr common.Address, id uint64) { + c.Lock() + defer c.Unlock() + + c.accountsByAddress[addr] = id +} + +func (c *accountCache) Clone() map[common.Address]uint64 { + c.Lock() + defer c.Unlock() + + return maps.Clone(c.accountsByAddress) +} diff --git a/lib/fireblocks/client_internal_test.go b/lib/fireblocks/client_internal_test.go new file mode 100644 index 00000000..3e540345 --- /dev/null +++ b/lib/fireblocks/client_internal_test.go @@ -0,0 +1,28 @@ +package fireblocks + +import ( + "crypto/ecdsa" + "encoding/hex" + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +// TransactionResponse returns a transaction response for testing purposes. +func TransactionResponseForT(t *testing.T, id string, sig [65]byte, pubkey *ecdsa.PublicKey) transaction { + t.Helper() + + return transaction{ + ID: id, + Status: "COMPLETED", + SignedMessages: []signedMessage{{ + PublicKey: hex.EncodeToString(crypto.CompressPubkey(pubkey)), + Signature: signature{ + FullSig: hex.EncodeToString(sig[:64]), + R: hex.EncodeToString(sig[:32]), + S: hex.EncodeToString(sig[32:64]), + V: int(sig[64]), + }, + }}, + } +} diff --git a/lib/fireblocks/client_test.go b/lib/fireblocks/client_test.go new file mode 100644 index 00000000..017dc372 --- /dev/null +++ b/lib/fireblocks/client_test.go @@ -0,0 +1,162 @@ +package fireblocks_test + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/fireblocks" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" + + _ "embed" +) + +//go:embed testdata/test_private_key.pem +var testPrivateKey []byte + +func TestSignOK(t *testing.T) { + t.Parallel() + + ctx := context.Background() + apiKey := uuid.New().String() + txID := uuid.New().String() + + // Create a private key and sign an expected message + privKey, err := crypto.GenerateKey() + require.NoError(t, err) + addr := crypto.PubkeyToAddress(privKey.PublicKey) + digest := crypto.Keccak256([]byte("test")) + expectSig, err := crypto.Sign(digest, privKey) + require.NoError(t, err) + + // Start a test http server that serves the expected responses + var count int + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + if count <= 2 { + // Just return txID and "submitted" on first two attempts + bz, _ := json.Marshal(struct { + ID string `json:"id"` + Status string `json:"status"` + }{ + ID: txID, + Status: "SUBMITTED", + }) + _, _ = w.Write(bz) + + return + } + + // Then return the signed transaction + bz, _ := json.Marshal(fireblocks.TransactionResponseForT(t, txID, [65]byte(expectSig), &privKey.PublicKey)) + _, _ = w.Write(bz) + })) + defer ts.Close() + + client, err := fireblocks.New(netconf.Simnet, apiKey, parseKey(t, testPrivateKey), + fireblocks.WithHost(ts.URL), // Use the test server for all requests. + fireblocks.WithQueryInterval(time.Millisecond), // Fast timeout and interval for testing + fireblocks.WithLogFreqFactor(1), + fireblocks.WithTestAccount(addr, 0), + ) + require.NoError(t, err) + + actualSig, err := client.Sign(ctx, [32]byte(digest), addr) + require.NoError(t, err) + + require.Equal(t, [65]byte(expectSig), actualSig) +} + +func parseKey(t *testing.T, data []byte) *rsa.PrivateKey { + t.Helper() + + p, _ := pem.Decode(data) + k, err := x509.ParsePKCS8PrivateKey(p.Bytes) + require.NoError(t, err) + + return k.(*rsa.PrivateKey) //nolint:forcetypeassert // parseKey is only used for testing +} + +// Populate this or run TestSmoke via terminal +// func init() { +// os.Setenv("FIREBLOCKS_APIKEY", "") +// os.Setenv("FIREBLOCKS_KEY_PATH", "") +//} + +func TestSmoke(t *testing.T) { + t.Parallel() + ctx := context.Background() + + apiKey, ok := os.LookupEnv("FIREBLOCKS_APIKEY") + if !ok { + t.Skip("FIREBLOCKS_APIKEY not set") + } + privKeyFile, ok := os.LookupEnv("FIREBLOCKS_KEY_PATH") + if !ok { + t.Skip("FIREBLOCKS_KEY_PATH not set") + } + privKey, err := os.ReadFile(privKeyFile) + require.NoError(t, err) + + client, err := fireblocks.New(netconf.Staging, apiKey, parseKey(t, privKey)) + require.NoError(t, err) + + addr := common.BytesToAddress(hexutil.MustDecode("0x7a6cF389082dc698285474976d7C75CAdE08ab7e")) + + t.Run("assets", func(t *testing.T) { + t.Parallel() + + assets, err := client.GetSupportedAssets(ctx) + require.NoError(t, err) + + for i, asset := range assets { + if !strings.Contains(asset.ID, "ETH_TEST") { + continue + } + t.Logf("asset %d: %#v", i, asset) + } + }) + + t.Run("accounts", func(t *testing.T) { + t.Parallel() + + accounts, err := client.Accounts(ctx) + tutil.RequireNoError(t, err) + + t.Logf("accounts: %#v", accounts) + }) + + t.Run("pubkey", func(t *testing.T) { + t.Parallel() + + pubkey, err := client.GetPublicKey(ctx, 0) + tutil.RequireNoError(t, err) + + t.Logf("address: %#v", crypto.PubkeyToAddress(*pubkey)) + }) + + t.Run("sign", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + + _, err = client.Sign(ctx, tutil.RandomHash(), addr) + tutil.RequireNoError(t, err) + }) +} diff --git a/lib/fireblocks/config.go b/lib/fireblocks/config.go new file mode 100644 index 00000000..cb067ddf --- /dev/null +++ b/lib/fireblocks/config.go @@ -0,0 +1,93 @@ +package fireblocks + +import ( + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/piplabs/story/lib/errors" +) + +// options houses parameters for altering the behavior of a SimpleTxManager. +type options struct { + // NetworkTimeout is the allowed duration for a single network request. + // This is intended to be used for network requests that can be replayed. + NetworkTimeout time.Duration + + // QueryInterval is the interval at which the FireBlocks client will + // call the get transaction by id to check for confirmations after a txn + // has been sent + QueryInterval time.Duration + + // LogFreqFactor is the frequency at which the FireBlocks client will + // log a warning message if the transaction has not been signed yet + LogFreqFactor int + + // SignNote is a note to include in the sign request + SignNote string + + // Host is the base URL for the FireBlocks API. + Host string + + // TestAccounts overrides dynamic account + TestAccounts map[common.Address]uint64 +} + +// defaultOptions returns a options with default values. +func defaultOptions() options { + return options{ + NetworkTimeout: time.Duration(30) * time.Second, + QueryInterval: time.Second, + LogFreqFactor: 10, + Host: hostProd, + TestAccounts: make(map[common.Address]uint64), + SignNote: "iliad sign note not set", + } +} + +func WithQueryInterval(interval time.Duration) func(*options) { + return func(cfg *options) { + cfg.QueryInterval = interval + } +} + +func WithLogFreqFactor(factor int) func(*options) { + return func(cfg *options) { + cfg.LogFreqFactor = factor + } +} + +func WithTestAccount(addr common.Address, accID uint64) func(*options) { + return func(cfg *options) { + cfg.TestAccounts[addr] = accID + } +} + +func WithHost(host string) func(*options) { + return func(cfg *options) { + cfg.Host = host + } +} + +func WithSignNote(note string) func(*options) { + return func(cfg *options) { + cfg.SignNote = note + } +} + +// check validates the options. +func (c options) check() error { + if c.LogFreqFactor <= 0 { + return errors.New("must provide LogFreqFactor") + } + + if c.NetworkTimeout <= 0 { + return errors.New("must provide NetworkTimeout") + } + + if c.QueryInterval <= 0 { + return errors.New("must provide QueryInterval") + } + + return nil +} diff --git a/lib/fireblocks/gettransaction.go b/lib/fireblocks/gettransaction.go new file mode 100644 index 00000000..2d662e20 --- /dev/null +++ b/lib/fireblocks/gettransaction.go @@ -0,0 +1,37 @@ +package fireblocks + +import ( + "context" + "net/http" + "path/filepath" + + "github.com/piplabs/story/lib/errors" +) + +// getTransactionByID gets a transaction by its ID. +func (c Client) getTransactionByID(ctx context.Context, transactionID string) (transaction, error) { + endpoint := filepath.Join(endpointTransactions, transactionID) + headers, err := c.authHeaders(endpoint, nil) + if err != nil { + return transaction{}, err + } + + var res transaction + var errRes errorResponse + ok, err := c.jsonHTTP.Send( + ctx, + endpoint, + http.MethodGet, + nil, + headers, + &res, + &errRes, + ) + if err != nil { + return transaction{}, err + } else if !ok { + return transaction{}, errors.New("failed to get transaction", "resp_msg", errRes.Message, "resp_code", errRes.Code) + } + + return res, nil +} diff --git a/lib/fireblocks/jsonhttp.go b/lib/fireblocks/jsonhttp.go new file mode 100644 index 00000000..63e38898 --- /dev/null +++ b/lib/fireblocks/jsonhttp.go @@ -0,0 +1,104 @@ +package fireblocks + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + + "github.com/piplabs/story/lib/errors" +) + +// jsonHTTP provides a simple interface for sending JSON HTTP requests. +type jsonHTTP struct { + apiKey string + host string + http http.Client +} + +// newJSONHTTP creates a new jsonHTTP. +func newJSONHTTP(host string, apiKey string) jsonHTTP { + return jsonHTTP{ + host: host, + apiKey: apiKey, + } +} + +// Send sends an JSON HTTP request with the json formatted request as body. +// If the response status code is 2XX, it marshals the response body into the response pointer and returns true. +// Else, it marshals the response body into the errResponse pointer and returns false. +func (c jsonHTTP) Send(ctx context.Context, uri string, httpMethod string, request any, headers map[string]string, response any, errResponse any) (bool, error) { + endpoint, err := url.Parse(c.host + uri) + if err != nil { + return false, errors.Wrap(err, "parse") + } + + // on get requests even will a nil request, we are passing in a non nil request body as the body marshaled to equal `null` + // so we just set it to nil if the request is nil + var reqBytes []byte + if request != nil { + reqBytes, err = json.Marshal(request) + if err != nil { + return false, errors.Wrap(err, "marshaling JSON") + } + } + + req, err := http.NewRequestWithContext( + ctx, + httpMethod, + endpoint.String(), + bytes.NewReader(reqBytes), + ) + if err != nil { + return false, errors.Wrap(err, "new http request") + } + + req.Header = mergeJSONHeaders(headers) + + resp, err := c.http.Do(req) + if err != nil { + return false, errors.Wrap(err, "http do") + } + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, errors.Wrap(err, "read response body") + } + + if resp.StatusCode/http.StatusContinue != 2 { + if errResponse != nil { + err = json.Unmarshal(respBytes, errResponse) + if err != nil { + return false, errors.Wrap(err, "unmarshal error response", "status code", resp.StatusCode, "body", string(respBytes)) + } + } + + return false, nil + } + + if response != nil { + err = json.Unmarshal(respBytes, response) + if err != nil { + return false, errors.Wrap(err, "unmarshal response") + } + } + + return true, nil +} + +// mergeJSONHeaders merges the default JSON headers with the given headers. +func mergeJSONHeaders(m map[string]string) http.Header { + header := http.Header{} + + header.Set("Accept", "application/json") + header.Set("Content-Type", "application/json") + + for k, v := range m { + header.Set(k, v) + } + + return header +} diff --git a/lib/fireblocks/jwt.go b/lib/fireblocks/jwt.go new file mode 100644 index 00000000..f55fad2d --- /dev/null +++ b/lib/fireblocks/jwt.go @@ -0,0 +1,43 @@ +package fireblocks + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + + "github.com/piplabs/story/lib/errors" +) + +const validFor = 29 * time.Second // Must be less than 30sec. + +// token generates a JWT token for the Fireblocks API. +func (c Client) token(uri string, reqBody any) (string, error) { + nonce := uuid.New().String() + + bz, err := json.Marshal(reqBody) + if err != nil { + return "", errors.Wrap(err, "marshaling JSON") + } + reqHash := sha256.Sum256(bz) + + claims := jwt.MapClaims{ + "uri": uri, // uri - The URI part of the request (e.g., /v1/transactions?foo=bar). + "nonce": nonce, // nonce - Unique number or string. Each API request needs to have a different nonce. + "iat": time.Now().Unix(), // iat - The time at which the JWT was issued, in seconds since Epoch. + "exp": time.Now().Add(validFor).Unix(), // exp - The expiration time on and after which the JWT must not be accepted for processing, in seconds since Epoch. (Must be less than iat+30sec.) + "sub": c.apiKey, // sub - The API Key. + "bodyHash": hex.EncodeToString(reqHash[:]), // bodyHash - Hex-encoded SHA-256 hash of the raw HTTP request body. + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + tokenString, err := token.SignedString(c.privateKey) + if err != nil { + return "", errors.Wrap(err, "signing token") + } + + return tokenString, nil +} diff --git a/lib/fireblocks/key.go b/lib/fireblocks/key.go new file mode 100644 index 00000000..5f7f57c2 --- /dev/null +++ b/lib/fireblocks/key.go @@ -0,0 +1,32 @@ +package fireblocks + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/piplabs/story/lib/errors" +) + +// LoadKey loads and returns the RSA256 from disk. +func LoadKey(path string) (*rsa.PrivateKey, error) { + bz, err := os.ReadFile(path) + if err != nil { + return nil, errors.Wrap(err, "load fireblocks key", "path", path) + } + + p, _ := pem.Decode(bz) + k, err := x509.ParsePKCS8PrivateKey(p.Bytes) + if err != nil { + return nil, errors.Wrap(err, "parse fireblocks key") + } + + resp, ok := k.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("invalid fireblocks key type", "type", fmt.Sprintf("%T", resp)) + } + + return resp, nil +} diff --git a/lib/fireblocks/sign.go b/lib/fireblocks/sign.go new file mode 100644 index 00000000..23af4afa --- /dev/null +++ b/lib/fireblocks/sign.go @@ -0,0 +1,153 @@ +package fireblocks + +import ( + "context" + "encoding/hex" + "net/http" + "strconv" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" +) + +func (c Client) createRawSignTransaction(ctx context.Context, account uint64, digest common.Hash) (string, error) { + request := c.newRawSignRequest(account, digest) + headers, err := c.authHeaders(endpointTransactions, request) + if err != nil { + return "", err + } + + var res createTransactionResponse + var errRes errorResponse + ok, err := c.jsonHTTP.Send( + ctx, + endpointTransactions, + http.MethodPost, + request, + headers, + &res, + &errRes, + ) + if err != nil { + return "", err + } else if !ok { + return "", errors.New("failed to create transaction", "resp_msg", errRes.Message, "resp_code", errRes.Code) + } + + return res.ID, nil +} + +// Sign creates a raw sign transaction and waits for it to complete, returning the resulting signature (Ethereum RSV format). +// The signer address is checked against the resulting signed address. +func (c Client) Sign(ctx context.Context, digest common.Hash, signer common.Address) ([65]byte, error) { + account, err := c.getAccount(ctx, signer) + if err != nil { + return [65]byte{}, err + } + + id, err := c.createRawSignTransaction(ctx, account, digest) + if err != nil { + return [65]byte{}, errors.Wrap(err, "create raw sign tx") + } + + // First try immediately. + resp, status, err := c.maybeGetSignature(ctx, id, digest, signer) + if err != nil { + return [65]byte{}, errors.Wrap(err, "get sig") + } else if status.Completed() { + return resp, nil + } + + // Then poll every QueryInterval + queryTicker := time.NewTicker(c.opts.QueryInterval) + defer queryTicker.Stop() + + var attempt int + prevStatus := status + for { + select { + case <-ctx.Done(): + return [65]byte{}, errors.Wrap(ctx.Err(), "timeout waiting", "prev_status", prevStatus) + case <-queryTicker.C: + resp, status, err = c.maybeGetSignature(ctx, id, digest, signer) + if err != nil { + return [65]byte{}, errors.Wrap(err, "get sig", "prev_status", prevStatus) + } else if status.Completed() { + return resp, nil + } + + prevStatus = status + + attempt++ + if attempt%c.opts.LogFreqFactor == 0 { + log.Warn(ctx, "Fireblocks transaction not signed yet (will retry)", nil, + "attempt", attempt, + "status", status, + "id", id, + ) + } + } + } +} + +// maybeGetSignature returns the resulting signature and "completed" status if the transaction has been signed. +// If the transaction is still pending, it returns an empty signature and the "pending" status. +// If the transaction has failed, it returns an empty signature and the "failed" status and an error. +func (c Client) maybeGetSignature(ctx context.Context, txID string, digest common.Hash, signer common.Address) ([65]byte, Status, error) { + tx, err := c.getTransactionByID(ctx, txID) + if err != nil { + return [65]byte{}, "", errors.Wrap(err, "get tx") + } + + if tx.Status.Failed() { + return [65]byte{}, tx.Status, errors.New("transaction failed", "status", tx.Status) + } else if !tx.Status.Completed() { + return [65]byte{}, tx.Status, nil + } + // Get the resulting signature. + sig, err := tx.Sig0() + if err != nil { + return [65]byte{}, "", errors.Wrap(err, "get signature") + } + + // Get the signer pubkey. + pubkey, err := tx.Pubkey0() + if err != nil { + return [65]byte{}, "", err + } + addr := crypto.PubkeyToAddress(*pubkey) + if addr != signer { // Ensure it matches the expected signer. + return [65]byte{}, "", errors.New("signed address mismatch", "expect", signer, "actual", addr) + } + + // Ensure the signature is valid. + if !crypto.VerifySignature(crypto.CompressPubkey(pubkey), digest[:], sig[:64]) { + return [65]byte{}, "", errors.New("signature verification failed") + } + + return sig, tx.Status, nil +} + +// newRawSignRequest creates a new transaction request. +func (c Client) newRawSignRequest(account uint64, digest common.Hash) createTransactionRequest { + return createTransactionRequest{ + Operation: "RAW", + Source: source{ + Type: "VAULT_ACCOUNT", + ID: strconv.FormatUint(account, 10), + }, + AssetID: c.getAssetID(), + ExtraParameters: &extraParameters{ + RawMessageData: rawMessageData{ + Messages: []unsignedRawMessage{{ + Content: hex.EncodeToString(digest[:]), // No 0x prefix, just hex. + }}, + }, + }, + Note: c.opts.SignNote, + } +} diff --git a/lib/fireblocks/supportedassets.go b/lib/fireblocks/supportedassets.go new file mode 100644 index 00000000..97add776 --- /dev/null +++ b/lib/fireblocks/supportedassets.go @@ -0,0 +1,35 @@ +package fireblocks + +import ( + "context" + "net/http" + + "github.com/piplabs/story/lib/errors" +) + +// GetSupportedAssets returns all asset types supported by Fireblocks. +func (c Client) GetSupportedAssets(ctx context.Context) ([]Asset, error) { + headers, err := c.authHeaders(endpointAssets, nil) + if err != nil { + return nil, err + } + + var res []Asset + var errRes errorResponse + ok, err := c.jsonHTTP.Send( + ctx, + endpointAssets, + http.MethodGet, + nil, + headers, + &res, + &errRes, + ) + if err != nil { + return nil, err + } else if !ok { + return nil, errors.New("failed to get supported assets", "resp_msg", errRes.Message, "resp_code", errRes.Code) + } + + return res, nil +} diff --git a/lib/fireblocks/testdata/test_private_key.pem b/lib/fireblocks/testdata/test_private_key.pem new file mode 100644 index 00000000..ce399555 --- /dev/null +++ b/lib/fireblocks/testdata/test_private_key.pem @@ -0,0 +1,52 @@ +-----BEGIN TESTING KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCmH55T2e8fdUaL +iWVL2yI7d/wOu/sxI4nVGoiRMiSMlMZlOEZ4oJY6l2y9N/b8ftwoIpjYO8CBk5au +x2Odgpuz+FJyHppvKakUIeAn4940zoNkRe/iptybIuH5tCBygjs0y1617TlR/c5+ +FF5YRkzsEJrGcLqXzj0hDyrwdplBOv1xz2oHYlvKWWcVMR/qgwoRuj65Ef262t/Q +ELH3+fFLzIIstFTk2co2WaALquOsOB6xGOJSAAr8cIAWe+3MqWM8DOcgBuhABA42 +9IhbBBw0uqTXUv/TGi6tcF29H2buSxAx/Wm6h2PstLd6IJAbWHAa6oTz87H0S6XZ +v42cYoFhHma1OJw4id1oOZMFDTPDbHxgUnr2puSU+Fpxrj9+FWwViKE4j0YatbG9 +cNVpx9xo4NdvOkejWUrqziRorMZTk/zWKz0AkGQzTN3PrX0yy61BoWfznH/NXZ+o +j3PqVtkUs6schoIYvrUcdhTCrlLwGSHhU1VKNGAUlLbNrIYTQNgt2gqvjLEsn4/i +PgS1IsuDHIc7nGjzvKcuR0UeYCDkmBQqKrdhGbdJ1BRohzLdm+woRpjrqmUCbMa5 +VWWldJen0YyAlxNILvXMD117azeduseM1sZeGA9L8MmE12auzNbKr371xzgANSXn +jRuyrblAZKc10kYStrcEmJdfNlzYAwIDAQABAoICABdQBpsD0W/buFuqm2GKzgIE +c4Xp0XVy5EvYnmOp4sEru6/GtvUErDBqwaLIMMv8TY8AU+y8beaBPLsoVg1rn8gg +yAklzExfT0/49QkEDFHizUOMIP7wpbLLsWSmZ4tKRV7CT3c+ZDXiZVECML84lmDm +b6H7feQB2EhEZaU7L4Sc76ZCEkIZBoKeCz5JF46EdyxHs7erE61eO9xqC1+eXsNh +Xr9BS0yWV69K4o/gmnS3p2747AHP6brFWuRM3fFDsB5kPScccQlSyF/j7yK+r+qi +arGg/y+z0+sZAr6gooQ8Wnh5dJXtnBNCxSDJYw/DWHAeiyvk/gsndo3ZONlCZZ9u +bpwBYx3hA2wTa5GUQxFM0KlI7Ftr9Cescf2jN6Ia48C6FcQsepMzD3jaMkLir8Jk +/YD/s5KPzNvwPAyLnf7x574JeWuuxTIPx6b/fHVtboDK6j6XQnzrN2Hy3ngvlEFo +zuGYVvtrz5pJXWGVSjZWG1kc9iXCdHKpmFdPj7XhU0gugTzQ/e5uRIqdOqfNLI37 +fppSuWkWd5uaAg0Zuhd+2L4LG2GhVdfFa1UeHBe/ncFKz1km9Bmjvt04TpxlRnVG +wHxJZKlxpxCZ3AuLNUMP/QazPXO8OIfGOCbwkgFiqRY32mKDUvmEADBBoYpk/wBv +qV99g5gvYFC5Le4QLzOJAoIBAQDcnqnK2tgkISJhsLs2Oj8vEcT7dU9vVnPSxTcC +M0F+8ITukn33K0biUlA+ktcQaF+eeLjfbjkn/H0f2Ajn++ldT56MgAFutZkYvwxJ +2A6PVB3jesauSpe8aqoKMDIj8HSA3+AwH+yU+yA9r5EdUq1S6PscP+5Wj22+thAa +l65CFD77C0RX0lly5zdjQo3Vyca2HYGm/cshFCPRZc66TPjNAHFthbqktKjMQ91H +Hg+Gun2zv8KqeSzMDeHnef4rVaWMIyIBzpu3QdkKPUXMQQxvJ+RW7+MORV9VjE7Z +KVnHa/6x9n+jvtQ0ydHc2n0NOp6BQghTCB2G3w3JJfmPcRSNAoIBAQDAw6mPddoz +UUzANMOYcFtos4EaWfTQE2okSLVAmLY2gtAK6ldTv6X9xl0IiC/DmWqiNZJ/WmVI +glkp6iZhxBSmqov0X9P0M+jdz7CRnbZDFhQWPxSPicurYuPKs52IC08HgIrwErzT +/lh+qRXEqzT8rTdftywj5fE89w52NPHBsMS07VhFsJtU4aY2Yl8y1PHeumXU6h66 +yTvoCLLxJPiLIg9PgvbMF+RiYyomIg75gwfx4zWvIvWdXifQBC88fE7lP2u5gtWL +JUJaMy6LNKHn8YezvwQp0dRecvvoqzoApOuHfsPASHb9cfvcy/BxDXFMJO4QWCi1 +6WLaR835nKLPAoIBAFw7IHSjxNRl3b/FaJ6k/yEoZpdRVaIQHF+y/uo2j10IJCqw +p2SbfQjErLNcI/jCCadwhKkzpUVoMs8LO73v/IF79aZ7JR4pYRWNWQ/N+VhGLDCb +dVAL8x9b4DZeK7gGoE34SfsUfY1S5wmiyiHeHIOazs/ikjsxvwmJh3X2j20klafR +8AJe9/InY2plunHz5tTfxQIQ+8iaaNbzntcXsrPRSZol2/9bX231uR4wHQGQGVj6 +A+HMwsOT0is5Pt7S8WCCl4b13vdf2eKD9xgK4a3emYEWzG985PwYqiXzOYs7RMEV +cgr8ji57aPbRiJHtPbJ/7ob3z5BA07yR2aDz/0kCggEAZDyajHYNLAhHr98AIuGy +NsS5CpnietzNoeaJEfkXL0tgoXxwQqVyzH7827XtmHnLgGP5NO4tosHdWbVflhEf +Z/dhZYb7MY5YthcMyvvGziXJ9jOBHo7Z8Nowd7Rk41x2EQGfve0QcfBd1idYoXch +y47LL6OReW1Vv4z84Szw1fZ0o1yUPVDzxPS9uKP4uvcOevJUh53isuB3nVYArvK5 +p6fjbEY+zaxS33KPdVrajJa9Z+Ptg4/bRqSycTHr2jkN0ZnkC4hkQMH0OfFJb6vD +0VfAaBCZOqHZG/AQ3FFFjRY1P7UEV5WXAn3mKU+HTVJfKug9PxSIvueIttcF3Zm8 +8wKCAQAM43+DnGW1w34jpsTAeOXC5mhIz7J8spU6Uq5bJIheEE2AbX1z+eRVErZX +1WsRNPsNrQfdt/b5IKboBbSYKoGxxRMngJI1eJqyj4LxZrACccS3euAlcU1q+3oN +T10qfQol54KjGld/HVDhzbsZJxzLDqvPlroWgwLdOLDMXhwJYfTnqMEQkaG4Aawr +3P14+Zp/woLiPWw3iZFcL/bt23IOa9YI0NoLhp5MFNXfIuzx2FhVz6BUSeVfQ6Ko +Nx2YZ03g6Kt6B6c43LJx1a/zEPYSZcPERgWOSHlcjmwRfTs6uoN9xt1qs4zEUaKv +Axreud3rJ0rekUp6rI1joG717Wls +-----END TESTING KEY----- \ No newline at end of file diff --git a/lib/fireblocks/types.go b/lib/fireblocks/types.go new file mode 100644 index 00000000..e50c36fb --- /dev/null +++ b/lib/fireblocks/types.go @@ -0,0 +1,303 @@ +package fireblocks + +import ( + "crypto/ecdsa" + "encoding/hex" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" +) + +// Status of a transaction. See https://developers.fireblocks.com/reference/primary-transaction-statuses. +type Status string + +const ( + StatusCompleted Status = "COMPLETED" + StatusFailed Status = "FAILED" + StatusRejected Status = "REJECTED" + StatusBlocked Status = "BLOCKED" + StatusCancelled Status = "CANCELED" + StatusCancelling Status = "CANCELING" + StatusConfirming Status = "CONFIRMING" + StatusBroadcasting Status = "BROADCASTING" + StatusPending3rdParty Status = "PENDING_3RD_PARTY" + StatusPendingSignature Status = "PENDING_SIGNATURE" + StatusQueued Status = "QUEUED" + StatusPendingAuthorization Status = "PENDING_AUTHORIZATION" + StatusPendingAmlScreening Status = "PENDING_AML_SCREENING" + StatusSubmitted Status = "SUBMITTED" +) + +func (s Status) Completed() bool { + return s == StatusCompleted +} + +func (s Status) Failed() bool { + return map[Status]bool{ + StatusFailed: true, + StatusRejected: true, + StatusBlocked: true, + StatusCancelled: true, + StatusCancelling: true, + }[s] +} + +type createTransactionRequest struct { + Operation string `json:"operation"` + Note string `json:"note,omitempty"` + ExternalTxID string `json:"externalTxId,omitempty"` + AssetID string `json:"assetId,omitempty"` + Source source `json:"source"` + Destination *destination `json:"destination,omitempty"` + Destinations []any `json:"destinations,omitempty"` + CustomerRefID string `json:"customerRefId,omitempty"` + Amount string `json:"amountAll,omitempty"` + TreatAsGrossAmount bool `json:"treatAsGrossAmount,omitempty"` + ForceSweep bool `json:"forceSweep,omitempty"` + FeeLevel string `json:"feeLevel,omitempty"` + Fee string `json:"fee,omitempty"` + PriorityFee string `json:"priorityFee,omitempty"` + MaxFee string `json:"maxFee,omitempty"` + GasLimit string `json:"gasLimit,omitempty"` + GasPrice string `json:"gasPrice,omitempty"` + NetworkFee string `json:"networkFee,omitempty"` + ReplaceTxByHash string `json:"replaceTxByHash,omitempty"` + ExtraParameters *extraParameters `json:"extraParameters,omitempty"` +} + +type source struct { + Type string `json:"type"` + SubType string `json:"subType,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + WalletID string `json:"walletId,omitempty"` +} + +type destination struct { + Type string `json:"type"` + SubType string `json:"subType,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + WalletID string `json:"walletId,omitempty"` + OneTimeAddress *oneTimeAddress `json:"oneTimeAddress,omitempty"` +} + +type oneTimeAddress struct { + Address string `json:"address,omitempty"` + Tag string `json:"tag,omitempty"` +} + +type extraParameters struct { + RawMessageData rawMessageData `json:"rawMessageData"` +} + +type rawMessageData struct { + Messages []unsignedRawMessage `json:"messages"` + Algorithm string `json:"algorithm,omitempty"` +} + +type unsignedRawMessage struct { + Content string `json:"content"` +} + +type transaction struct { + ID string `json:"id"` + ExternalTxID string `json:"externalTxId,omitempty"` + Status Status `json:"status"` + SubStatus string `json:"subStatus,omitempty"` + TxHash string `json:"txHash"` + Operation string `json:"operation"` + Note string `json:"note,omitempty"` + AssetID string `json:"assetId,omitempty"` + Source source `json:"source"` + SourceAddress string `json:"sourceAddress,omitempty"` + Tag string `json:"tag,omitempty"` + Destination *destination `json:"destination"` + Destinations []any `json:"destinations,omitempty"` + DestinationAddress string `json:"destinationAddress,omitempty"` + DestinationAddressDescription string `json:"destinationAddressDescription,omitempty"` + DestinationTag string `json:"destinationTag,omitempty"` + ContractCallDecodedData any `json:"contractCallDecodedData,omitempty"` + AmountInfo *amountInfo `json:"amountInfo,omitempty"` + TreatAsGrossAmount bool `json:"treatAsGrossAmount"` + FeeInfo *feeInfo `json:"feeInfo,omitempty"` + FeeCurrency string `json:"feeCurrency,omitempty"` + NetworkRecords []networkRecords `json:"networkRecords,omitempty"` + CreatedAt int `json:"createdAt"` + LastUpdated int `json:"lastUpdated"` + CreatedBy string `json:"createdBy"` + SignedBy []string `json:"signedBy"` + RejectedBy string `json:"rejectedBy"` + AuthorizationInfo *authorizationInfo `json:"authorizationInfo"` + ExchangeTxID string `json:"exchangeTxId,omitempty"` + CustomerRefID string `json:"customerRefId,omitempty"` + AmlScreeningResult *amlScreeningResult `json:"amlScreeningResult,omitempty"` + ExtraParameters map[string]any `json:"extraParameters,omitempty"` + SignedMessages []signedMessage `json:"signedMessages"` + NumOfConfirmations int `json:"numOfConfirmations"` + BlockInfo *blockInfo `json:"blockInfo"` + Index int `json:"index"` + RewardInfo *rewardInfo `json:"rewardInfo,omitempty"` + SystemMessages *systemMessages `json:"systemMessages,omitempty"` + AddressType string `json:"addressType"` +} + +// Pubkey0 returns the public key of the first signed message in the transaction. +func (t transaction) Pubkey0() (*ecdsa.PublicKey, error) { + if len(t.SignedMessages) != 1 { + return nil, errors.New("unexpected number of signed messages", "count", len(t.SignedMessages)) + } + + msg := t.SignedMessages[0] + + pk, err := hex.DecodeString(msg.PublicKey) + if err != nil { + return nil, errors.Wrap(err, "decode public key") + } + + pubkey, err := crypto.DecompressPubkey(pk) + if err != nil { + return nil, errors.Wrap(err, "decompress public key") + } + + return pubkey, nil +} + +// Sig0 returns the signature (Ethereum RSV format) of the first signed message in the transaction. +func (t transaction) Sig0() ([65]byte, error) { + if len(t.SignedMessages) != 1 { + return [65]byte{}, errors.New("unexpected number of signed messages", "count", len(t.SignedMessages)) + } + + msg := t.SignedMessages[0] + + // FullSig field is [R || S] in hex format. + sig, err := hex.DecodeString(msg.Signature.FullSig) + if err != nil { + return [65]byte{}, errors.Wrap(err, "decode signature") + } else if len(sig) != 64 { + return [65]byte{}, errors.New("unexpected signature length", "length", len(sig)) + } + + // V is either 0 or 1 + sig = append(sig, byte(msg.Signature.V)) + + return [65]byte(sig), nil +} + +type amountInfo struct { + Amount string `json:"amount,omitempty"` + RequestedAmount string `json:"requestedAmount,omitempty"` + NetAmount string `json:"netAmount,omitempty"` + AmountUSD string `json:"amountUSD,omitempty"` +} + +type feeInfo struct { + NetworkFee string `json:"networkFee,omitempty"` + ServiceFee string `json:"serviceFee,omitempty"` + GasPrice string `json:"gasPrice,omitempty"` +} + +type amlScreeningResult struct { + Provider string `json:"provider,omitempty"` + Payload any `json:"payload,omitempty"` +} + +type networkRecords struct { + Source source `json:"source"` + Destination *destination `json:"destination,omitempty"` + TxHash string `json:"txHash,omitempty"` + NetworkFee string `json:"networkFee,omitempty"` + AssetID string `json:"assetId,omitempty"` + NetAmount string `json:"netAmount,omitempty"` + IsDropped bool `json:"isDropped,omitempty"` + Type string `json:"type,omitempty"` + DestinationAddress string `json:"destinationAddress,omitempty"` + SourceAddress string `json:"sourceAddress,omitempty"` + AmountUSD string `json:"amountUSD,omitempty"` + Index int `json:"index"` + RewardInfo *rewardInfo `json:"rewardInfo,omitempty"` +} + +type rewardInfo struct { + SrcRewards string `json:"srcRewards"` + DestRewards string `json:"destRewards"` +} + +type signature struct { + FullSig string `json:"fullSig"` + R string `json:"r"` + S string `json:"s"` + V int `json:"v"` +} +type signedMessage struct { + Content string `json:"content"` + Algorithm string `json:"algorithm"` + DerivationPath []int `json:"derivationPath"` + Signature signature `json:"signature"` + PublicKey string `json:"publicKey"` +} +type blockInfo struct { + BlockHeight string `json:"blockHeight"` + BlockHash string `json:"blockHash"` +} + +type users struct { + AdditionalProp string `json:"additionalProp"` +} +type groups struct { + Th int `json:"th"` + Users users `json:"users"` +} +type authorizationInfo struct { + AllowOperatorAsAuthorizer bool `json:"allowOperatorAsAuthorizer"` + Logic string `json:"logic"` + Groups []groups `json:"groups"` +} + +type createTransactionResponse struct { + ID string `json:"id"` + Status string `json:"status"` + SystemMessages *systemMessages `json:"systemMessages,omitempty"` +} + +type systemMessages struct { + Type string `json:"type"` + Message string `json:"message"` +} + +type Asset struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + ContractAddress string `json:"contractAddress,omitempty"` + NativeAsset string `json:"nativeAsset,omitempty"` + Decimals int `json:"decimals,omitempty"` +} + +type pubkeyResponse struct { + Algorithm string `json:"algorithm"` + DerivationPath []int `json:"derivationPath"` + PublicKey string `json:"publicKey"` +} + +type errorResponse struct { + Message string `json:"message"` + Code int `json:"code"` +} + +type vaultsResponse struct { + Accounts []account + Paging paging +} + +type account struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type paging struct { + Before string `json:"before"` + After string `json:"after"` +} diff --git a/lib/forkjoin/forkjoin.go b/lib/forkjoin/forkjoin.go new file mode 100644 index 00000000..7fa42eac --- /dev/null +++ b/lib/forkjoin/forkjoin.go @@ -0,0 +1,264 @@ +// Package forkjoin provides an API for "doing work +// concurrently (fork) and then waiting for the results (join)". +// It is similar to errgroups, except that each "group" doesn't only return an error, but also a result. +// +// This package was copied from Obol's Charon repo. +// See https://github.com/ObolNetwork/charon/blob/main/app/forkjoin/forkjoin.go. +package forkjoin + +import ( + "context" + "sync" + + "github.com/piplabs/story/lib/errors" +) + +const ( + defaultWorkers = 8 + defaultInputBuf = 100 + defaultFailFast = true + defaultWaitOnCancel = false +) + +// Fork function enqueues the input to be processed asynchronously. +// Note Fork may block temporarily while the input buffer is full, see WithInputBuffer. +// Note Fork will panic if called after Join. +type Fork[I any] func(I) + +// Join function closes the input queue and returns the result channel. +// Note Fork will panic if called after Join. +// Note Join must only be called once, otherwise panics. +type Join[I, O any] func() Results[I, O] + +// Work defines the work function signature workers will call. +type Work[I, O any] func(ctx context.Context, input I) (output O, err error) + +// Results contains enqueued results. +type Results[I, O any] <-chan Result[I, O] + +// Result contains the input and resulting output from the work function. +type Result[I, O any] struct { + Input I + Output O + Err error +} + +// Flatten blocks and returns all the outputs when all completed and +// the first "real error". +// +// A real error is the error that triggered the fail fast, all subsequent +// results will contain context canceled errors. +func (r Results[I, O]) Flatten() ([]O, error) { + //nolint:prealloc // We don't know the length of the results. + var ( + ctxErr error + otherErr error + resp []O + ) + for result := range r { + resp = append(resp, result.Output) + + if result.Err == nil { + continue + } + + if errors.Is(result.Err, context.Canceled) && ctxErr == nil { + ctxErr = result.Err + } + if !errors.Is(result.Err, context.Canceled) && otherErr == nil { + otherErr = result.Err + } + } + + if otherErr != nil { + return resp, otherErr + } else if ctxErr != nil { + return resp, ctxErr + } + + return resp, nil +} + +type options struct { + inputBuf int + workers int + failFast bool + waitOnCancel bool +} + +type Option func(*options) + +// WithWaitOnCancel returns an option configuring a forkjoin to wait for all workers to return when canceling. +// The default behavior just cancels the worker context and closes the output channel without waiting +// for the workers to return. +func WithWaitOnCancel() Option { + return func(o *options) { + o.waitOnCancel = true + } +} + +// WithWorkers returns an option configuring a forkjoin with w number of workers. +func WithWorkers(w int) Option { + return func(o *options) { + o.workers = w + } +} + +// WithInputBuffer returns an option configuring a forkjoin with an input buffer +// of length i overriding the default of 100. +// Useful to prevent temporary blocking during calls to Fork if enqueuing more than 100 inputs. +func WithInputBuffer(i int) Option { + return func(o *options) { + o.inputBuf = i + } +} + +// WithoutFailFast returns an option configuring a forkjoin to not stop execution on any error. +func WithoutFailFast() Option { + return func(o *options) { + o.failFast = false + } +} + +// New returns fork, join, and cancel functions with generic input type I and output type O. +// It provides an API for "doing work concurrently (fork) and then waiting for the results (join)". +// +// It fails fast by default, stopping execution on any error. All active work function contexts +// are canceled and no further inputs are executed with remaining result errors being set +// to context canceled. See WithoutFailFast. +// +// Usage: +// +// var workFunc := func(ctx context.Context, input MyInput) (MyResult, error) { +// ... do work +// return result, nil +// } +// +// fork, join, cancel := forkjoin.New[MyInput,MyResult](ctx, workFunc) +// defer cancel() // Release any remaining resources. +// +// for _, in := range inputs { +// fork(in) // Note that calling fork AFTER join panics! +// } +// +// resultChan := join() +// +// // Either read results from the channel as they appear +// for result := range resultChan { ... } +// +// // Or block until all results are complete and flatten +// results, firstErr := resultChan.Flatten() +func New[I, O any](rootCtx context.Context, work Work[I, O], opts ...Option) (Fork[I], Join[I, O], context.CancelFunc) { + options := options{ + workers: defaultWorkers, + inputBuf: defaultInputBuf, + failFast: defaultFailFast, + waitOnCancel: defaultWaitOnCancel, + } + + for _, opt := range opts { + opt(&options) + } + + var ( + wg sync.WaitGroup + zero O + input = make(chan I, options.inputBuf) + results = make(chan Result[I, O]) + dropOutput = make(chan struct{}) + done = make(chan struct{}) + ) + + workCtx, cancelWorkers := context.WithCancel(rootCtx) + + // enqueue result asynchronously since results channel is unbuffered/blocking. + enqueue := func(in I, out O, err error) { + go func() { + select { + case results <- Result[I, O]{ + Input: in, + Output: out, + Err: err, + }: + case <-dropOutput: + // Dropping output. + } + wg.Done() + }() + } + + for range options.workers { // Start workers + go func() { + for in := range input { // Process all inputs (channel closed on Join) + if workCtx.Err() != nil { // Skip work if failed fast + enqueue(in, zero, workCtx.Err()) + continue + } + + out, err := work(workCtx, in) + if options.failFast && err != nil { // Maybe fail fast + cancelWorkers() + } + + enqueue(in, out, err) + } + }() + } + + // Fork enqueues inputs, keeping track of how many was enqueued. + fork := func(i I) { + var added bool + defer func() { + // Handle panic use-case as well as rootCtx done. + if !added { + wg.Done() + } + }() + + wg.Add(1) + select { + case input <- i: + added = true + case <-rootCtx.Done(): + } + } + + // Join returns the results channel that will contain all the results in the future. + // It also closes the input queue (causing subsequent calls Fork to panic) + // It also starts a shutdown goroutine that closes the results channel when processing completed + join := func() Results[I, O] { + close(input) + + go func() { + // Auto close result channel when done + wg.Wait() + close(results) + close(done) + }() + + return results + } + + // cancel, drop remaining results and cancel workers if not done already. + cancel := func() { + close(dropOutput) + cancelWorkers() + if options.waitOnCancel { + <-done + } + } + + return fork, join, cancel +} + +// NewWithInputs is a convenience function that calls New and then forks all the inputs +// returning the join result and a cancel function. +func NewWithInputs[I, O any](ctx context.Context, work Work[I, O], inputs []I, opts ...Option, +) (Results[I, O], context.CancelFunc) { + fork, join, cancel := New[I, O](ctx, work, opts...) + for _, input := range inputs { + fork(input) + } + + return join(), cancel +} diff --git a/lib/forkjoin/forkjoin_test.go b/lib/forkjoin/forkjoin_test.go new file mode 100644 index 00000000..8d725f50 --- /dev/null +++ b/lib/forkjoin/forkjoin_test.go @@ -0,0 +1,142 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +//nolint:paralleltest // Goleak doesn't support parrellel tests +package forkjoin_test + +import ( + "context" + "sort" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/forkjoin" + + "go.uber.org/goleak" +) + +func TestForkJoin(t *testing.T) { + ctx := context.Background() + + const n = 100 + testErr := errors.New("test error") + + tests := []struct { + name string + work forkjoin.Work[int, int] + failfast bool + expectedErr error + allOutput bool + }{ + { + name: "happy", + expectedErr: nil, + work: func(_ context.Context, i int) (int, error) { return i, nil }, + allOutput: true, + }, + { + name: "first error fast fail", + expectedErr: testErr, + failfast: true, + work: func(ctx context.Context, i int) (int, error) { + if i == 0 { + return 0, testErr + } + if i > n/2 { + require.Fail(t, "not failed fast") + } + <-ctx.Done() // This will hang if not failing fast + + return 0, ctx.Err() + }, + }, + { + name: "all error no fast fail", + allOutput: true, + expectedErr: testErr, + work: func(_ context.Context, i int) (int, error) { + return i, testErr + }, + }, + { + name: "all context cancel", + expectedErr: context.Canceled, + failfast: true, + work: func(_ context.Context, i int) (int, error) { + if i < n/2 { + return 0, context.Canceled + } + + return 0, nil + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + defer goleak.VerifyNone(t) + + var opts []forkjoin.Option + if !test.failfast { + opts = append(opts, forkjoin.WithoutFailFast()) + } + + fork, join, cancel := forkjoin.New[int, int](ctx, test.work, opts...) + defer cancel() + + var allOutput []int + for i := range n { + fork(i) + allOutput = append(allOutput, i) + } + + resp, err := join().Flatten() + require.Len(t, resp, n) + + if test.expectedErr != nil { + require.Equal(t, test.expectedErr, err) + } else { + require.NoError(t, err) + } + + if test.allOutput { + sort.Ints(resp) + require.Equal(t, allOutput, resp) + } + }) + } +} + +func TestPanic(t *testing.T) { + defer goleak.VerifyNone(t) + + fork, join, cancel := forkjoin.New[int, int](context.Background(), nil, forkjoin.WithWaitOnCancel()) + join() + cancel() + + // Calling fork after join panics + require.Panics(t, func() { + fork(0) + }) + + // Calling join again panics + require.Panics(t, func() { + join() + }) +} + +func TestLeak(t *testing.T) { + defer goleak.VerifyNone(t) + + fork, join, cancel := forkjoin.New[int, int]( + context.Background(), + func(ctx context.Context, i int) (int, error) { return i, nil }, + forkjoin.WithWaitOnCancel(), + ) + fork(1) + fork(2) + results := join() + <-results // Read 1 or 2 + cancel() // Fails if not called. +} diff --git a/lib/k1util/k1util.go b/lib/k1util/k1util.go new file mode 100644 index 00000000..4c8ab5a0 --- /dev/null +++ b/lib/k1util/k1util.go @@ -0,0 +1,179 @@ +// Package k1util provides functions to sign and verify Ethereum RSV style signatures. +package k1util + +import ( + stdecdsa "crypto/ecdsa" + + "github.com/cometbft/cometbft/crypto" + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + cryptopb "github.com/cometbft/cometbft/proto/tendermint/crypto" + cosmosk1 "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cosmoscrypto "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" +) + +// privKeyLen is the length of a secp256k1 private key. +const privKeyLen = 32 + +// pubkeyCompressedLen is the length of a secp256k1 compressed public key. +const pubkeyCompressedLen = 33 + +// pubkeyUncompressedLen is the length of a secp256k1 uncompressed public key. +const pubkeyUncompressedLen = 65 + +// Sign returns a signature from input data. +// +// The produced signature is 65 bytes in the [R || S || V] format where V is 27 or 28. +func Sign(key crypto.PrivKey, input [32]byte) ([65]byte, error) { + bz := key.Bytes() + if len(bz) != privKeyLen { + return [65]byte{}, errors.New("invalid private key length") + } + + sig := ecdsa.SignCompact(secp256k1.PrivKeyFromBytes(bz), input[:], false) + + // Convert signature from "compact" into "Ethereum R S V" format. + return [65]byte(append(sig[1:], sig[0])), nil +} + +// Verify returns whether the 65 byte signature is valid for the provided hash +// and Ethereum address. +// +// Note the signature MUST be 65 bytes in the Ethereum [R || S || V] format. +func Verify(address common.Address, hash [32]byte, sig [65]byte) (bool, error) { + // Adjust V from Ethereum 27/28 to secp256k1 0/1 + const vIdx = 64 + if v := sig[vIdx]; v != 27 && v != 28 { + return false, errors.New("invalid recovery id (V) format, must be 27 or 28") + } + sig[vIdx] -= 27 + + pubkey, err := ethcrypto.SigToPub(hash[:], sig[:]) + if err != nil { + return false, errors.Wrap(err, "recover public key") + } + + actual := ethcrypto.PubkeyToAddress(*pubkey) + + return actual == address, nil +} + +// PubKeyToAddress returns the Ethereum address for the given k1 public key. +func PubKeyToAddress(pubkey crypto.PubKey) (common.Address, error) { + pubkeyBytes := pubkey.Bytes() + if len(pubkeyBytes) != pubkeyCompressedLen { + return common.Address{}, errors.New("invalid pubkey length", "length", len(pubkeyBytes)) + } + + ethPubKey, err := ethcrypto.DecompressPubkey(pubkeyBytes) + if err != nil { + return common.Address{}, errors.Wrap(err, "decompress pubkey") + } + + return ethcrypto.PubkeyToAddress(*ethPubKey), nil +} + +func StdPrivKeyToComet(privkey *stdecdsa.PrivateKey) (crypto.PrivKey, error) { + bz := ethcrypto.FromECDSA(privkey) + if len(bz) != privKeyLen { + return nil, errors.New("invalid private key length") + } + + return k1.PrivKey(bz), nil +} + +func StdPrivKeyFromComet(privkey crypto.PrivKey) (*stdecdsa.PrivateKey, error) { + bz := privkey.Bytes() + if len(bz) != privKeyLen { + return nil, errors.New("invalid private key length") + } + + resp, err := ethcrypto.ToECDSA(bz) + if err != nil { + return nil, errors.Wrap(err, "convert to ECDSA") + } + + return resp, nil +} + +func StdPubKeyToCosmos(pubkey *stdecdsa.PublicKey) (cosmoscrypto.PubKey, error) { + return PubKeyBytesToCosmos(ethcrypto.CompressPubkey(pubkey)) +} + +func PubKeyToCosmos(pubkey crypto.PubKey) (cosmoscrypto.PubKey, error) { + return PubKeyBytesToCosmos(pubkey.Bytes()) +} + +func PubKeyBytesToCosmos(pubkey []byte) (cosmoscrypto.PubKey, error) { + if len(pubkey) != pubkeyCompressedLen { + return nil, errors.New("invalid pubkey length", "length", len(pubkey)) + } + + return &cosmosk1.PubKey{ + Key: pubkey, + }, nil +} + +func PBPubKeyFromBytes(pubkey []byte) (cryptopb.PublicKey, error) { + if len(pubkey) != pubkeyCompressedLen { + return cryptopb.PublicKey{}, errors.New("invalid pubkey length", "length", len(pubkey)) + } + + return cryptopb.PublicKey{Sum: &cryptopb.PublicKey_Secp256K1{Secp256K1: pubkey}}, nil +} + +// PubKeyPBToAddress returns the Ethereum address for the given k1 public key. +func PubKeyPBToAddress(pubkey cryptopb.PublicKey) (common.Address, error) { + pubkeyBytes := pubkey.GetSecp256K1() + if len(pubkeyBytes) != pubkeyCompressedLen { + return common.Address{}, errors.New("invalid pubkey length", "length", len(pubkeyBytes)) + } + + ethPubKey, err := ethcrypto.DecompressPubkey(pubkeyBytes) + if err != nil { + return common.Address{}, errors.Wrap(err, "decompress pubkey") + } + + return ethcrypto.PubkeyToAddress(*ethPubKey), nil +} + +// PubKeyToBytes64 returns the 64 byte uncompressed version of the public key, by removing the prefix (0x04 for uncompressed keys). +func PubKeyToBytes64(pubkey *stdecdsa.PublicKey) []byte { + return ethcrypto.FromECDSAPub(pubkey)[1:] +} + +// PubKeyFromBytes64 returns the public key from the 64 byte uncompressed version. +// It adds the prefix (0x04 for uncompressed keys) to the input bytes. +func PubKeyFromBytes64(pubkey []byte) (*stdecdsa.PublicKey, error) { + if len(pubkey) != pubkeyUncompressedLen-1 { + return nil, errors.New("invalid pubkey length", "length", len(pubkey)) + } + + const prefix = 0x04 + + // TODO: Fix possible panics if the pubkey is not on the curve + resp, err := ethcrypto.UnmarshalPubkey(append([]byte{prefix}, pubkey...)) + if err != nil { + return nil, errors.Wrap(err, "unmarshal pubkey") + } + + return resp, nil +} + +// CosmosPubkeyToEVMAddress converts a 33-byte Cosmos pubkey to a 20-byte EVM address. It decompresses the pubkey, +// applies keccak256, then take the last 20 bytes to get the corresponding EVM address. +func CosmosPubkeyToEVMAddress(pubkeyCmp []byte) (addr common.Address, err error) { + key, err := ethcrypto.DecompressPubkey(pubkeyCmp) + if err != nil { + return addr, err + } + + addr = ethcrypto.PubkeyToAddress(*key) + + return addr, nil +} diff --git a/lib/k1util/k1util_test.go b/lib/k1util/k1util_test.go new file mode 100644 index 00000000..700ee367 --- /dev/null +++ b/lib/k1util/k1util_test.go @@ -0,0 +1,170 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package k1util_test + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "path/filepath" + "testing" + + k1 "github.com/cometbft/cometbft/crypto/secp256k1" + "github.com/cometbft/cometbft/privval" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + cmttime "github.com/cometbft/cometbft/types/time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/tutil" +) + +//nolint:lll // No wrap +const ( + privKey1 = "41d3ff12045b73c870529fe44f70dca2745bafbe1698ffc3c8759eef3cfbaee1" + pubKey1 = "02bc8e7cdb50e0ffd52a54faf984d6ac8fe5ee6856d38a5f8acd9bd33fc9c7d50d" + digest1 = "52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649" // 32 byte digest. + sig1 = "e08097bed6dc40d70aa0076f9d8250057566cdf40c652b3785ad9c06b1e38d584f8f331bf46f68e3737823a3bda905e90ca96735d510a6934b215753c09acec21c" + addr1 = "0xF88D5892faF084DCF4143566d9C9b3F047c153Ca" +) + +func TestK1Util(t *testing.T) { + t.Parallel() + key := k1.PrivKey(fromHex(t, privKey1)) + + require.Equal(t, fromHex(t, privKey1), key.Bytes()) + require.Equal(t, fromHex(t, pubKey1), key.PubKey().Bytes()) + + digest := fromHex(t, digest1) + + sig, err := k1util.Sign(key, [32]byte(digest)) + require.NoError(t, err) + require.EqualValues(t, fromHex(t, sig1), sig[:]) + + addr, err := k1util.PubKeyToAddress(key.PubKey()) + require.NoError(t, err) + require.Equal(t, addr1, addr.Hex()) + + ok, err := k1util.Verify(addr, [32]byte(digest), sig) + require.NoError(t, err) + require.True(t, ok) +} + +func TestRandom(t *testing.T) { + t.Parallel() + key := k1.GenPrivKey() + + var digest [32]byte + _, _ = rand.Read(digest[:]) + + sig, err := k1util.Sign(key, digest) + require.NoError(t, err) + + addr, err := k1util.PubKeyToAddress(key.PubKey()) + require.NoError(t, err) + + ok, err := k1util.Verify(addr, digest, sig) + require.NoError(t, err) + require.True(t, ok) +} + +func TestCosmosPbukey(t *testing.T) { + t.Parallel() + + priv, err := crypto.GenerateKey() + require.NoError(t, err) + + cosmosPub, err := k1util.StdPubKeyToCosmos(&priv.PublicKey) + require.NoError(t, err) + + require.NotPanics(t, func() { + cosmosPub.Address() + }) +} + +// TestCometBFT tests that CometBFT and k1util can produce the same signatures. +// This ensures that we can use web3signer to sign votes and proposals. +func TestCometBFT(t *testing.T) { + t.Parallel() + dir := t.TempDir() + + const chainID = "test" + + // Create a key + key := k1.GenPrivKey() + pv := privval.NewFilePV(key, filepath.Join(dir, "key"), filepath.Join(dir, "state")) + + // Create a vote + block := cmttypes.BlockID{Hash: tutil.RandomBytes(32)} + vote := newVote(pv.Key.Address, 1, 2, 3, cmtproto.PrecommitType, block, []byte("vote extension")) + votePB1 := vote.ToProto() + votePB2 := vote.ToProto() + + // Sign it with CometBFT privval. + err := pv.SignVote(chainID, votePB1) + require.NoError(t, err) + + require.Nil(t, votePB2.Signature) // Ensure no pointer shenanigans. + + // Sign it with k1util. + signVote(t, key, chainID, votePB2) + + require.Equal(t, votePB1.Signature, votePB2.Signature) + require.Equal(t, votePB1.ExtensionSignature, votePB2.ExtensionSignature) +} + +func TestPubkey64(t *testing.T) { + t.Parallel() + + priv, err := crypto.GenerateKey() + require.NoError(t, err) + + bz64 := k1util.PubKeyToBytes64(&priv.PublicKey) + require.Len(t, bz64, 64) + + pub, err := k1util.PubKeyFromBytes64(bz64) + require.NoError(t, err) + + require.True(t, pub.Equal(&priv.PublicKey)) +} + +func signVote(t *testing.T, key k1.PrivKey, chainID string, vote *cmtproto.Vote) { + t.Helper() + + sigBytes := cmttypes.VoteSignBytes(chainID, vote) + hashed := sha256.Sum256(sigBytes) + sig, err := k1util.Sign(key, hashed) + require.NoError(t, err) + + extSignBytes := cmttypes.VoteExtensionSignBytes(chainID, vote) + extHashed := sha256.Sum256(extSignBytes) + extSig, err := k1util.Sign(key, extHashed) + require.NoError(t, err) + + vote.Signature = sig[:64] // CometBFT drops recovery id. + vote.ExtensionSignature = extSig[:64] // CometBFT drops recovery id. +} + +func fromHex(t *testing.T, hexStr string) []byte { + t.Helper() + b, err := hex.DecodeString(hexStr) + require.NoError(t, err) + + return b +} + +func newVote(addr cmttypes.Address, idx int32, height int64, round int32, + typ cmtproto.SignedMsgType, blockID cmttypes.BlockID, extension []byte) *cmttypes.Vote { + return &cmttypes.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: height, + Round: round, + Type: typ, + Timestamp: cmttime.Now(), + BlockID: blockID, + Extension: extension, + } +} diff --git a/lib/log/attrs.go b/lib/log/attrs.go new file mode 100644 index 00000000..78069902 --- /dev/null +++ b/lib/log/attrs.go @@ -0,0 +1,19 @@ +package log + +import ( + "encoding/hex" + "log/slog" +) + +// Hex7 is a convenience function for a hex-encoded log attribute limited to first 7 chars for brevity. +// Note this is NOT 0x prefixed. +func Hex7(key string, value []byte) slog.Attr { + h := hex.EncodeToString(value) + + const maxLen = 7 + if len(h) > maxLen { + h = h[:maxLen] + } + + return slog.String(key, h) +} diff --git a/lib/log/config.go b/lib/log/config.go new file mode 100644 index 00000000..fe04090f --- /dev/null +++ b/lib/log/config.go @@ -0,0 +1,123 @@ +package log + +import ( + "log/slog" + "strings" + + "github.com/muesli/termenv" + "github.com/spf13/pflag" + + "github.com/piplabs/story/lib/errors" +) + +const ( + FormatCLI = "cli" + FormatConsole = "console" + FormatJSON = "json" + FormatLogfmt = "logfmt" + + ColorForce = "force" + ColorDisable = "disable" + ColorAuto = "auto" +) + +//nolint:gochecknoglobals // Static mapping. +var ( + levelDebug = strings.ToLower(slog.LevelDebug.String()) + levelInfo = strings.ToLower(slog.LevelInfo.String()) + levelWarn = strings.ToLower(slog.LevelWarn.String()) + levelError = strings.ToLower(slog.LevelError.String()) + + levels = []string{levelDebug, levelInfo, levelWarn, levelError} +) + +//nolint:gochecknoglobals // Static mapping. +var loggerFuncs = map[string]func(...func(*options)) *slog.Logger{ + FormatConsole: newConsoleLogger, + FormatJSON: newJSONLogger, + FormatLogfmt: newLogfmtLogger, + FormatCLI: newCLILogger, +} + +//nolint:gochecknoglobals // Static mapping. +var colors = map[string]termenv.Profile{ + ColorForce: termenv.TrueColor, + ColorDisable: termenv.Ascii, + ColorAuto: termenv.ColorProfile(), +} + +// DefaultConfig returns a default config. +func DefaultConfig() Config { + return Config{ + Level: levelInfo, + Color: ColorAuto, + Format: FormatConsole, + } +} + +type Config struct { + Level string + Color string + Format string +} + +func (c Config) color() (termenv.Profile, error) { + color := c.Color + if c.Color == "" { + color = ColorAuto + } + resp, ok := colors[color] + if !ok { + return 0, errors.New("invalid color", "color", c.Color) + } + + return resp, nil +} + +func (c Config) level() (slog.Level, error) { + var level slog.Level + if err := level.UnmarshalText([]byte(c.Level)); err != nil { + return slog.Level(0), errors.Wrap(err, "parse log level") + } + + return level, nil +} + +func (c Config) loggerFunc() (func(...func(*options)) *slog.Logger, error) { + f, ok := loggerFuncs[c.Format] + if !ok { + return nil, errors.New("invalid format", "format", c.Format) + } + + return f, nil +} + +// make returns a new global as per the config. +func (c Config) make() (*slog.Logger, error) { + level, err := c.level() + if err != nil { + return nil, errors.Wrap(err, "parse log level") + } + + color, err := c.color() + if err != nil { + return nil, errors.Wrap(err, "parse log color") + } + + loggerFunc, err := c.loggerFunc() + if err != nil { + return nil, errors.Wrap(err, "parse log format") + } + + return loggerFunc(func(o *options) { + o.Level = level + o.Color = color + }), nil +} + +// BindFlags binds the standard flags to provide logging config at runtime. +func BindFlags(flags *pflag.FlagSet, cfg *Config) { + flags.StringVar(&cfg.Level, "log-level", cfg.Level, "Log level; debug, info, warn, error") + flags.StringVar(&cfg.Color, "log-color", cfg.Color, "Log color (only applicable to console format); auto, force, disable") + flags.StringVar(&cfg.Format, "log-format", cfg.Format, "Log format; console, json") +} diff --git a/lib/log/log.go b/lib/log/log.go new file mode 100644 index 00000000..b09f01f2 --- /dev/null +++ b/lib/log/log.go @@ -0,0 +1,159 @@ +// Package log provides a wrapper around the slog package (might change implementation later). +// It provides an opinionated interface for logging structured data always with context. +package log + +import ( + "context" + "log/slog" + "runtime" + "strings" + "time" + + pkgerrors "github.com/pkg/errors" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type attrsKey struct{} +type skipKey struct{} + +// WithCtx returns a copy of the context with which the logging attributes are associated. +// Usage: +// +// ctx := log.WithCtx(ctx, "height", 1234) +// ... +// log.Info(ctx, "Height processed") // Will contain attribute: height=1234 +func WithCtx(ctx context.Context, attrs ...any) context.Context { + return context.WithValue(ctx, attrsKey{}, mergeAttrs(ctx, attrs)) +} + +// Debug logs the message and attributes at default level. +func Debug(ctx context.Context, msg string, attrs ...any) { + log(ctx, slog.LevelDebug, msg, mergeAttrs(ctx, attrs)...) +} + +// Info logs the message and attributes at info level. +func Info(ctx context.Context, msg string, attrs ...any) { + log(ctx, slog.LevelInfo, msg, mergeAttrs(ctx, attrs)...) +} + +// Warn logs the message and error and attributes at warning level. +// If err is nil, it will not be logged. +func Warn(ctx context.Context, msg string, err error, attrs ...any) { + if err != nil { + attrs = append(attrs, slog.String("err", err.Error())) + attrs = append(attrs, errAttrs(err)...) + } + + log(ctx, slog.LevelWarn, msg, mergeAttrs(ctx, attrs)...) +} + +// Error logs the message and error and arguments at error level. +// If err is nil, it will not be logged. +func Error(ctx context.Context, msg string, err error, attrs ...any) { + if err != nil { + attrs = append(attrs, slog.String("err", err.Error())) + attrs = append(attrs, errAttrs(err)...) + } + log(ctx, slog.LevelError, msg, mergeAttrs(ctx, attrs)...) +} + +// log is the low-level logging method for methods that take ...any. +// It must always be called directly by an exported logging method +// or function, because it uses a fixed call depth to obtain the pc. +// +// Copied from stdlib since we wrap slog and the source/caller is incorrect otherwise. +// See https://github.com/golang/go/blob/master/src/log/slog/logger.go#L241 +func log(ctx context.Context, level slog.Level, msg string, attrs ...any) { + logTotal.WithLabelValues(strings.ToLower(level.String())).Inc() + + logger := getLogger(ctx) + + if !logger.Enabled(ctx, level) { + return + } + + // Default skip [runtime.Callers, this function, this function's caller] + var skip = 3 + if v, ok := ctx.Value(skipKey{}).(int); ok { + skip = v + } + + var pcs [1]uintptr + runtime.Callers(skip, pcs[:]) + + r := slog.NewRecord(time.Now(), level, msg, pcs[0]) + r.Add(attrs...) + + // Build trace event + traceAttrs := []attribute.KeyValue{attribute.String("msg", msg)} + r.Attrs(func(attr slog.Attr) bool { + traceAttrs = append(traceAttrs, attribute.Stringer(attr.Key, attr.Value)) + return true + }) + trace.SpanFromContext(ctx).AddEvent( + "log."+level.String(), + trace.WithAttributes(traceAttrs...), + ) + + _ = logger.Handler().Handle(ctx, r) +} + +// errFields is similar to z.Err and returns the structured error fields and +// stack trace but without the error message. It avoids duplication of the error message +// since it is used as the main log message in Error above. +func errAttrs(err error) []any { + type stackTracer interface { + StackTrace() pkgerrors.StackTrace + } + + type iliadErr interface { + Attrs() []any + } + + var attrs []any + var stack pkgerrors.StackTrace + + // Go up the cause chain (from the outermost error to the innermost error) + for { + // Use the first encountered iliadErr's attributes. + if len(attrs) == 0 { + if serr, ok := err.(iliadErr); ok { + attrs = serr.Attrs() + } + } + + // Use the last encountered stack trace. + if serr, ok := err.(stackTracer); ok { + stack = serr.StackTrace() + } + + if cause := pkgerrors.Unwrap(err); cause != nil { + err = cause + continue // Continue up the cause chain. + } + + // Root cause reached, break the loop. + + if len(stack) > 0 { + attrs = append(attrs, slog.Any("stacktrace", stack)) + } + + return attrs + } +} + +// mergeAttrs returns the attributes from the context merged with the provided attributes. +func mergeAttrs(ctx context.Context, attrs []any) []any { + resp, _ := ctx.Value(attrsKey{}).([]any) //nolint:revive // We know the type. + resp = append(resp, attrs...) + + return resp +} + +// WithSkip returns a copy of the context with the skip value set. +// This is used to control the number of stack frames to skip when `source` is calculated. +func WithSkip(ctx context.Context, skip int) context.Context { + return context.WithValue(ctx, skipKey{}, skip) +} diff --git a/lib/log/log_internal_test.go b/lib/log/log_internal_test.go new file mode 100644 index 00000000..863cd74e --- /dev/null +++ b/lib/log/log_internal_test.go @@ -0,0 +1,29 @@ +package log + +import ( + "io" + "log/slog" + + "github.com/muesli/termenv" +) + +// LoggersForT returns a map of loggers for testing. +func LoggersForT() map[string]func(io.Writer) *slog.Logger { + testOpts := func(w io.Writer) func(*options) { + return func(o *options) { + o.Writer = w + o.Test = true + o.Level = slog.LevelDebug + o.Color = termenv.Ascii + } + } + + resp := make(map[string]func(w io.Writer) *slog.Logger) + for name, fn := range loggerFuncs { + resp[name] = func(w io.Writer) *slog.Logger { + return fn(testOpts(w)) + } + } + + return resp +} diff --git a/lib/log/log_test.go b/lib/log/log_test.go new file mode 100644 index 00000000..4d7e0500 --- /dev/null +++ b/lib/log/log_test.go @@ -0,0 +1,73 @@ +package log_test + +import ( + "bytes" + "context" + "io" + "os" + "testing" + + pkgerrors "github.com/pkg/errors" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test . -golden -clean + +// TestSimpleLogs tests the simple logs. +func TestSimpleLogs(t *testing.T) { + t.Parallel() + + AssertLogging(t, func(t *testing.T, ctx context.Context) { + t.Helper() + { // Test some basic logging + log.Info(ctx, "info message", "with", "args") + log.Debug(ctx, "debug this code for me please", "number", 1) + log.Warn(ctx, "watch out!", os.ErrExist) + log.Error(ctx, "something went wrong", io.EOF, "float", 1.234) + } + { // Test errors wrapping and logging + err := errors.New("first", "1", 1) + log.Warn(ctx, "err1", err) + err = errors.Wrap(err, "second", "2", 2) + log.Error(ctx, "err2", err) + } + { // Test attributes in context + ctx1 := log.WithCtx(ctx, "ctx_key1", "ctx_value1") + log.Debug(ctx1, "ctx debug message", "debug_key1", "debug_value1") + ctx2 := log.WithCtx(ctx1, "ctx_key2", "ctx_value2") + log.Info(ctx2, "ctx info message", "info_key2", "info_value2") + } + { // Test cometBFT wrapping our errors in pkg errors + err := errors.New("new", "new", "new") + err = errors.Wrap(err, "iliad wrap", "wrap", "wrap") + err = pkgerrors.Wrap(err, "pkg wrap") + log.Warn(ctx, "Pkg wrapped error", err) + } + }) +} + +// AssertLogging returns a function that will assert all loggers' output against +// golden test files. +func AssertLogging(t *testing.T, testFunc func(*testing.T, context.Context)) { + t.Helper() + + loggers := log.LoggersForT() + + for name, initFunc := range loggers { + t.Run(name, func(t *testing.T) { + t.Parallel() + var buf bytes.Buffer + logger := initFunc(&buf) + + ctx := context.Background() + ctx = log.WithLogger(ctx, logger) + + testFunc(t, ctx) + + tutil.RequireGoldenBytes(t, buf.Bytes()) + }) + } +} diff --git a/lib/log/logger.go b/lib/log/logger.go new file mode 100644 index 00000000..4a1756d4 --- /dev/null +++ b/lib/log/logger.go @@ -0,0 +1,235 @@ +package log + +import ( + "context" + "io" + "log/slog" + "os" + "sync" + + charm "github.com/charmbracelet/log" + "github.com/muesli/termenv" + + "github.com/piplabs/story/lib/errors" +) + +//nolint:gochecknoglobals // Global logger is our approach. +var ( + global = newConsoleLogger() + globalMu = new(sync.RWMutex) +) + +type loggerKey struct{} + +// Init initializes the global logger with the given config. +// It also returns a copy of the context with the logger attached, see WithLogger. +// It returns an error if the config is invalid. +func Init(ctx context.Context, cfg Config) (context.Context, error) { + l, err := cfg.make() + if err != nil { + return nil, err + } + + globalMu.Lock() + global = l + globalMu.Unlock() + + zeroLogMetrics() + + return WithLogger(ctx, l), nil +} + +// WithLogger returns a copy of the context with which the logger +// is associated replacing the default global logger when logging with this context. +func WithLogger(ctx context.Context, logger *slog.Logger) context.Context { + return context.WithValue(ctx, loggerKey{}, logger) +} + +// getLogger returns the logger from the context, or the global logger if not present. +func getLogger(ctx context.Context) *slog.Logger { + if l := ctx.Value(loggerKey{}); l != nil { + return l.(*slog.Logger) //nolint:forcetypeassert,revive // We know the type. + } + + globalMu.RLock() + defer globalMu.RUnlock() + + return global +} + +func newLogfmtLogger(opts ...func(*options)) *slog.Logger { + o := defaultOptions() + for _, opt := range opts { + opt(&o) + } + + handler := slog.NewTextHandler(o.Writer, &slog.HandlerOptions{ + AddSource: true, + Level: o.Level, + ReplaceAttr: slogReplaceAtts(o), + }) + + return slog.New(handler) +} + +func newJSONLogger(opts ...func(*options)) *slog.Logger { + o := defaultOptions() + for _, opt := range opts { + opt(&o) + } + + handler := slog.NewJSONHandler(o.Writer, &slog.HandlerOptions{ + AddSource: true, + Level: o.Level, + ReplaceAttr: slogReplaceAtts(o), + }) + + return slog.New(handler) +} + +// newCLILogger returns a new cli logger which doesn't print timestamps, level, source or stacktraces. +func newCLILogger(opts ...func(*options)) *slog.Logger { + o := defaultOptions() + o.Level = slog.LevelInfo // Only show info and above + for _, opt := range opts { + opt(&o) + } + + charmLevel, _ := charm.ParseLevel(o.Level.String()) // Ignore error as all slog levels are valid charm levels. + + logger := charm.NewWithOptions(o.Writer, charm.Options{ + ReportTimestamp: false, + Level: charmLevel, + }) + + styles := charm.DefaultStyles() + const padWidth = 90 + styles.Message = styles.Message.Width(padWidth) + styles.Levels = nil + logger.SetStyles(styles) + logger.SetColorProfile(o.Color) + + if o.Test { + return slog.New(stubHandler{Handler: logger, skip: true}) + } + + return slog.New(logger) +} + +// newConsoleLogger returns a new console logger for the following opinionated style: +// - Colored log levels (if tty supports it) +// - Timestamps are concise with millisecond precision +// - Timestamps and structured keys are faint +// - Messages are right padded to 40 characters +// This is aimed at local-dev and debugging. Production should use json or logfmt. +func newConsoleLogger(opts ...func(*options)) *slog.Logger { + o := defaultOptions() + for _, opt := range opts { + opt(&o) + } + + timeFormat := "06-01-02 15:04:05.000" + if o.Test { + timeFormat = "00-00-00 00:00:00" + } + + charmLevel, _ := charm.ParseLevel(o.Level.String()) // Ignore error as all slog levels are valid charm levels. + + logger := charm.NewWithOptions(o.Writer, charm.Options{ + TimeFormat: timeFormat, + ReportTimestamp: true, + Level: charmLevel, + }) + + styles := charm.DefaultStyles() + styles.Timestamp = styles.Timestamp.Faint(true) + const padWidth = 40 + styles.Message = styles.Message.Width(padWidth).Inline(true) + logger.SetStyles(styles) + logger.SetColorProfile(o.Color) + + if o.Test { + return slog.New(stubHandler{Handler: logger}) + } + + return slog.New(logger) +} + +// options configure new loggers. +type options struct { + Writer io.Writer // Write to some buffer + Level slog.Level + Color termenv.Profile + Test bool // Stubs non-deterministic output for tests. +} + +func defaultOptions() options { + return options{ + Writer: os.Stderr, + Level: slog.LevelDebug, + Color: termenv.ColorProfile(), + Test: false, + } +} + +// WithNoopLogger returns a copy of the context with a noop logger which discards all logs. +func WithNoopLogger(ctx context.Context) context.Context { + return WithLogger(ctx, newConsoleLogger(func(o *options) { + o.Writer = io.Discard + })) +} + +// WithCLILogger returns a copy of the context with a cli logger. +func WithCLILogger(ctx context.Context) context.Context { + return WithLogger(ctx, newCLILogger()) +} + +// stubHandler is a handler that replaces the stacktrace and source attributes with stubs. +type stubHandler struct { + slog.Handler + skip bool // Skip instead of stubbing. +} + +func (t stubHandler) Handle(ctx context.Context, r slog.Record) error { + resp := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) + + r.Attrs(func(a slog.Attr) bool { + if a.Key == "stacktrace" { + if t.skip { + return true + } + resp.AddAttrs(slog.String("stacktrace", "")) + } else { + resp.AddAttrs(a) + } + + return true + }) + + if err := t.Handler.Handle(ctx, resp); err != nil { + return errors.Wrap(err, "handle") + } + + return nil +} + +// slogReplaceAtts returns a the slog replace attr function based on the test flag. +func slogReplaceAtts(o options) func(groups []string, a slog.Attr) slog.Attr { + if !o.Test { + return func(_ []string, attr slog.Attr) slog.Attr { return attr } + } + + return func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return slog.String(slog.TimeKey, "00-00-00 00:00:00") + } + if a.Key == slog.SourceKey && len(groups) == 0 { + return slog.String(slog.SourceKey, "") + } + if a.Key == "stacktrace" && len(groups) == 0 { + return slog.String(slog.SourceKey, "") + } + + return a + } +} diff --git a/lib/log/metrics.go b/lib/log/metrics.go new file mode 100644 index 00000000..43f7ff92 --- /dev/null +++ b/lib/log/metrics.go @@ -0,0 +1,22 @@ +package log + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + logTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "lib", + Subsystem: "log", + Name: "total", + Help: "Total number of log messages per level.", + }, []string{"level"}) +) + +// zeroLogMetrics zeros the log metrics so they display nicely in grafana. +func zeroLogMetrics() { + for _, level := range levels { + logTotal.WithLabelValues(level).Add(0) + } +} diff --git a/lib/log/testdata/TestSimpleLogs_cli.golden b/lib/log/testdata/TestSimpleLogs_cli.golden new file mode 100644 index 00000000..c7210ea9 --- /dev/null +++ b/lib/log/testdata/TestSimpleLogs_cli.golden @@ -0,0 +1,9 @@ +info message with=args +debug this code for me please number=1 +watch out! err="file already exists" +something went wrong float=1.234 err=EOF +err1 err=first 1=1 +err2 err="second: first" 2=2 1=1 +ctx debug message ctx_key1=ctx_value1 debug_key1=debug_value1 +ctx info message ctx_key1=ctx_value1 ctx_key2=ctx_value2 info_key2=info_value2 +Pkg wrapped error err="pkg wrap: iliad wrap: new" wrap=wrap new=new diff --git a/lib/log/testdata/TestSimpleLogs_console.golden b/lib/log/testdata/TestSimpleLogs_console.golden new file mode 100644 index 00000000..4f13bcdb --- /dev/null +++ b/lib/log/testdata/TestSimpleLogs_console.golden @@ -0,0 +1,9 @@ +00-00-00 00:00:00 INFO info message with=args +00-00-00 00:00:00 DEBU debug this code for me please number=1 +00-00-00 00:00:00 WARN watch out! err="file already exists" +00-00-00 00:00:00 ERRO something went wrong float=1.234 err=EOF +00-00-00 00:00:00 WARN err1 err=first 1=1 stacktrace= +00-00-00 00:00:00 ERRO err2 err="second: first" 2=2 1=1 stacktrace= +00-00-00 00:00:00 DEBU ctx debug message ctx_key1=ctx_value1 debug_key1=debug_value1 +00-00-00 00:00:00 INFO ctx info message ctx_key1=ctx_value1 ctx_key2=ctx_value2 info_key2=info_value2 +00-00-00 00:00:00 WARN Pkg wrapped error err="pkg wrap: iliad wrap: new" wrap=wrap new=new stacktrace= diff --git a/lib/log/testdata/TestSimpleLogs_json.golden b/lib/log/testdata/TestSimpleLogs_json.golden new file mode 100644 index 00000000..5407e8ed --- /dev/null +++ b/lib/log/testdata/TestSimpleLogs_json.golden @@ -0,0 +1,9 @@ +{"time":"00-00-00 00:00:00","level":"INFO","source":"","msg":"info message","with":"args"} +{"time":"00-00-00 00:00:00","level":"DEBUG","source":"","msg":"debug this code for me please","number":1} +{"time":"00-00-00 00:00:00","level":"WARN","source":"","msg":"watch out!","err":"file already exists"} +{"time":"00-00-00 00:00:00","level":"ERROR","source":"","msg":"something went wrong","float":1.234,"err":"EOF"} +{"time":"00-00-00 00:00:00","level":"WARN","source":"","msg":"err1","err":"first","1":1,"source":""} +{"time":"00-00-00 00:00:00","level":"ERROR","source":"","msg":"err2","err":"second: first","2":2,"1":1,"source":""} +{"time":"00-00-00 00:00:00","level":"DEBUG","source":"","msg":"ctx debug message","ctx_key1":"ctx_value1","debug_key1":"debug_value1"} +{"time":"00-00-00 00:00:00","level":"INFO","source":"","msg":"ctx info message","ctx_key1":"ctx_value1","ctx_key2":"ctx_value2","info_key2":"info_value2"} +{"time":"00-00-00 00:00:00","level":"WARN","source":"","msg":"Pkg wrapped error","err":"pkg wrap: iliad wrap: new","wrap":"wrap","new":"new","source":""} diff --git a/lib/log/testdata/TestSimpleLogs_logfmt.golden b/lib/log/testdata/TestSimpleLogs_logfmt.golden new file mode 100644 index 00000000..c4ccb98d --- /dev/null +++ b/lib/log/testdata/TestSimpleLogs_logfmt.golden @@ -0,0 +1,9 @@ +time="00-00-00 00:00:00" level=INFO source= msg="info message" with=args +time="00-00-00 00:00:00" level=DEBUG source= msg="debug this code for me please" number=1 +time="00-00-00 00:00:00" level=WARN source= msg="watch out!" err="file already exists" +time="00-00-00 00:00:00" level=ERROR source= msg="something went wrong" float=1.234 err=EOF +time="00-00-00 00:00:00" level=WARN source= msg=err1 err=first 1=1 source= +time="00-00-00 00:00:00" level=ERROR source= msg=err2 err="second: first" 2=2 1=1 source= +time="00-00-00 00:00:00" level=DEBUG source= msg="ctx debug message" ctx_key1=ctx_value1 debug_key1=debug_value1 +time="00-00-00 00:00:00" level=INFO source= msg="ctx info message" ctx_key1=ctx_value1 ctx_key2=ctx_value2 info_key2=info_value2 +time="00-00-00 00:00:00" level=WARN source= msg="Pkg wrapped error" err="pkg wrap: iliad wrap: new" wrap=wrap new=new source= diff --git a/lib/merkle/README.md b/lib/merkle/README.md new file mode 100644 index 00000000..ccb6ae5d --- /dev/null +++ b/lib/merkle/README.md @@ -0,0 +1,5 @@ +# merkle + +This is a port of OpenZeppelin's JS [merkle-tree](https://github.com/OpenZeppelin/merkle-tree/tree/master) library to Go. +It excludes the `StandardTree` wrapper types since our use-case doesn't require it. +It also moves all non-iliad-required logic to the test package to decrease the surface area of the library. diff --git a/lib/merkle/core.go b/lib/merkle/core.go new file mode 100644 index 00000000..a0648db2 --- /dev/null +++ b/lib/merkle/core.go @@ -0,0 +1,198 @@ +// Package merkle provides an API to generate merkle trees and proofs from 32 byte leaves. +// It is a port of the OpenZeppelin JS merkle-tree library. +// See https://github.com/OpenZeppelin/merkle-tree/tree/master. +package merkle + +import ( + "bytes" + "sort" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" +) + +// StdLeafHash returns the standard leaf hash of the given data. +// The data is hashed twice with keccak256 to prevent pre-image attacks. +func StdLeafHash(data []byte) [32]byte { + h1 := hash(data) + h2 := hash(h1[:]) + + return h2 +} + +// MakeTree returns a merkle tree given the leaves. +func MakeTree(leaves [][32]byte) ([][32]byte, error) { + if len(leaves) == 0 { + return nil, errors.New("no leaves provided") + } + + treeLen := 2*len(leaves) - 1 + tree := make([][32]byte, treeLen) + + // Fill in leaves in reverse order. + for i, leaf := range leaves { + tree[treeLen-1-i] = leaf + } + + // Fill in the intermediate nodes up to the root. + for i := treeLen - 1 - len(leaves); i >= 0; i-- { + tree[i] = hashPair(tree[leftChildIndex(i)], tree[rightChildIndex(i)]) + } + + return tree, nil +} + +// MultiProof is a merkle-multi-proof for multiple leaves. +type MultiProof struct { + Leaves [][32]byte + Proof [][32]byte + ProofFlags []bool +} + +// GetMultiProof returns a merkle-multi-proof for the given leaf indices. +func GetMultiProof(tree [][32]byte, indices ...int) (MultiProof, error) { + if len(indices) == 0 { + return MultiProof{}, errors.New("no indices provided") + } + + for _, i := range indices { + if err := checkLeafNode(tree, i); err != nil { + return MultiProof{}, err + } + } + + // Sort indices in reverse order. + sort.Slice(indices, func(i, j int) bool { + return indices[i] > indices[j] + }) + + // Check for duplicates. + for i, j := range indices[1:] { + if j == indices[i] { + return MultiProof{}, errors.New("cannot prove duplicated index") + } + } + + stack := make([]int, len(indices)) + copy(stack, indices) + var proof [][32]byte + var proofFlags []bool + + for len(stack) > 0 && stack[0] > 0 { + // Pop from the beginning. + j := stack[0] + stack = stack[1:] + + s := siblingIndex(j) + p := parentIndex(j) + + if len(stack) > 0 && s == stack[0] { + proofFlags = append(proofFlags, true) + stack = stack[1:] + } else { + proofFlags = append(proofFlags, false) + proof = append(proof, tree[s]) + } + stack = append(stack, p) //nolint:makezero // Appending to non-zero initialized slice is ok + } + + leaves := make([][32]byte, 0, len(indices)) + for _, i := range indices { + leaves = append(leaves, tree[i]) + } + + return MultiProof{ + Leaves: leaves, + Proof: proof, + ProofFlags: proofFlags, + }, nil +} + +// isTreeNode returns true if the given index is a node in the tree. +func isTreeNode(tree [][32]byte, i int) bool { + return i >= 0 && i < len(tree) +} + +// isInternalNode returns true if the given index is an internal node (not a leaf) in the tree. +func isInternalNode(tree [][32]byte, i int) bool { + return isTreeNode(tree, leftChildIndex(i)) +} + +// isLeafNode returns true if the given index is a leaf node in the tree. +func isLeafNode(tree [][32]byte, i int) bool { + return isTreeNode(tree, i) && !isInternalNode(tree, i) +} + +// checkLeafNode returns an error if the given index is not a leaf node. +func checkLeafNode(tree [][32]byte, i int) error { + if !isLeafNode(tree, i) { + return errors.New("index is not a leaf") + } + + return nil +} + +// leftChildIndex returns the index of the left child of the node at the given index. +func leftChildIndex(i int) int { + return 2*i + 1 +} + +// rightChildIndex returns the index of the right child of the node at the given index. +func rightChildIndex(i int) int { + return 2*i + 2 +} + +// parentIndex returns the index of the parent of the node at the given index. +// It panics if the given index is 0. +func parentIndex(i int) int { + if i == 0 { + panic("root has no parent") + } + + return (i - 1) / 2 +} + +// siblingIndex returns the index of the sibling of the node at the given index. +func siblingIndex(i int) int { + if i == 0 { + panic("root has no sibling") + } + + if i%2 == 0 { + return i - 1 + } + + return i + 1 +} + +// sortBytes returns the given byte slices sorted in ascending order. +func sortBytes(buf ...[]byte) [][]byte { + sort.Slice(buf, func(i, j int) bool { + return bytes.Compare(buf[i], buf[j]) < 0 + }) + + return buf +} + +// concatBytes returns the concatenation of the given byte slices. +func concatBytes(buf ...[]byte) []byte { + var resp []byte + for _, b := range buf { + resp = append(resp, b...) + } + + return resp +} + +// hashPair returns the 32 byte keccak256 hash of the sorted concatenation of the given byte arrays. +func hashPair(a [32]byte, b [32]byte) [32]byte { + return hash(concatBytes(sortBytes(a[:], b[:])...)) +} + +// hash returns the 32 byte keccak256 hash of the given byte slice. +func hash(buf []byte) [32]byte { + resp := crypto.Keccak256(buf) + + return [32]byte(resp) +} diff --git a/lib/merkle/core_internal_test.go b/lib/merkle/core_internal_test.go new file mode 100644 index 00000000..245c7461 --- /dev/null +++ b/lib/merkle/core_internal_test.go @@ -0,0 +1,102 @@ +package merkle + +import "github.com/piplabs/story/lib/errors" + +// These functions are also ported from OpenZeppelin's library, but they +// are not used by iliad's production code, so they are part of the +// tests to decrease prod code surface. + +// GetProof returns a merkle proof for the given leaf index. +func GetProof(tree [][32]byte, index int) ([][32]byte, error) { + if err := checkLeafNode(tree, index); err != nil { + return nil, err + } + + var proof [][32]byte + for index > 0 { + proof = append(proof, tree[siblingIndex(index)]) + index = parentIndex(index) + } + + return proof, nil +} + +// ProcessProof returns the root hash of the merkle tree given the leaf hash and the proof. +func ProcessProof(leaf [32]byte, proof [][32]byte) [32]byte { + node := leaf + for _, p := range proof { + node = hashPair(node, p) + } + + return node +} + +// ProcessMultiProof returns the root hash of the tree given a multi proof. +func ProcessMultiProof(multi MultiProof) ([32]byte, error) { + if err := verifyMultiProof(multi); err != nil { + return [32]byte{}, err + } + + // Copy leaves and proof. + stack := make([][32]byte, len(multi.Leaves)) + copy(stack, multi.Leaves) + proof := make([][32]byte, len(multi.Proof)) + copy(proof, multi.Proof) + + for _, flag := range multi.ProofFlags { + // Pop from the beginning of the stack. + a := stack[0] + stack = stack[1:] + + // Either pop from the stack or the proof, depending on the flag. + var b [32]byte + if flag { + b = stack[0] + stack = stack[1:] + } else { + b = proof[0] + proof = proof[1:] + } + + stack = append(stack, hashPair(a, b)) //nolint:makezero // Appending to non-zero initialized slice is ok + } + + // Either the stack or the proof should have one element left. + if len(stack)+len(proof) != 1 { + return [32]byte{}, errors.New("broken invariant") + } + + if len(stack) > 0 { + return stack[0], nil + } + + return proof[0], nil +} + +// LeafToTreeIndex returns the index of the leaf in the tree given the original index in the leaves slice. +func LeafToTreeIndex(tree [][32]byte, leafIndex int) int { + return len(tree) - 1 - leafIndex +} + +// verifyMultiProof returns an error if the given multi proof is invalid. +func verifyMultiProof(multi MultiProof) error { + var falseFlags int + for _, flag := range multi.ProofFlags { + if !flag { + falseFlags++ + } + } + if len(multi.Proof) != falseFlags { + return errors.New("false proof flags don't match proof") + } + + if len(multi.Leaves)+len(multi.Proof) != len(multi.ProofFlags)+1 { + return errors.New("proof flags don't match leaves and proof") + } + + if len(multi.Leaves) == 0 { + return errors.New("no leaves provided") + } + + return nil +} diff --git a/lib/merkle/core_test.go b/lib/merkle/core_test.go new file mode 100644 index 00000000..e1d23c06 --- /dev/null +++ b/lib/merkle/core_test.go @@ -0,0 +1,87 @@ +package merkle_test + +import ( + "math/rand" + "testing" + + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/merkle" +) + +// TestLeaveProvable tests that a leaf can be proven. +func TestLeaveProvable(t *testing.T) { + t.Parallel() + + // Create random leaves + var leaves [][32]byte + fuzz.New().NilChance(0).NumElements(1, 256).Fuzz(&leaves) + + // Make tree + tree, err := merkle.MakeTree(leaves) + require.NoError(t, err) + + // Pick random leaf + leafIndex := rand.Intn(len(leaves)) + treeIndex := merkle.LeafToTreeIndex(tree, leafIndex) + + // Get the proof + proof, err := merkle.GetProof(tree, treeIndex) + require.NoError(t, err) + + leaf := leaves[leafIndex] + root := merkle.ProcessProof(leaf, proof) + require.Equal(t, tree[0], root) +} + +// TestLeavesProvable tests that multiple leaves can be proven. +func TestLeavesProvable(t *testing.T) { + t.Parallel() + + // Create random leaves + var leaves [][32]byte + fuzz.New().NilChance(0).NumElements(1, 256).Fuzz(&leaves) + + // Make tree + tree, err := merkle.MakeTree(leaves) + require.NoError(t, err) + + // Pick random leaves + leafIndices := randomIndicesRange(leaves) + treeIndices := make([]int, len(leafIndices)) + for i, leafIndex := range leafIndices { + treeIndices[i] = merkle.LeafToTreeIndex(tree, leafIndex) + } + + // Get the multi proof + multi, err := merkle.GetMultiProof(tree, treeIndices...) + require.NoError(t, err) + + // Check that the proof contains the leaves + require.Equal(t, len(leafIndices), len(multi.Leaves)) + for _, i := range leafIndices { + require.Contains(t, multi.Leaves, leaves[i]) + } + + // Check that the proof is valid + root, err := merkle.ProcessMultiProof(multi) + require.NoError(t, err) + require.Equal(t, tree[0], root) +} + +// randomIndicesRange returns a random range of indices of the provided slice. +func randomIndicesRange(slice [][32]byte) []int { + start := rand.Intn(len(slice)) + count := rand.Intn(len(slice) - start) + if count == 0 { + count = 1 + } + + indices := make([]int, count) + for i := range indices { + indices[i] = start + i + } + + return indices +} diff --git a/lib/netconf/flag.go b/lib/netconf/flag.go new file mode 100644 index 00000000..6deb11a3 --- /dev/null +++ b/lib/netconf/flag.go @@ -0,0 +1,8 @@ +package netconf + +import "github.com/spf13/pflag" + +// BindFlag binds the network identifier flag. +func BindFlag(flags *pflag.FlagSet, network *ID) { + flags.StringVar((*string)(network), "network", string(*network), "Iliad network to participate in: mainnet, testnet, devnet") +} diff --git a/lib/netconf/iliad/genesis.json b/lib/netconf/iliad/genesis.json new file mode 100644 index 00000000..6ef50f9c --- /dev/null +++ b/lib/netconf/iliad/genesis.json @@ -0,0 +1,1078 @@ +{ + "genesis_time": "2024-04-16T11:04:40.60280319Z", + "chain_id": "1512-1723078116", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": ["secp256k1"] + }, + "version": { + "app": "0" + }, + "abci": { + "vote_extensions_enable_height": "1" + } + }, + "app_hash": "", + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1qt55c9g0ejhjhnxck8cte0dq5l04hm6dlgcxuf", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1l3yxxceum6p7696k5eqq3p3f5eh00ehpjr4e5x", + "pub_key": null, + "account_number": "1", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1qtnqm7rp8t5ftjnqgl64400qle2avtyvxsw5yq", + "pub_key": null, + "account_number": "2", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1e3hkz3w05t35d8m3dw9ws9vx2rz0vtss69we8p", + "pub_key": null, + "account_number": "3", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1kcke6kg78ldrw7exz74k6v4d5qzmtatahp54f5", + "pub_key": null, + "account_number": "4", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1s6w84ny8p209pkjwrthmssn5c37lt0wgpf29tc", + "pub_key": null, + "account_number": "5", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1gszk8u4k5u8m365354anje296qx4v2jw4u3yhq", + "pub_key": null, + "account_number": "6", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1vlh0t7ujxe5dvvytvwf0d96yj8grjj8xmpp78j", + "pub_key": null, + "account_number": "7", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1lqgjsarc74jk073ccztynf0wu3ymflxfdgmev7", + "pub_key": null, + "account_number": "8", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1cnjashqzv3q8wyfkaaxnrp9vjyzvd734uyqwrj", + "pub_key": null, + "account_number": "9", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story13lwn9jp255npcykqd79973mxxy64cc35cs8z04", + "pub_key": null, + "account_number": "10", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1ht5tmwmerzx6729w383hvnvl7cxwk20pe8rdee", + "pub_key": null, + "account_number": "11", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1u50w6tjpcq7qlmqfg8jjzhfs4j9ye6fnk8k4ku", + "pub_key": null, + "account_number": "12", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1pgyj3c85yfp4umrppnk2xvrzc2rjd7yakcttq3", + "pub_key": null, + "account_number": "13", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1nhwpl60ezc59wtp9npr2z3h2xuwgma7anepw2z", + "pub_key": null, + "account_number": "14", + "sequence": "0" + } + ] + }, + "bank": { + "params": { + "send_enabled": [{ "denom": "stake", "enabled": true }], + "default_send_enabled": false + }, + "balances": [ + { + "address": "story1qt55c9g0ejhjhnxck8cte0dq5l04hm6dlgcxuf", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1l3yxxceum6p7696k5eqq3p3f5eh00ehpjr4e5x", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1qtnqm7rp8t5ftjnqgl64400qle2avtyvxsw5yq", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1e3hkz3w05t35d8m3dw9ws9vx2rz0vtss69we8p", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1kcke6kg78ldrw7exz74k6v4d5qzmtatahp54f5", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1s6w84ny8p209pkjwrthmssn5c37lt0wgpf29tc", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1gszk8u4k5u8m365354anje296qx4v2jw4u3yhq", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1vlh0t7ujxe5dvvytvwf0d96yj8grjj8xmpp78j", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1lqgjsarc74jk073ccztynf0wu3ymflxfdgmev7", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1cnjashqzv3q8wyfkaaxnrp9vjyzvd734uyqwrj", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story13lwn9jp255npcykqd79973mxxy64cc35cs8z04", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1ht5tmwmerzx6729w383hvnvl7cxwk20pe8rdee", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1u50w6tjpcq7qlmqfg8jjzhfs4j9ye6fnk8k4ku", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1pgyj3c85yfp4umrppnk2xvrzc2rjd7yakcttq3", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + }, + { + "address": "story1nhwpl60ezc59wtp9npr2z3h2xuwgma7anepw2z", + "coins": [ + { + "denom": "stake", + "amount": "1000000000000000" + } + ] + } + ], + "supply": [ + { + "denom": "stake", + "amount": "15000000000000000" + } + ], + "denom_metadata": [], + "send_enabled": [{ "denom": "stake", "enabled": true }] + }, + "distribution": { + "params": { + "community_tax": "0.000000000000000000", + "base_proposer_reward": "0.000000000000000000", + "bonus_proposer_reward": "0.000000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "evmengine": { + "params": { + "execution_block_hash": "C+QEeblc5kpddmK2rD9Pxd4kedaAlbfNV+dSMJ4vBg0=" + } + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x02E94C150FCCAF2BCCD8B1F0BCBDA0A7DF5BEF4D", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1qt55c9g0ejhjhnxck8cte0dq5l04hm6dlgcxuf", + "validator_address": "storyvaloper1qt55c9g0ejhjhnxck8cte0dq5l04hm6d38v8hz", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "At6TavtEaoK1O8TC9ojmMImXQZsD6A6Y1M1w1PViWE4w" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xFC4863633CDE83ED1756A640088629A66EF7E6E1", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1l3yxxceum6p7696k5eqq3p3f5eh00ehpjr4e5x", + "validator_address": "storyvaloper1l3yxxceum6p7696k5eqq3p3f5eh00ehpuvpcld", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AtKiyCeN/uusDBo4qoPSkrYeuSaHB4pftE8zQW8YWgUS" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x02E60DF8613AE895CA6047F55ABDE0FE55D62C8C", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1qtnqm7rp8t5ftjnqgl64400qle2avtyvxsw5yq", + "validator_address": "storyvaloper1qtnqm7rp8t5ftjnqgl64400qle2avtyvgl640t", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "Ap/rwo39GMKt+IThhDfO2bkgm1hAYSRhHz1THihjIVxT" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xCC6F6145CFA2E3469F716B8AE8158650C4F62E10", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1e3hkz3w05t35d8m3dw9ws9vx2rz0vtss69we8p", + "validator_address": "storyvaloper1e3hkz3w05t35d8m3dw9ws9vx2rz0vtss526cv2", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AveyI/HEBmSF8vadY7ZTL5v+dvm2u9Ve2szNPNDqaOvw" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xB62D9D591E3FDA377B2617AB6D32ADA005B5F57D", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1kcke6kg78ldrw7exz74k6v4d5qzmtatahp54f5", + "validator_address": "storyvaloper1kcke6kg78ldrw7exz74k6v4d5qzmtataewq5zl", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AkR1wDGaAWdxsaww6B3BCmnpfzBFNYwdbDW3qX5F9ow2" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x869C7ACC870A9E50DA4E1AEFB84274C47DF5BDC8", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1s6w84ny8p209pkjwrthmssn5c37lt0wgpf29tc", + "validator_address": "storyvaloper1s6w84ny8p209pkjwrthmssn5c37lt0wg0x7yqn", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsajeOrBHt+KrqQl2UtVLtTrCo0qnxA/Z8UlcZnLHNGE" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x440563F2B6A70FB8EA91A57B396545D00D562A4E", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1gszk8u4k5u8m365354anje296qx4v2jw4u3yhq", + "validator_address": "storyvaloper1gszk8u4k5u8m365354anje296qx4v2jwmn99ut", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsU2Y9z9ZfFHqnlZYjSTL+Bs1xD+5QTRj0X2/GrRWzt0" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x67EEF5FB923668D6308B6392F6974491D03948E6", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1vlh0t7ujxe5dvvytvwf0d96yj8grjj8xmpp78j", + "validator_address": "storyvaloper1vlh0t7ujxe5dvvytvwf0d96yj8grjj8x4w4lve", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A0eTAvAHHFkURCriUG1nt/Ax5/TF2ngzrw4JHCJdd7Gx" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xF811287478F56567FA38C09649A5EEE449B4FCC9", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1lqgjsarc74jk073ccztynf0wu3ymflxfdgmev7", + "validator_address": "storyvaloper1lqgjsarc74jk073ccztynf0wu3ymflxfr80c84", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AiFAZJchDhG8TEYPqTLCZvLnbWvvJnRfIOKHPeXt2tLx" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xC4E5D85C026440771136EF4D3184AC9104C6FA35", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1cnjashqzv3q8wyfkaaxnrp9vjyzvd734uyqwrj", + "validator_address": "storyvaloper1cnjashqzv3q8wyfkaaxnrp9vjyzvd734jt50ge", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "Asu2e5HkS0uAG3oG9QKxSZNaczTwNrLApoEotVQy5/Y+" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x8FDD32C82AA5261C12C06F8A5F476631355C6234", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story13lwn9jp255npcykqd79973mxxy64cc35cs8z04", + "validator_address": "storyvaloper13lwn9jp255npcykqd79973mxxy64cc35klnry7", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A/zb7OE0tC3QQ2HC1pTv2GmNH0y7zlhnJrzAVgJZ6m7c" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xBAE8BDBB79188DAF28AE89E3764D9FF60CEB29E1", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1ht5tmwmerzx6729w383hvnvl7cxwk20pe8rdee", + "validator_address": "storyvaloper1ht5tmwmerzx6729w383hvnvl7cxwk20phghvjj", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AgzQcZvp+Cp31dTnApm2T+33CNudryU2zZ2NR3+PKV9P" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xE51EED2E41C03C0FEC0941E5215D30AC8A4CE933", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1u50w6tjpcq7qlmqfg8jjzhfs4j9ye6fnk8k4ku", + "validator_address": "storyvaloper1u50w6tjpcq7qlmqfg8jjzhfs4j9ye6fncgz5ah", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A6Q0FSqzXQS7C7kDNa3QhQXJTYU8COFMrIbvFzzv1NkJ" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x0A0928E0F422435E6C610CECA33062C28726F89D", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1pgyj3c85yfp4umrppnk2xvrzc2rjd7yakcttq3", + "validator_address": "storyvaloper1pgyj3c85yfp4umrppnk2xvrzc2rjd7yachl2t6", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "Akm6NK7ECzmpdEYg/6DbI1OacIh6bAd6bS/ORY2hhFoD" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x9DDC1FE9F91628572C259846A146EA371C8DF7DD", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "story1nhwpl60ezc59wtp9npr2z3h2xuwgma7anepw2z", + "validator_address": "storyvaloper1nhwpl60ezc59wtp9npr2z3h2xuwgma7aak40pf", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AjeY4h65xdNw8WuvskWpcp6dB/ZA+fHHQeLhIxatuhe+" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + } + ] + }, + "slashing": { + "params": { + "signed_blocks_window": "300", + "min_signed_per_window": "0.050000000000000000", + "downtime_jail_duration": "60s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "10s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "stake", + "min_commission_rate": "0.000000000000000000" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false + }, + "valsync": {}, + "evmstaking": { + "params": { + "max_withdrawal_per_block": 32, + "max_sweep_per_block": 128, + "min_partial_withdrawal_amount": 100000000 + } + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "stake", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + } + } +} diff --git a/lib/netconf/iliad/seeds.txt b/lib/netconf/iliad/seeds.txt new file mode 100644 index 00000000..d2e67520 --- /dev/null +++ b/lib/netconf/iliad/seeds.txt @@ -0,0 +1,3 @@ +7a934929eaa71a13c97739a41d04364bba00a58e@52.9.220.233:26656 +4307adcc8eba7fb96626185659da6b182260c7b3@54.241.155.73:26656 + diff --git a/lib/netconf/local/genesis.json b/lib/netconf/local/genesis.json new file mode 100644 index 00000000..fc97a88c --- /dev/null +++ b/lib/netconf/local/genesis.json @@ -0,0 +1,196 @@ +{ + "genesis_time": "2024-04-16T11:04:40.60280319Z", + "chain_id": "story-1001511", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": ["secp256k1"] + }, + "version": { + "app": "0" + }, + "abci": { + "vote_extensions_enable_height": "1" + } + }, + "app_hash": "", + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "{{LOCAL_ACCOUNT_ADDRESS}}", + "pub_key": null, + "account_number": "0", + "sequence": "0" + } + ] + }, + "bank": { + "params": { + "send_enabled": [{ "denom": "stake", "enabled": true }], + "default_send_enabled": false + }, + "balances": [ + { + "address": "{{LOCAL_ACCOUNT_ADDRESS}}", + "coins": [ + { + "denom": "stake", + "amount": "10000000000000000" + } + ] + } + ], + "supply": [ + { + "denom": "stake", + "amount": "10000000000000000" + } + ], + "denom_metadata": [], + "send_enabled": [{ "denom": "stake", "enabled": true }] + }, + "distribution": { + "params": { + "community_tax": "0.000000000000000000", + "base_proposer_reward": "0.000000000000000000", + "bonus_proposer_reward": "0.000000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "evmengine": { + "params": { + "execution_block_hash": "x1AaLCAZ7s5wgA7U7ImJzFebqyQKHP42VmkPXCXgqO4=" + } + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "Test", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "{{LOCAL_ACCOUNT_ADDRESS}}", + "validator_address": "{{LOCAL_VALIDATOR_ADDRESS}}", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "{{LOCAL_VALIDATOR_KEY}}" + }, + "value": { + "denom": "stake", + "amount": "1000000000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + } + ] + }, + "slashing": { + "params": { + "signed_blocks_window": "300", + "min_signed_per_window": "0.050000000000000000", + "downtime_jail_duration": "60s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "10s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "stake", + "min_commission_rate": "0.000000000000000000" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false + }, + "valsync": {}, + "evmstaking": { + "params": { + "max_withdrawal_per_block": 32, + "max_sweep_per_block": 128, + "min_partial_withdrawal_amount": 100000000 + } + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "stake", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + } + } +} diff --git a/lib/netconf/local/seeds.txt b/lib/netconf/local/seeds.txt new file mode 100644 index 00000000..e69de29b diff --git a/lib/netconf/netconf.go b/lib/netconf/netconf.go new file mode 100644 index 00000000..886e861c --- /dev/null +++ b/lib/netconf/netconf.go @@ -0,0 +1,143 @@ +// Package netconf provides the configuration of an Iliad network, an instance +// of the Iliad cross chain protocol. +package netconf + +import ( + "time" + + "github.com/piplabs/story/lib/evmchain" +) + +// Network defines a deployment of the Iliad cross chain protocol. +// It spans an iliad chain (both execution and consensus) and a set of +// supported EVMs. +type Network struct { + ID ID `json:"name"` // ID of the network. e.g. "simnet", "testnet", "staging", "mainnet" + Chains []Chain `json:"chains"` // Chains that are part of the network +} + +// Validate returns an error if the configuration is invalid. +func (n Network) Validate() error { + if err := n.ID.Verify(); err != nil { + return err + } + + // TODO: Validate chains + + return nil +} + +// EVMChains returns all evm chains in the network. It excludes the iliad consensus chain. +func (n Network) EVMChains() []Chain { + resp := make([]Chain, 0, len(n.Chains)) + for _, chain := range n.Chains { + if IsIliadConsensus(n.ID, chain.ID) { + continue + } + + resp = append(resp, chain) + } + + return resp +} + +// ChainIDs returns the all chain IDs in the network. +// This is a convenience method. +func (n Network) ChainIDs() []uint64 { + resp := make([]uint64, 0, len(n.Chains)) + for _, chain := range n.Chains { + resp = append(resp, chain.ID) + } + + return resp +} + +// ChainNamesByIDs returns the all chain IDs and names in the network. +// This is a convenience method. +func (n Network) ChainNamesByIDs() map[uint64]string { + resp := make(map[uint64]string) + for _, chain := range n.Chains { + resp[chain.ID] = chain.Name + } + + return resp +} + +// IliadEVMChain returns the Iliad execution chain config or false if it does not exist. +func (n Network) IliadEVMChain() (Chain, bool) { + for _, chain := range n.Chains { + if IsIliadExecution(n.ID, chain.ID) { + return chain, true + } + } + + return Chain{}, false +} + +// IliadConsensusChain returns the Iliad consensus chain config or false if it does not exist. +func (n Network) IliadConsensusChain() (Chain, bool) { + for _, chain := range n.Chains { + if IsIliadConsensus(n.ID, chain.ID) { + return chain, true + } + } + + return Chain{}, false +} + +// EthereumChain returns the ethereum Layer1 chain config or false if it does not exist. +func (n Network) EthereumChain() (Chain, bool) { + for _, chain := range n.Chains { + switch n.ID { + case Mainnet: + if chain.ID == evmchain.IDEthereum { + return chain, true + } + case Local: + if chain.ID == evmchain.IDLocal { + return chain, true + } + case Iliad: + if chain.ID == evmchain.IDIliad { + return chain, true + } + case Testnet: + if chain.ID == evmchain.IDHolesky { + return chain, true + } + default: + if chain.ID == evmchain.IDMockL1Fast || chain.ID == evmchain.IDMockL1Slow { + return chain, true + } + } + } + + return Chain{}, false +} + +// ChainName returns the chain name for the given ID or an empty string if it does not exist. +func (n Network) ChainName(id uint64) string { + chain, _ := n.Chain(id) + return chain.Name +} + +// Chain returns the chain config for the given ID or false if it does not exist. +func (n Network) Chain(id uint64) (Chain, bool) { + for _, chain := range n.Chains { + if chain.ID == id { + return chain, true + } + } + + return Chain{}, false +} + +// Chain defines the configuration of an execution chain that supports +// the Iliad cross chain protocol. This is most supported EVMs, but +// also the Iliad EVM, and the Iliad Consensus chain. +type Chain struct { + ID uint64 // Chain ID asa per https://chainlist.org + Name string // Chain name as per https://chainlist.org + // RPCURL string // RPC URL of the chain + BlockPeriod time.Duration // Block period of the chain +} diff --git a/lib/netconf/netconf_test.go b/lib/netconf/netconf_test.go new file mode 100644 index 00000000..4c8ca205 --- /dev/null +++ b/lib/netconf/netconf_test.go @@ -0,0 +1,158 @@ +package netconf_test + +import ( + "context" + "encoding/hex" + "flag" + "fmt" + "sort" + "strings" + "testing" + + "github.com/BurntSushi/toml" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/e2e/app/key" + "github.com/piplabs/story/e2e/manifests" + "github.com/piplabs/story/e2e/types" + "github.com/piplabs/story/lib/k1util" + "github.com/piplabs/story/lib/netconf" + "github.com/piplabs/story/lib/tutil" +) + +//go:generate go test -golden -run=TestGenConsSeeds + +// TestGenConsSeeds generates /consensus-seeds.txt by loading e2e manifests and parsing seed* p2p_consensus keys. +func TestGenConsSeeds(t *testing.T) { + t.Parallel() + tests := []struct { + network netconf.ID + manifestFunc func() []byte + }{ + { + network: netconf.Testnet, + manifestFunc: manifests.Testnet, + }, + } + for _, test := range tests { + t.Run(test.network.String(), func(t *testing.T) { + t.Parallel() + var manifest types.Manifest + //nolint:musttag // ignore perturb json + _, err := toml.Decode(string(test.manifestFunc()), &manifest) + require.NoError(t, err) + + var peers []string + for _, node := range sortedKeys(manifest.Keys) { + if !strings.HasPrefix(node, "seed") { + continue + } + + for typ, addr := range manifest.Keys[node] { + if typ != key.P2PConsensus { + continue + } + + peers = append(peers, fmt.Sprintf("%s@%s.%s.storyprotocol.xyz", addr, node, test.network)) // ABCDE123@seed01.staging.storyprotocol.xyz + } + } + + seeds := strings.Join(peers, "\n") + seedsFile := fmt.Sprintf("../%s/consensus-seeds.txt", test.network) + tutil.RequireGoldenBytes(t, []byte(seeds), tutil.WithFilename(seedsFile)) + }) + } +} + +var genExecutionSeeds = flag.Bool("gen-execution-seeds", false, "Enable to generate execution-seeds.txt. Note this requires GCP secret manager read-access") + +func TestGenExecutionSeeds(t *testing.T) { + t.Parallel() + if !*genExecutionSeeds { + t.Skip("Skipping since --gen-execution-seeds=false") + return + } + + tests := []struct { + network netconf.ID + manifestFunc func() []byte + }{ + { + network: netconf.Testnet, + manifestFunc: manifests.Testnet, + }, + } + for _, test := range tests { + t.Run(test.network.String(), func(t *testing.T) { + t.Parallel() + ctx := context.Background() + var manifest types.Manifest + //nolint:musttag // ignore perturb json + _, err := toml.Decode(string(test.manifestFunc()), &manifest) + require.NoError(t, err) + + var peers []string + for _, node := range sortedKeys(manifest.Keys) { + if !strings.HasPrefix(node, "seed") { + continue + } + + for typ, addr := range manifest.Keys[node] { + if typ != key.P2PExecution { + continue + } + + key, err := key.Download(ctx, test.network, node, typ, addr) + require.NoError(t, err) + + stdPrivKey, err := key.ECDSA() + require.NoError(t, err) + + pubkey64 := k1util.PubKeyToBytes64(&stdPrivKey.PublicKey) + pubkeyHex := hex.EncodeToString(pubkey64) + nodeName := strings.TrimSuffix(node, "_evm") + + peers = append(peers, fmt.Sprintf("enode://%s@%s.%s.storyprotocol:30303", pubkeyHex, nodeName, test.network)) + } + } + + seeds := strings.Join(peers, "\n") + seedsFile := fmt.Sprintf("../%s/execution-seeds.txt", test.network) + tutil.RequireGoldenBytes(t, []byte(seeds), tutil.WithFilename(seedsFile)) + }) + } +} + +func sortedKeys[T any](m map[string]T) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys +} + +func TestConsensusSeeds(t *testing.T) { + t.Parallel() + + require.Len(t, netconf.Testnet.Static().ConsensusSeeds(), 2) +} + +func TestExecutionSeeds(t *testing.T) { + t.Skip("testnet shutdown at the moment") + t.Parallel() + + seeds := netconf.Testnet.Static().ExecutionSeeds() + require.Len(t, seeds, 2) + for _, seed := range seeds { + node, err := enode.ParseV4(seed) + require.NoError(t, err) + + require.EqualValues(t, 30303, node.TCP()) + require.EqualValues(t, 30303, node.UDP()) + t.Logf("Seed IP: %s: %s", node.IP(), seed) + require.NotEmpty(t, node.IP()) + } +} diff --git a/lib/netconf/network.go b/lib/netconf/network.go new file mode 100644 index 00000000..b4323fe0 --- /dev/null +++ b/lib/netconf/network.go @@ -0,0 +1,102 @@ +package netconf + +import "github.com/piplabs/story/lib/errors" + +// ID is a network identifier. +type ID string + +// IsEphemeral returns true if the network is short-lived, therefore ephemeral. +func (i ID) IsEphemeral() bool { + return i == Simnet || i == Devnet || i == Staging +} + +// IsProtected returns true if the network is long-lived, therefore protected. +func (i ID) IsProtected() bool { + return !i.IsEphemeral() +} + +// Static returns the static config and data for the network. +func (i ID) Static() Static { + return statics[i] +} + +func (i ID) Verify() error { + if !supported[i] { + return errors.New("unsupported network", "network", i) + } + + return nil +} + +func (i ID) String() string { + return string(i) +} + +func (i ID) Version() string { + return i.Static().Version +} + +const ( + // Simnet is a simulated network for very simple testing of individual binaries. + // It is a single binary with mocked clients (no networking). + Simnet ID = "simnet" // Single binary with mocked clients (no network) + + // Devnet is the most basic single-machine deployment of the Iliad cross chain protocol. + // It uses docker compose to setup a network with multi containers. + // E.g. 2 geth nodes, 4 iliad validators and 2 anvil. + Devnet ID = "devnet" + + // Staging is the Iliad team's internal staging network, similar to a internal testnet. + // It connects to real public testnets (e.g. Arbitrum testnet). + // It is deployed to GCP using terraform. + // E.g. 1 Erigon, 1 Geth, 4 iliad validators, and 2 iliad sentries. + Staging ID = "staging" + + // Testnet is the Iliad public testnet. + Testnet ID = "testnet" + + // Mainnet is the Iliad public mainnet. + Mainnet ID = "mainnet" + + // Iliad is the official Story Protocol public testnet. + Iliad ID = "iliad" + + // Used for local network testing. + Local ID = "local" +) + +// supported is a map of supported networks. +// +//nolint:gochecknoglobals // Global state here is fine. +var supported = map[ID]bool{ + Simnet: true, + Devnet: true, + Staging: true, + Testnet: true, + Mainnet: true, + Iliad: true, + Local: true, +} + +// IsAny returns true if the `ID` matches any of the provided targets. +func IsAny(id ID, targets ...ID) bool { + for _, target := range targets { + if id == target { + return true + } + } + + return false +} + +// All returns all the supported network IDs. +func All() []ID { + var resp []ID + for id, ok := range supported { + if ok { + resp = append(resp, id) + } + } + + return resp +} diff --git a/lib/netconf/static.go b/lib/netconf/static.go new file mode 100644 index 00000000..66d422c7 --- /dev/null +++ b/lib/netconf/static.go @@ -0,0 +1,187 @@ +package netconf + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/evmchain" + + _ "embed" +) + +const consensusIDPrefix = "iliad-" +const consensusIDOffset = 1_000_000 +const maxValidators = 10 + +// Static defines static config and data for a network. +type Static struct { + Version string + IliadExecutionChainID uint64 + MaxValidators uint32 + ConsensusGenesisJSON []byte + ConsensusSeedTXT []byte + ExecutionGenesisJSON []byte + ExecutionSeedTXT []byte +} + +type Deployment struct { + ChainID uint64 + Address common.Address +} + +// IliadConsensusChainIDStr returns the chain ID string for the Iliad consensus chain. +// It is calculated as "iliad-". +func (s Static) IliadConsensusChainIDStr() string { + return fmt.Sprintf("%s%d", consensusIDPrefix, s.IliadConsensusChainIDUint64()) +} + +// IliadConsensusChainIDUint64 returns the chain ID uint64 for the Iliad consensus chain. +// It is calculated as 1_000_000 + IliadExecutionChainID. +func (s Static) IliadConsensusChainIDUint64() uint64 { + return consensusIDOffset + s.IliadExecutionChainID +} + +// IliadConsensusChain returns the story consensus Chain struct. +func (s Static) IliadConsensusChain() Chain { + return Chain{ + ID: s.IliadConsensusChainIDUint64(), + Name: "iliad_consensus", + BlockPeriod: time.Second * 2, + } +} + +func (s Static) ConsensusSeeds() []string { + var resp []string + for _, seed := range strings.Split(string(s.ConsensusSeedTXT), "\n") { + if seed = strings.TrimSpace(seed); seed != "" { + resp = append(resp, seed) + } + } + + return resp +} + +func (s Static) ExecutionSeeds() []string { + var resp []string + for _, seed := range strings.Split(string(s.ExecutionSeedTXT), "\n") { + if seed = strings.TrimSpace(seed); seed != "" { + resp = append(resp, seed) + } + } + + return resp +} + +// Use random runid for version in ephemeral networks. +// +//nolint:gochecknoglobals // Static ID +var runid = uuid.New().String() + +//nolint:gochecknoglobals // Static addresses +var ( + //go:embed testnet/consensus-genesis.json + testnetConsensusGenesisJSON []byte + + //go:embed testnet/consensus-seeds.txt + testnetConsensusSeedsTXT []byte + + //go:embed testnet/execution-genesis.json + testnetExecutionGenesisJSON []byte + + //go:embed testnet/execution-seeds.txt + testnetExecutionSeedsTXT []byte +) + +//nolint:gochecknoglobals // Static addresses +var ( + //go:embed iliad/genesis.json + iliadConsensusGenesisJSON []byte + + //go:embed iliad/seeds.txt + iliadConsensusSeedsTXT []byte +) + +//nolint:gochecknoglobals // Static addresses +var ( + //go:embed local/genesis.json + localConsensusGenesisJSON []byte + + //go:embed local/seeds.txt + localConsensusSeedsTXT []byte +) + +//nolint:gochecknoglobals // Static mappings. +var statics = map[ID]Static{ + Simnet: { + Version: runid, + IliadExecutionChainID: evmchain.IDIliadEphemeral, + MaxValidators: maxValidators, + }, + Devnet: { + Version: runid, + IliadExecutionChainID: evmchain.IDIliadEphemeral, + MaxValidators: maxValidators, + }, + Staging: { + Version: runid, + IliadExecutionChainID: evmchain.IDIliadEphemeral, + MaxValidators: maxValidators, + }, + Testnet: { + Version: "v0.0.1", + IliadExecutionChainID: evmchain.IDIliadTestnet, + MaxValidators: maxValidators, + ConsensusGenesisJSON: testnetConsensusGenesisJSON, + ConsensusSeedTXT: testnetConsensusSeedsTXT, + ExecutionGenesisJSON: testnetExecutionGenesisJSON, + ExecutionSeedTXT: testnetExecutionSeedsTXT, + }, + Iliad: { + Version: "v0.0.1", + IliadExecutionChainID: evmchain.IDIliadTestnet, + ConsensusGenesisJSON: iliadConsensusGenesisJSON, + ConsensusSeedTXT: iliadConsensusSeedsTXT, + }, + Local: { + Version: "v0.0.1", + IliadExecutionChainID: evmchain.IDLocal, + ConsensusGenesisJSON: localConsensusGenesisJSON, + ConsensusSeedTXT: localConsensusSeedsTXT, + }, + Mainnet: { + Version: "v0.0.1", + MaxValidators: maxValidators, + }, +} + +// ConsensusChainIDStr2Uint64 parses the uint suffix from the provided a consensus chain ID string. +func ConsensusChainIDStr2Uint64(id string) (uint64, error) { + if !strings.HasPrefix(id, consensusIDPrefix) { + return 0, errors.New("invalid consensus chain ID", "id", id) + } + + suffix := strings.TrimPrefix(id, consensusIDPrefix) + + resp, err := strconv.ParseUint(suffix, 10, 64) + if err != nil { + return 0, errors.Wrap(err, "parse consensus chain ID", "id", id) + } + + return resp, nil +} + +// IsIliadConsensus returns true if provided chainID is the iliad consensus chain for the network. +func IsIliadConsensus(network ID, chainID uint64) bool { + return network.Static().IliadConsensusChainIDUint64() == chainID +} + +// IsIliadExecution returns true if provided chainID is the iliad execution chain for the network. +func IsIliadExecution(network ID, chainID uint64) bool { + return network.Static().IliadExecutionChainID == chainID +} diff --git a/lib/netconf/testnet/consensus-genesis.json b/lib/netconf/testnet/consensus-genesis.json new file mode 100644 index 00000000..89fb122f --- /dev/null +++ b/lib/netconf/testnet/consensus-genesis.json @@ -0,0 +1,367 @@ +{ + "genesis_time": "2024-04-16T11:04:40.60280319Z", + "chain_id": "story-1001513", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "secp256k1" + ] + }, + "version": { + "app": "0" + }, + "abci": { + "vote_extensions_enable_height": "1" + } + }, + "app_hash": "", + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story17ex776zl7qtkr4a46kz9yqtdphyl5sl0vr6ztl", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1xns2lvhu8apag6wecp6rdsdgsksf0xfzlzgfvg", + "pub_key": null, + "account_number": "1", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story1tea4kfpcyns409kr82yznauq0uk9amwqjswrnc", + "pub_key": null, + "account_number": "2", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "story16y9x5ew9tp40vzf6f7vnc0tqjavv9getmv2zx7", + "pub_key": null, + "account_number": "3", + "sequence": "0" + } + ] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "balances": [ + { + "address": "story1xns2lvhu8apag6wecp6rdsdgsksf0xfzlzgfvg", + "coins": [ + { + "denom": "stake", + "amount": "1000000000" + } + ] + }, + { + "address": "story1tea4kfpcyns409kr82yznauq0uk9amwqjswrnc", + "coins": [ + { + "denom": "stake", + "amount": "1000000000" + } + ] + }, + { + "address": "story16y9x5ew9tp40vzf6f7vnc0tqjavv9getmv2zx7", + "coins": [ + { + "denom": "stake", + "amount": "1000000000" + } + ] + }, + { + "address": "story17ex776zl7qtkr4a46kz9yqtdphyl5sl0vr6ztl", + "coins": [ + { + "denom": "stake", + "amount": "1000000000" + } + ] + } + ], + "supply": [ + { + "denom": "stake", + "amount": "4000000000000000000" + } + ], + "denom_metadata": [], + "send_enabled": [] + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.000000000000000000", + "bonus_proposer_reward": "0.000000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xf64deF685fF01761D7b5D58452016D0dC9fa43eF", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1000000000", + "delegator_address": "", + "validator_address": "storyvaloper17ex776zl7qtkr4a46kz9yqtdphyl5sl02q39ud", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A4r1h+aCEGqaXcmFiE/V3EYnfx1vYoY+FmsN7GDEfK9b" + }, + "value": { + "denom": "stake", + "amount": "1000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x34E0afB2FC3f43D469d9C07436c1A885A0979922", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1000000000", + "delegator_address": "", + "validator_address": "storyvaloper1xns2lvhu8apag6wecp6rdsdgsksf0xfzeprwm6", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A0poYEpHgbWvIoWM91Edv0Re985t1DXKDSbsYp8waZZr" + }, + "value": { + "denom": "stake", + "amount": "1000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0x5e7B5b243824E15796c33a8829F7807F2C5EeDc0", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1000000000", + "delegator_address": "", + "validator_address": "storyvaloper1tea4kfpcyns409kr82yznauq0uk9amwq5n9yy2", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AlkzSMJdQnCMZ8Q7S+FxSpCMjxgsrtlswK9dLnoA6T/t" + }, + "value": { + "denom": "stake", + "amount": "1000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "0xD10A6A65C5586aF6093A4F993C3d609758c2a32b", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.000000000000000000", + "max_rate": "0.000000000000000000", + "max_change_rate": "0.000000000000000000" + }, + "min_self_delegation": "1000000000", + "delegator_address": "", + "validator_address": "storyvaloper16y9x5ew9tp40vzf6f7vnc0tqjavv9geta0p93v", + "pubkey": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AkVnDoR9dRCdWBsnGuwTr/w+yEMTXDE9L+jla6Od9Ppd" + }, + "value": { + "denom": "stake", + "amount": "1000000000" + } + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] + } + ] + }, + "slashing": { + "params": { + "signed_blocks_window": "3000", + "min_signed_per_window": "0.050000000000000000", + "downtime_jail_duration": "60s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "10s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "stake", + "min_commission_rate": "0.000000000000000000" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false + }, + "evmstaking": { + "params": { + "max_withdrawal_per_block": 4, + "max_sweep_per_block": 64, + "min_partial_withdrawal_amount": 600000 + } + } + } +} diff --git a/lib/netconf/testnet/consensus-seeds.txt b/lib/netconf/testnet/consensus-seeds.txt new file mode 100644 index 00000000..9607ace1 --- /dev/null +++ b/lib/netconf/testnet/consensus-seeds.txt @@ -0,0 +1,2 @@ +9644D22FCC9D3966CFF5789E6C411FDD40B101ED@seed01.testnet.storyprotocol.xyz +A839711D734491E1BA99A69E3FEEEBB965223792@seed02.testnet.storyprotocol.xyz \ No newline at end of file diff --git a/lib/netconf/testnet/execution-genesis.json b/lib/netconf/testnet/execution-genesis.json new file mode 100644 index 00000000..25e80dfa --- /dev/null +++ b/lib/netconf/testnet/execution-genesis.json @@ -0,0 +1,55 @@ +{ + "config": { + "chainId": 1513, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "shanghaiTime": 0, + "cancunTime": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x20000", + "gasLimit": "8000000", + "extradata": "", + "nonce": "0x0000000000000042", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "alloc": { + "0000000000000000000000000000000000000001": { "balance": "0x1" }, + "0000000000000000000000000000000000000002": { "balance": "0x1" }, + "0000000000000000000000000000000000000003": { "balance": "0x1" }, + "0000000000000000000000000000000000000004": { "balance": "0x1" }, + "0000000000000000000000000000000000000005": { "balance": "0x1" }, + "0000000000000000000000000000000000000006": { "balance": "0x1" }, + "0000000000000000000000000000000000000007": { "balance": "0x1" }, + "0000000000000000000000000000000000000008": { "balance": "0x1" }, + "0000000000000000000000000000000000000009": { "balance": "0x1" }, + "000000000000000000000000000000000000001A": { "balance": "0x1" }, + "f39fd6e51aad88f6f4ce6ab8827279cfffb92266": { "balance": "0xd3c21bcecceda1000000" }, + "f398C12A45Bc409b6C652E25bb0a3e702492A4ab": { "balance": "0x52B7D2DCC80CD2E4000000" }, + "EcB1D051475A7e330b1DD6683cdC7823Bbcf8Dcf": { "balance": "0x52B7D2DCC80CD2E4000000" }, + "5518D1BD054782792D2783509FbE30fa9D888888": { "balance": "0x52B7D2DCC80CD2E4000000" }, + "bd39FAe873F301b53e14d365383118cD4a222222": { "balance": "0x52B7D2DCC80CD2E4000000" }, + "00FCeC044cD73e8eC6Ad771556859b00C9011111": { "balance": "0x52B7D2DCC80CD2E4000000" }, + "b5350B7CaE94C2bF6B2b56Ef6A06cC1153900000": { "balance": "0x52B7D2DCC80CD2E4000000" }, + "CCCCCC0000000000000000000000000000000001": { + "code": "0x6080604052600436106100f35760003560e01c8063991b20bc1161008a578063bf7e214f11610059578063bf7e214f146102c4578063cd6dc68714610318578063ee8e4af514610338578063f4914d331461034b57600080fd5b8063991b20bc1461021b578063a00e84161461023b578063a5a470ad1461025b578063ad3cb1cc1461026e57600080fd5b806372a02aab116100c657806372a02aab146101755780637a9e5e4b146101955780638fb36037146101b557806397bfaed8146101fb57600080fd5b80632a80cda3146100f8578063311391521461011a5780634f1ef2861461014d57806352d1902d14610160575b600080fd5b34801561010457600080fd5b50610118610113366004611a36565b61037f565b005b34801561012657600080fd5b5061013a610135366004611a98565b610445565b6040519081526020015b60405180910390f35b61011861015b366004611b32565b610493565b34801561016c57600080fd5b5061013a6104b2565b34801561018157600080fd5b50610118610190366004611c12565b6104e1565b3480156101a157600080fd5b506101186101b0366004611c86565b610543565b3480156101c157600080fd5b506101ca610670565b6040517fffffffff000000000000000000000000000000000000000000000000000000009091168152602001610144565b34801561020757600080fd5b50610118610216366004611ca1565b6106e5565b34801561022757600080fd5b5061013a610236366004611a98565b610908565b34801561024757600080fd5b5061013a610256366004611d1f565b61093e565b610118610269366004611a98565b6109ab565b34801561027a57600080fd5b506102b76040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b6040516101449190611daf565b3480156102d057600080fd5b507ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a005460405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610144565b34801561032457600080fd5b50610118610333366004611e00565b610b64565b610118610346366004611d1f565b610e40565b34801561035757600080fd5b507ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955005461013a565b61038c335b600036610e52565b60008111610421576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f4950546f6b656e5374616b653a206d696e4465706f736974416d6f756e74206360448201527f616e6e6f7420626520300000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b7ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d6568629550055565b60007ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d65686295500600201838360405161047b929190611e2a565b90815260200160405180910390205490505b92915050565b61049b611051565b6104a482611157565b6104ae8282611163565b5050565b60006104bc6112a1565b507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc90565b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600f60248201527f4e6f7420696d706c656d656e74656400000000000000000000000000000000006044820152606401610418565b336105827ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a005473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146105fe576040517f068ca9d800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610418565b8173ffffffffffffffffffffffffffffffffffffffff163b600003610667576040517fc2f31e5e00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602401610418565b6104ae82611310565b7ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a0080546000919074010000000000000000000000000000000000000000900460ff166106bd5760006106df565b7f8fb36037000000000000000000000000000000000000000000000000000000005b91505090565b6040517ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955009082907ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955039061073b908a908a90611e2a565b90815260200160405180910390208686604051610759929190611e2a565b90815260200160405180910390205410156107f6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f4950546f6b656e5374616b653a20496e73756666696369656e74207374616b6560448201527f6420616d6f756e740000000000000000000000000000000000000000000000006064820152608401610418565b8181600101888860405161080b929190611e2a565b908152602001604051809103902060008282546108289190611e69565b9250508190555081816002018686604051610844929190611e2a565b908152602001604051809103902060008282546108619190611e69565b925050819055508181600301888860405161087d929190611e2a565b9081526020016040518091039020868660405161089b929190611e2a565b908152602001604051809103902060008282546108b89190611e69565b90915550506040517f7c24130a52a0da8275f87318c4e912c0dc2340e81bf3ed1d86207b6b5b01662b906108f790899089908990899089908990611ec5565b60405180910390a150505050505050565b60007ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d65686295500600101838360405161047b929190611e2a565b60007ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955006003018585604051610974929190611e2a565b90815260200160405180910390208383604051610992929190611e2a565b9081526020016040518091039020549050949350505050565b7ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955008054341015610a5c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f4950546f6b656e5374616b653a204465706f73697420616d6f756e7420746f6f60448201527f206c6f77000000000000000000000000000000000000000000000000000000006064820152608401610418565b34816001018484604051610a71929190611e2a565b90815260200160405180910390206000828254610a8e9190611f02565b9250508190555034816002018484604051610aaa929190611e2a565b90815260200160405180910390206000828254610ac79190611f02565b9250508190555034816003018484604051610ae3929190611e2a565b90815260200160405180910390208484604051610b01929190611e2a565b90815260200160405180910390206000828254610b1e9190611f02565b90915550506040517fa334aafeebd858ba4ccf49c18ec8cee74be731f0d439a6ae08020068e71e22b590610b5790859085903490611f15565b60405180910390a1505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff16600081158015610baf5750825b905060008267ffffffffffffffff166001148015610bcc5750303b155b905081158015610bda575080155b15610c11576040517ff92ee8a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b84547fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000001660011785558315610c725784547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff16680100000000000000001785555b73ffffffffffffffffffffffffffffffffffffffff8716610d15576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f4950546f6b656e5374616b653a204163636573734d616e616765722063616e6e60448201527f6f742062652030000000000000000000000000000000000000000000000000006064820152608401610418565b60008611610da5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f4950546f6b656e5374616b653a206d696e4465706f736974416d6f756e74206360448201527f616e6e6f742062652030000000000000000000000000000000000000000000006064820152608401610418565b610dae876113a8565b610db66113b9565b7ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955008690558315610e375784547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2906020016108f7565b50505050505050565b610e4c848484846113c1565b50505050565b7ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a00600080610ed5610eb77ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a005473ffffffffffffffffffffffffffffffffffffffff1690565b8730610ec7600460008a8c611f39565b610ed091611f63565b611580565b91509150816110495763ffffffff811615610fff5782547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000178355610f657ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a005473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff166394c7d7ee8787876040518463ffffffff1660e01b8152600401610fa193929190611fab565b600060405180830381600087803b158015610fbb57600080fd5b505af1158015610fcf573d6000803e3d6000fd5b505084547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff168555506110499050565b6040517f068ca9d800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff87166004820152602401610418565b505050505050565b3073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000c8d451cd5ba38af9f72e628da0998d8ab4b206a616148061111e57507f000000000000000000000000c8d451cd5ba38af9f72e628da0998d8ab4b206a673ffffffffffffffffffffffffffffffffffffffff166111057f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1614155b15611155576040517fe07c8dba00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b565b61116033610384565b50565b8173ffffffffffffffffffffffffffffffffffffffff166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156111e8575060408051601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682019092526111e591810190611fdb565b60015b611236576040517f4c9c8ce300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83166004820152602401610418565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8114611292576040517faa1d49a400000000000000000000000000000000000000000000000000000000815260048101829052602401610418565b61129c83836116fd565b505050565b3073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000c8d451cd5ba38af9f72e628da0998d8ab4b206a61614611155576040517fe07c8dba00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7ff3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a00805473ffffffffffffffffffffffffffffffffffffffff83167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117825560408051918252517f2f658b440c35314f52658ea8a740e05b284cdc84dc9ae01e891f21b8933e7cad9181900360200190a15050565b6113b0611760565b611160816117c7565b611155611760565b7ff608d1d531422217a12c257e55f19b7c5f4d601c341a42bb11c1d656862955008054341015611472576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f4950546f6b656e5374616b653a204465706f73697420616d6f756e7420746f6f60448201527f206c6f77000000000000000000000000000000000000000000000000000000006064820152608401610418565b34816001018686604051611487929190611e2a565b908152602001604051809103902060008282546114a49190611f02565b92505081905550348160020184846040516114c0929190611e2a565b908152602001604051809103902060008282546114dd9190611f02565b92505081905550348160030186866040516114f9929190611e2a565b90815260200160405180910390208484604051611517929190611e2a565b908152602001604051809103902060008282546115349190611f02565b90915550506040517fe77f103965e0ff8836ce54ba9bac869f217cd5da27d6bdefd090282c397211c0906115719087908790879087903490611ff4565b60405180910390a15050505050565b60405173ffffffffffffffffffffffffffffffffffffffff848116602483015283811660448301527fffffffff0000000000000000000000000000000000000000000000000000000083166064830152600091829182918291891690608401604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fb70096130000000000000000000000000000000000000000000000000000000017905251611660919061202e565b600060405180830381855afa9150503d806000811461169b576040519150601f19603f3d011682016040523d82523d6000602084013e6116a0565b606091505b509150915081156116f25760408151106116d257808060200190518101906116c8919061205a565b90945092506116f2565b60208151106116f257808060200190518101906116ef919061209a565b93505b505094509492505050565b611706826117d8565b60405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a28051156117585761129c82826118a7565b6104ae61192a565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005468010000000000000000900460ff16611155576040517fd7e6bcf800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6117cf611760565b61116081611310565b8073ffffffffffffffffffffffffffffffffffffffff163b600003611841576040517f4c9c8ce300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610418565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b60606000808473ffffffffffffffffffffffffffffffffffffffff16846040516118d1919061202e565b600060405180830381855af49150503d806000811461190c576040519150601f19603f3d011682016040523d82523d6000602084013e611911565b606091505b5091509150611921858383611962565b95945050505050565b3415611155576040517fb398979f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60608261197757611972826119f4565b6119ed565b815115801561199b575073ffffffffffffffffffffffffffffffffffffffff84163b155b156119ea576040517f9996b31500000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85166004820152602401610418565b50805b9392505050565b805115611a045780518082602001fd5b6040517f1425ea4200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600060208284031215611a4857600080fd5b5035919050565b60008083601f840112611a6157600080fd5b50813567ffffffffffffffff811115611a7957600080fd5b602083019150836020828501011115611a9157600080fd5b9250929050565b60008060208385031215611aab57600080fd5b823567ffffffffffffffff811115611ac257600080fd5b611ace85828601611a4f565b90969095509350505050565b803573ffffffffffffffffffffffffffffffffffffffff81168114611afe57600080fd5b919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b60008060408385031215611b4557600080fd5b611b4e83611ada565b9150602083013567ffffffffffffffff80821115611b6b57600080fd5b818501915085601f830112611b7f57600080fd5b813581811115611b9157611b91611b03565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715611bd757611bd7611b03565b81604052828152886020848701011115611bf057600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b600080600080600060608688031215611c2a57600080fd5b853567ffffffffffffffff80821115611c4257600080fd5b611c4e89838a01611a4f565b90975095506020880135915080821115611c6757600080fd5b50611c7488828901611a4f565b96999598509660400135949350505050565b600060208284031215611c9857600080fd5b6119ed82611ada565b60008060008060008060808789031215611cba57600080fd5b863567ffffffffffffffff80821115611cd257600080fd5b611cde8a838b01611a4f565b90985096506020890135915080821115611cf757600080fd5b50611d0489828a01611a4f565b979a9699509760408101359660609091013595509350505050565b60008060008060408587031215611d3557600080fd5b843567ffffffffffffffff80821115611d4d57600080fd5b611d5988838901611a4f565b90965094506020870135915080821115611d7257600080fd5b50611d7f87828801611a4f565b95989497509550505050565b60005b83811015611da6578181015183820152602001611d8e565b50506000910152565b6020815260008251806020840152611dce816040850160208701611d8b565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169190910160400192915050565b60008060408385031215611e1357600080fd5b611e1c83611ada565b946020939093013593505050565b8183823760009101908152919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8181038181111561048d5761048d611e3a565b8183528181602085013750600060208284010152600060207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b608081526000611ed960808301888a611e7c565b8281036020840152611eec818789611e7c565b6040840195909552505060600152949350505050565b8082018082111561048d5761048d611e3a565b604081526000611f29604083018587611e7c565b9050826020830152949350505050565b60008085851115611f4957600080fd5b83861115611f5657600080fd5b5050820193919092039150565b7fffffffff000000000000000000000000000000000000000000000000000000008135818116916004851015611fa35780818660040360031b1b83161692505b505092915050565b73ffffffffffffffffffffffffffffffffffffffff84168152604060208201526000611921604083018486611e7c565b600060208284031215611fed57600080fd5b5051919050565b606081526000612008606083018789611e7c565b828103602084015261201b818688611e7c565b9150508260408301529695505050505050565b60008251612040818460208701611d8b565b9190910192915050565b80518015158114611afe57600080fd5b6000806040838503121561206d57600080fd5b6120768361204a565b9150602083015163ffffffff8116811461208f57600080fd5b809150509250929050565b6000602082840312156120ac57600080fd5b6119ed8261204a56fea2646970667358221220a4275bfb1ca87803fd8f9ab7734898d93e7e97a43cda74bde3d797a5408a59c264736f6c63430008170033", + "balance": "0x1" + }, + "CCCCCC0000000000000000000000000000000002": { + "code": "0x6080604052600436106100b15760003560e01c806379ba509711610069578063e30c39781161004e578063e30c3978146101c2578063f2fde38b146101ed578063f679d3051461020d57600080fd5b806379ba5097146101825780638da5cb5b1461019757600080fd5b80632801f1ec1161009a5780632801f1ec1461013657806340eda14a1461015a578063715018a61461016d57600080fd5b806304ff53ed146100b65780630c863f7714610114575b600080fd5b3480156100c257600080fd5b506100ea7f000000000000000000000000cccccc000000000000000000000000000000000181565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561012057600080fd5b5061013461012f3660046107b3565b610215565b005b34801561014257600080fd5b5061014c60025481565b60405190815260200161010b565b6101346101683660046107cc565b610222565b34801561017957600080fd5b5061013461044b565b34801561018e57600080fd5b5061013461045f565b3480156101a357600080fd5b5060005473ffffffffffffffffffffffffffffffffffffffff166100ea565b3480156101ce57600080fd5b5060015473ffffffffffffffffffffffffffffffffffffffff166100ea565b3480156101f957600080fd5b5061013461020836600461083e565b6104d6565b610134610586565b61021d61058f565b600255565b6040517f6889b16e00000000000000000000000000000000000000000000000000000000815260009073e3ad13f6b60db254577bcc7bf3824543c100796790636889b16e90610277908690869060040161087b565b600060405180830381865af4158015610294573d6000803e3d6000fd5b505050506040513d6000823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682016040526102da91908101906109ad565b905060007f000000000000000000000000cccccc000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638d3e1e41836040518263ffffffff1660e01b815260040161033791906109fe565b600060405180830381865afa158015610354573d6000803e3d6000fd5b505050506040513d6000823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016820160405261039a9190810190610a68565b5050505050905080610433576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602960248201527f4950546f6b656e536c617368696e673a2056616c696461746f7220646f65732060448201527f6e6f74206578697374000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b61044561044085856105e2565b61060f565b50505050565b61045361058f565b61045d6000610712565b565b600154339073ffffffffffffffffffffffffffffffffffffffff1681146104ca576040517f118cdaa700000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8216600482015260240161042a565b6104d381610712565b50565b6104de61058f565b6001805473ffffffffffffffffffffffffffffffffffffffff83167fffffffffffffffffffffffff0000000000000000000000000000000000000000909116811790915561054160005473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b61045d3361060f565b60005473ffffffffffffffffffffffffffffffffffffffff16331461045d576040517f118cdaa700000000000000000000000000000000000000000000000000000000815233600482015260240161042a565b60006105f18260018186610b0c565b6040516105ff929190610b36565b6040519081900390209392505050565b6002543410156106a1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f4950546f6b656e536c617368696e673a20496e73756666696369656e7420666560448201527f6500000000000000000000000000000000000000000000000000000000000000606482015260840161042a565b6040516000903480156108fc029183818181858288f193505050501580156106cd573d6000803e3d6000fd5b5060405173ffffffffffffffffffffffffffffffffffffffff8216907fc3ef55ddda4bc9300706e15ab3aed03c762d8afd43a7d358a7b9503cb39f281b90600090a250565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001690556104d3816000805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000602082840312156107c557600080fd5b5035919050565b600080602083850312156107df57600080fd5b823567ffffffffffffffff808211156107f757600080fd5b818501915085601f83011261080b57600080fd5b81358181111561081a57600080fd5b86602082850101111561082c57600080fd5b60209290920196919550909350505050565b60006020828403121561085057600080fd5b813573ffffffffffffffffffffffffffffffffffffffff8116811461087457600080fd5b9392505050565b60208152816020820152818360408301376000818301604090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0160101919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b60005b838110156109125781810151838201526020016108fa565b50506000910152565b600067ffffffffffffffff80841115610936576109366108c8565b604051601f85017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190828211818310171561097c5761097c6108c8565b8160405280935085815286868601111561099557600080fd5b6109a38660208301876108f7565b5050509392505050565b6000602082840312156109bf57600080fd5b815167ffffffffffffffff8111156109d657600080fd5b8201601f810184136109e757600080fd5b6109f68482516020840161091b565b949350505050565b6020815260008251806020840152610a1d8160408501602087016108f7565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169190910160400192915050565b805163ffffffff81168114610a6357600080fd5b919050565b60008060008060008060c08789031215610a8157600080fd5b86518015158114610a9157600080fd5b602088015190965067ffffffffffffffff811115610aae57600080fd5b8701601f81018913610abf57600080fd5b610ace8982516020840161091b565b95505060408701519350610ae460608801610a4f565b9250610af260808801610a4f565b9150610b0060a08801610a4f565b90509295509295509295565b60008085851115610b1c57600080fd5b83861115610b2957600080fd5b5050820193919092039150565b818382376000910190815291905056fea26469706673582212208f6c466131c9651a265155a8f63bd5dbe9727b44a87cdbc1999901bf323bba2664736f6c63430008170033", + "balance": "0x1" + } + } +} \ No newline at end of file diff --git a/lib/netconf/testnet/execution-seeds.txt b/lib/netconf/testnet/execution-seeds.txt new file mode 100644 index 00000000..4018086d --- /dev/null +++ b/lib/netconf/testnet/execution-seeds.txt @@ -0,0 +1,2 @@ +enode://e0f1f87044b3fd8c6e8198e0233501388c0656b7207546954bd188e4839f353d284e96d5aca12587b7cede2b684b0e7d895adabf6c6850bc51353b316821bc30@seed01.testnet.storyprotocol.xyz:30303 +enode://6121283c696a4896d47a00c9a41fa5c170a974e05208b5aa39c49c54d8aee64889ffb58dda7374c444e39356736ca20b3531c5fb93238d94eb4f463f755eb801@seed02.testnet.storyprotocol.xyz:30303 \ No newline at end of file diff --git a/lib/promutil/evmstaking.go b/lib/promutil/evmstaking.go new file mode 100644 index 00000000..0faf630e --- /dev/null +++ b/lib/promutil/evmstaking.go @@ -0,0 +1,12 @@ +package promutil + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + EVMStakingQueueDepth = promauto.NewGauge(prometheus.GaugeOpts{ //nolint:promlinter // skip + Name: "evmstaking_queue_depth", + }) +) diff --git a/lib/promutil/resetgauge.go b/lib/promutil/resetgauge.go new file mode 100644 index 00000000..17706087 --- /dev/null +++ b/lib/promutil/resetgauge.go @@ -0,0 +1,71 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +// Package promutil provides Prometheus utilities. +// This was copied from Obol's Charon repo. +package promutil + +import ( + "strings" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const separator = "|" + +// NewResetGaugeVec creates a new ResetGaugeVec. +func NewResetGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *ResetGaugeVec { + return &ResetGaugeVec{ + inner: promauto.NewGaugeVec(opts, labelNames), + labels: make(map[string]bool), + } +} + +// ResetGaugeVec is a GaugeVec that can be reset which deletes all previously set labels. +// This is useful to clear out labels that are no longer present. +type ResetGaugeVec struct { + inner *prometheus.GaugeVec + + mu sync.Mutex + labels map[string]bool +} + +func (g *ResetGaugeVec) WithLabelValues(lvs ...string) prometheus.Gauge { + for _, lv := range lvs { + if strings.Contains(lv, separator) { + panic("label value cannot contain separator") + } + } + + g.mu.Lock() + defer g.mu.Unlock() + + g.labels[strings.Join(lvs, separator)] = true + + return g.inner.WithLabelValues(lvs...) +} + +// Reset deletes all previously set labels that match all the given label values. +// An empty slice will delete all previously set labels. +func (g *ResetGaugeVec) Reset(lvs ...string) { + g.mu.Lock() + defer g.mu.Unlock() + + for label := range g.labels { + match := true + for _, check := range lvs { + if !strings.Contains(label, check) { + match = false + break + } + } + + if !match { + continue + } + + g.inner.DeleteLabelValues(strings.Split(label, separator)...) + delete(g.labels, label) + } +} diff --git a/lib/promutil/resetgauge_test.go b/lib/promutil/resetgauge_test.go new file mode 100644 index 00000000..f6450509 --- /dev/null +++ b/lib/promutil/resetgauge_test.go @@ -0,0 +1,76 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package promutil_test + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/promutil" +) + +//nolint:paralleltest // This test uses global prometheus registry so concurrent tests are not safe. +func TestResetGaugeVec(t *testing.T) { + const resetTest = "reset_test" + + var testResetGauge = promutil.NewResetGaugeVec(prometheus.GaugeOpts{ + Name: resetTest, + Help: "", + }, []string{"label0", "label1"}) + + testResetGauge.WithLabelValues("1", "a").Set(0) + assertVecLen(t, resetTest, 1) + + // Same labels, should not increase length + testResetGauge.WithLabelValues("1", "a").Set(1) + assertVecLen(t, resetTest, 1) + + testResetGauge.WithLabelValues("2", "b").Set(2) + assertVecLen(t, resetTest, 2) + + testResetGauge.Reset() + assertVecLen(t, resetTest, 0) + + testResetGauge.WithLabelValues("3", "c").Set(3) + assertVecLen(t, resetTest, 1) + + testResetGauge.WithLabelValues("3", "d").Set(3) + assertVecLen(t, resetTest, 2) + + testResetGauge.WithLabelValues("3", "e").Set(3) + assertVecLen(t, resetTest, 3) + + testResetGauge.WithLabelValues("4", "z").Set(4) + assertVecLen(t, resetTest, 4) + + testResetGauge.Reset("3", "c") + assertVecLen(t, resetTest, 3) + + testResetGauge.Reset("3") + assertVecLen(t, resetTest, 1) +} + +func assertVecLen(t *testing.T, name string, l int) { //nolint:unparam // abstracting name is fine even though it is always currently constant + t.Helper() + + metrics, err := prometheus.DefaultGatherer.Gather() + require.NoError(t, err) + + for _, metricFam := range metrics { + if metricFam.GetName() != name { + continue + } + + require.Len(t, metricFam.GetMetric(), l) + + return + } + + if l == 0 { + return + } + + require.Fail(t, "metric not found") +} diff --git a/lib/solc/solc.go b/lib/solc/solc.go new file mode 100644 index 00000000..7dfdcaa1 --- /dev/null +++ b/lib/solc/solc.go @@ -0,0 +1,34 @@ +package solc + +type StorageLayout struct { + Storage []StorageLayoutEntry `json:"storage"` + Types map[string]StorageLayoutType `json:"types"` +} + +type StorageLayoutEntry struct { + AstID uint `json:"astId"` + Contract string `json:"contract"` + Label string `json:"label"` + Offset uint `json:"offset"` + Slot uint `json:"slot,string"` + Type string `json:"type"` +} + +type StorageLayoutType struct { + Encoding string `json:"encoding"` + Label string `json:"label"` + NumberOfBytes uint `json:"numberOfBytes,string"` + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` +} + +// SlotOf returns the slot number of the given label in the storage layout. +func SlotOf(layout StorageLayout, label string) (uint, bool) { + for _, entry := range layout.Storage { + if entry.Label == label { + return entry.Slot, true + } + } + + return 0, false +} diff --git a/lib/stream/stream.go b/lib/stream/stream.go new file mode 100644 index 00000000..0079b961 --- /dev/null +++ b/lib/stream/stream.go @@ -0,0 +1,341 @@ +// Package stream provide a generic stream function. +package stream + +import ( + "context" + "sync" + "time" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/log" + + "go.opentelemetry.io/otel/trace" +) + +type Callback[E any] func(ctx context.Context, elem E) error + +type Deps[E any] struct { + // Dependency functions + + // FetchBatch fetches the next batch of elements from the provided height (inclusive). + // The elements must be sequential, since the internal height cursors is incremented for each element returned. + FetchBatch func(ctx context.Context, chainID uint64, height uint64) ([]E, error) + // Backoff returns a backoff function. See expbackoff package for the implementation. + Backoff func(ctx context.Context) func() + // Verify is a sanity check function, it ensures each element is valid. + Verify func(ctx context.Context, elem E, height uint64) error + // Height returns the height of an element. + Height func(elem E) uint64 + + // Config + FetchWorkers uint64 + ElemLabel string + RetryCallback bool + + // Metrics + IncFetchErr func() + IncCallbackErr func() + SetStreamHeight func(uint64) + SetCallbackLatency func(time.Duration) + StartTrace func(ctx context.Context, height uint64, spanName string) (context.Context, trace.Span) +} + +// Stream streams elements from the provided height (inclusive) of a specific chain. +// It fetches the batches of elements from the current height, and +// calls the callback function for each element in strictly-sequential order. +// +// It supports concurrent fetching of single-element-batches only. +// It retries forever on fetch errors. +// It can either retry or return callback errors. +// It returns (nil) when the context is canceled. +// + +func Stream[E any](ctx context.Context, deps Deps[E], srcChainID uint64, startHeight uint64, callback Callback[E]) error { + if deps.FetchWorkers == 0 { + return errors.New("invalid zero fetch worker count") + } + + // Define a robust fetch function that fetches a batch of elements from a height (inclusive). + // It only returns an empty list if the context is canceled. + // It retries forever on error or if no elements found. + fetchFunc := func(ctx context.Context, height uint64) []E { + backoff := deps.Backoff(ctx) // Note that backoff returns immediately on ctx cancel. + for { + if ctx.Err() != nil { + return nil + } + + fetchCtx, span := deps.StartTrace(ctx, height, "fetch") + elems, err := deps.FetchBatch(fetchCtx, srcChainID, height) + span.End() + + if ctx.Err() != nil { + return nil + } else if err != nil { + log.Warn(ctx, "Failed fetching "+deps.ElemLabel+" (will retry)", err, "height", height) + deps.IncFetchErr() + backoff() + + continue + } else if len(elems) == 0 { + // We reached the head of the chain, wait for new blocks. + backoff() + + continue + } + + heightsOK := true + for i, elem := range elems { + if h := deps.Height(elem); h != height+uint64(i) { + log.Error(ctx, "Invalid "+deps.ElemLabel+" height [BUG]", nil, + "expect", height, + "actual", h, + ) + + heightsOK = false + } + } + if !heightsOK { // Can't return invalid elements, just retry fetching for now. + backoff() + continue + } + + return elems + } + } + + // Define a robust callback function that retries on error. + callbackFunc := func(ctx context.Context, elem E) error { + height := deps.Height(elem) + ctx, span := deps.StartTrace(ctx, height, "callback") + defer span.End() + ctx = log.WithCtx(ctx, "height", height) + + backoff := deps.Backoff(ctx) + + if err := deps.Verify(ctx, elem, height); err != nil { + return errors.Wrap(err, "verify") + } + + // Retry callback on error + for { + if ctx.Err() != nil { + return nil // Don't backoff or log on ctx cancel, just return nil. + } + + t0 := time.Now() + err := callback(ctx, elem) + deps.SetCallbackLatency(time.Since(t0)) + if ctx.Err() != nil { + return nil // Don't backoff or log on ctx cancel, just return nil. + } else if err != nil && !deps.RetryCallback { + deps.IncCallbackErr() + return errors.Wrap(err, "callback") + } else if err != nil { + log.Warn(ctx, "Failed processing "+deps.ElemLabel+" (will retry)", err) + deps.IncCallbackErr() + backoff() + + continue + } + + deps.SetStreamHeight(height) + + return nil + } + } + + // Sorting buffer connects the concurrent fetch workers to the callback + sorter := newSortingBuffer(startHeight, deps, callbackFunc) + + // Start fetching workers + startFetchWorkers(ctx, deps, fetchFunc, sorter, startHeight) + + // Sort fetch results and call callback + return sorter.Process(ctx) +} + +// startFetchWorkers starts worker goroutines +// that fetch all batches concurrently from the provided . +// +// Concurrent fetching is only supported for single-element-batches, since +// each worker fetches: startHeight + Ith-iteration + Nth-worker. +// +// For multi-element-batches, only a single worker is supported. +func startFetchWorkers[E any]( + ctx context.Context, + deps Deps[E], + work func(ctx context.Context, height uint64) []E, + sorter *sortingBuffer[E], + startHeight uint64, +) { + for i := range deps.FetchWorkers { + go func(workerID int, height uint64) { + for { + // Work function MUST be robust, always returning a non-empty strictly-sequential batch + // or nil if the context was canceled. + batch := work(ctx, height) + if ctx.Err() != nil { + return + } else if len(batch) == 0 { + log.Error(ctx, "Work function returned an empty batch [BUG]", nil) + return + } else if len(batch) > 1 && deps.FetchWorkers > 1 { + log.Error(ctx, "Concurrent fetching only supported for single element batches [BUG]", nil) + return + } + + var last uint64 + for i, e := range batch { + last = deps.Height(e) + if last != height+uint64(i) { + log.Error(ctx, "Invalid batch [BUG]", nil) + return + } + } + + sorter.Add(ctx, workerID, batch) + + // Calculate next height to fetch + height = last + deps.FetchWorkers + } + }(int(i), startHeight+i) // Initialize a height to fetch per worker + } +} + +// sortingBuffer buffers unordered batches of elements (one batch per worker), +// providing elements to the callback in strictly-sequential sorted order. +type sortingBuffer[E any] struct { + deps Deps[E] + callback func(ctx context.Context, elem E) error + startHeight uint64 + + mu sync.Mutex + buffer map[uint64]workerElem[E] // Worker elements by height + counts map[int]int // Count of elements per worker + signals map[int]chan struct{} // Processes <> Worker comms +} + +const processorID = -1 + +func newSortingBuffer[E any]( + startHeight uint64, + deps Deps[E], + callback func(ctx context.Context, elem E) error, +) *sortingBuffer[E] { + signals := make(map[int]chan struct{}) + signals[processorID] = make(chan struct{}, 1) + for i := range int(deps.FetchWorkers) { + signals[i] = make(chan struct{}, 1) + } + + return &sortingBuffer[E]{ + startHeight: startHeight, + deps: deps, + callback: callback, + buffer: make(map[uint64]workerElem[E]), + counts: make(map[int]int), + signals: signals, + } +} + +// signal signals the ID to wakeup. +func (m *sortingBuffer[E]) signal(signalID int) { + select { + case m.signals[signalID] <- struct{}{}: + default: + } +} + +// retryLock repeatedly obtains the lock and calls the callback while it returns false. +// It returns once the callback returns true or an error. +func (m *sortingBuffer[E]) retryLock(ctx context.Context, signalID int, fn func(ctx context.Context) (bool, error)) error { + timer := time.NewTicker(time.Nanosecond) // Initial timer is instant + defer timer.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-m.signals[signalID]: + case <-timer.C: + } + + m.mu.Lock() + done, err := fn(ctx) + m.mu.Unlock() + if err != nil { + return err + } else if done { + return nil + } + + // Not done, so retry again, much later + timer.Reset(time.Second) + } +} + +func (m *sortingBuffer[E]) Add(ctx context.Context, workerID int, batch []E) { + _ = m.retryLock(ctx, workerID, func(_ context.Context) (bool, error) { + // Wait for any previous batch this worker added to be processed before adding this batch. + // This results in backpressure to workers, basically only buffering a single batch per worker. + if m.counts[workerID] > 0 { + return false, nil // Previous batch still in buffer, retry a bit later + } + + // Add the batch + for _, e := range batch { + height := m.deps.Height(e) + + // Invariant check (error handling in workerFunc) + if _, ok := m.buffer[height]; ok { + return false, errors.New("duplicate element [BUG]") + } + + m.buffer[height] = workerElem[E]{WorkerID: workerID, E: e} + } + + m.counts[workerID] = len(batch) + m.signal(processorID) // Signal the processor + + return true, nil // Don't retry lock again, we are done. + }) +} + +// Process calls the callback function in strictly-sequential order from (inclusive) +// as elements become available in the buffer. +func (m *sortingBuffer[E]) Process(ctx context.Context) error { + next := m.startHeight + return m.retryLock(ctx, processorID, func(ctx context.Context) (bool, error) { + elem, ok := m.buffer[next] + if !ok { + return false, nil // Next height not in buffer, retry a bit later + } + delete(m.buffer, next) + + err := m.callback(ctx, elem.E) + if err != nil { + return false, err // Don't retry again + } + + m.counts[elem.WorkerID]-- + + if m.counts[elem.WorkerID] == 0 { + m.signal(elem.WorkerID) // Signal the worker that it can add another batch + } + + next++ + + if _, ok := m.buffer[next]; ok { + m.signal(processorID) // Signal ourselves if next elements already in buffer. + } + + return false, nil // Retry again with next height + }) +} + +// workerElem represents an element processed by a worker. +type workerElem[E any] struct { + WorkerID int + E E +} diff --git a/lib/tokens/coingecko/coingecko.go b/lib/tokens/coingecko/coingecko.go new file mode 100644 index 00000000..3c7f219d --- /dev/null +++ b/lib/tokens/coingecko/coingecko.go @@ -0,0 +1,107 @@ +package coingecko + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/tokens" +) + +const ( + endpointSimplePrice = "/api/v3/simple/price" + prodHost = "https://api.coingecko.com" +) + +type Client struct { + host string +} + +var _ tokens.Pricer = Client{} + +// New creates a new goingecko Client with the given options. +func New(opts ...func(*options)) Client { + o := defaultOpts() + for _, opt := range opts { + opt(&o) + } + + return Client{ + host: o.Host, + } +} + +// GetPriceUSD returns the price of each coin in USD. +func (c Client) Price(ctx context.Context, tkns ...tokens.Token) (map[tokens.Token]float64, error) { + return c.getPrice(ctx, "usd", tkns...) +} + +// simplePriceResponse is the response from the simple/price endpoint. +// It mapes coin id to currency to price. +type simplePriceResponse map[string]map[string]float64 + +// GetPrice returns the price of each coin in the given currency. +func (c Client) getPrice(ctx context.Context, currency string, tkns ...tokens.Token) (map[tokens.Token]float64, error) { + ids := make([]string, len(tkns)) + for i, t := range tkns { + ids[i] = t.CoingeckoID() + } + + params := url.Values{ + "ids": {strings.Join(ids, ",")}, + "vs_currencies": {currency}, + } + + var resp simplePriceResponse + if err := c.doReq(ctx, endpointSimplePrice, params, &resp); err != nil { + return nil, err + } + + prices := make(map[tokens.Token]float64) + for id, price := range resp { + prices[tokens.MustFromCoingeckoID(id)] = price[currency] + } + + return prices, nil +} + +// doReq makes a GET request to the given path & params, and decodes the response into response. +func (c Client) doReq(ctx context.Context, path string, params url.Values, response any) error { + uri, err := c.uri(path, params) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return errors.Wrap(err, "create request", "url", uri.String()) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return errors.Wrap(err, "get", "url", uri.String()) + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.New("get", "url", uri.String(), "status", resp.Status) + } + + if err := json.NewDecoder(resp.Body).Decode(response); err != nil { + return errors.Wrap(err, "decode response") + } + + return nil +} + +func (c Client) uri(path string, params url.Values) (*url.URL, error) { + uri, err := url.Parse(c.host + path + "?" + params.Encode()) + if err != nil { + return nil, errors.Wrap(err, "parse url", "host", c.host, "path", path, "params", params.Encode()) + } + + return uri, nil +} diff --git a/lib/tokens/coingecko/coingecko_test.go b/lib/tokens/coingecko/coingecko_test.go new file mode 100644 index 00000000..0121388d --- /dev/null +++ b/lib/tokens/coingecko/coingecko_test.go @@ -0,0 +1,65 @@ +package coingecko_test + +import ( + "context" + "encoding/json" + "math/rand" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/tokens" + "github.com/piplabs/story/lib/tokens/coingecko" + + "gotest.tools/v3/assert" +) + +func TestGetPrice(t *testing.T) { + t.Parallel() + + // map token id -> currency -> price + // set during request handler + testPrices := make(map[string]map[string]float64) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v3/simple/price", r.URL.Path) + + q := r.URL.Query() + ids := strings.Split(q.Get("ids"), ",") + currencies := strings.Split(q.Get("vs_currencies"), ",") + + resp := make(map[string]map[string]float64) + for _, id := range ids { + resp[id] = make(map[string]float64) + + if _, ok := testPrices[id]; !ok { + testPrices[id] = make(map[string]float64) + } + + for _, currency := range currencies { + resp[id][currency] = randPrice() + + // also store the price, so we can assert against it + testPrices[id][currency] = resp[id][currency] + } + } + + bz, _ := json.Marshal(resp) + _, _ = w.Write(bz) + })) + + defer ts.Close() + + c := coingecko.New(coingecko.WithHost(ts.URL)) + prices, err := c.Price(context.Background(), tokens.ILIAD, tokens.ETH) + require.NoError(t, err) + require.InEpsilon(t, prices[tokens.ILIAD], testPrices[tokens.ILIAD.CoingeckoID()]["usd"], 0.01) + require.InEpsilon(t, prices[tokens.ETH], testPrices[tokens.ETH.CoingeckoID()]["usd"], 0.01) +} + +func randPrice() float64 { + return float64(int(rand.Float64()*10000)) / 100 +} diff --git a/lib/tokens/coingecko/options.go b/lib/tokens/coingecko/options.go new file mode 100644 index 00000000..55021a06 --- /dev/null +++ b/lib/tokens/coingecko/options.go @@ -0,0 +1,17 @@ +package coingecko + +type options struct { + Host string +} + +func WithHost(host string) func(*options) { + return func(o *options) { + o.Host = host + } +} + +func defaultOpts() options { + return options{ + Host: prodHost, + } +} diff --git a/lib/tokens/mock.go b/lib/tokens/mock.go new file mode 100644 index 00000000..198941d2 --- /dev/null +++ b/lib/tokens/mock.go @@ -0,0 +1,41 @@ +package tokens + +import ( + "context" + "sync" +) + +type MockPricer struct { + mu sync.RWMutex + prices map[Token]float64 +} + +var _ Pricer = (*MockPricer)(nil) + +func NewMockPricer(prices map[Token]float64) *MockPricer { + cloned := make(map[Token]float64) + for k, v := range prices { + cloned[k] = v + } + + return &MockPricer{prices: cloned} +} + +func (m *MockPricer) Price(_ context.Context, tkns ...Token) (map[Token]float64, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + resp := make(map[Token]float64) + for _, t := range tkns { + resp[t] = m.prices[t] + } + + return resp, nil +} + +func (m *MockPricer) SetPrice(token Token, price float64) { + m.mu.Lock() + defer m.mu.Unlock() + + m.prices[token] = price +} diff --git a/lib/tokens/price.go b/lib/tokens/price.go new file mode 100644 index 00000000..d01ffeaf --- /dev/null +++ b/lib/tokens/price.go @@ -0,0 +1,57 @@ +package tokens + +import ( + "context" +) + +// Pricer is the token price provider interface. +type Pricer interface { + // Price returns the price of each provided token in USD. + Price(ctx context.Context, tokens ...Token) (map[Token]float64, error) +} + +type CachedPricer struct { + p Pricer + cache map[Token]float64 +} + +func NewCachedPricer(p Pricer) *CachedPricer { + return &CachedPricer{ + p: p, + cache: make(map[Token]float64), + } +} + +func (c *CachedPricer) Price(ctx context.Context, tokens ...Token) (map[Token]float64, error) { + prices := make(map[Token]float64) + + var uncached []Token + + for _, token := range tokens { + if price, ok := c.cache[token]; ok { + prices[token] = price + } else { + uncached = append(uncached, token) + } + } + + if len(uncached) == 0 { + return prices, nil + } + + newPrices, err := c.p.Price(ctx, uncached...) + if err != nil { + return nil, err + } + + for token, price := range newPrices { + prices[token] = price + c.cache[token] = price + } + + return prices, nil +} + +func (c *CachedPricer) ClearCache() { + c.cache = make(map[Token]float64) +} diff --git a/lib/tokens/tokens.go b/lib/tokens/tokens.go new file mode 100644 index 00000000..5a3c94af --- /dev/null +++ b/lib/tokens/tokens.go @@ -0,0 +1,42 @@ +package tokens + +type Token string + +const ( + ILIAD Token = "ILIAD" + ETH Token = "ETH" +) + +var ( + coingeckoIDs = map[Token]string{ + ILIAD: "storyprotocol", + ETH: "ethereum", + } +) + +func (t Token) String() string { + return string(t) +} + +func (t Token) CoingeckoID() string { + return coingeckoIDs[t] +} + +func FromCoingeckoID(id string) (Token, bool) { + for t, i := range coingeckoIDs { + if i == id { + return t, true + } + } + + return "", false +} + +func MustFromCoingeckoID(id string) Token { + t, ok := FromCoingeckoID(id) + if !ok { + panic("unknown coingecko id: " + id) + } + + return t +} diff --git a/lib/tracer/config.go b/lib/tracer/config.go new file mode 100644 index 00000000..f80f388f --- /dev/null +++ b/lib/tracer/config.go @@ -0,0 +1,77 @@ +package tracer + +import ( + "net/url" + "strings" + + "github.com/spf13/pflag" + + "github.com/piplabs/story/lib/errors" +) + +// BindFlags binds the provided flags to the corresponding fields in the Config struct. +func BindFlags(flags *pflag.FlagSet, cfg *Config) { + flags.StringVar(&cfg.Endpoint, "tracing-endpoint", cfg.Endpoint, "Tracing OTLP endpoint") + flags.StringVar(&cfg.Headers, "tracing-headers", cfg.Headers, "Tracing OTLP headers") +} + +// DefaultConfig returns the default empty configuration for OTLP tracing. +func DefaultConfig() Config { + return Config{} +} + +// Config defines OTLP config for grafana cloud. +// See https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/setup/quickstart/go/ +type Config struct { + Endpoint string // E.g. "https://otlp-gateway-prod-us-east-0.grafana.net/otlp" + Headers string // E.g. "Authorization=Basic NzQk..3O34" +} + +func (c Config) toOpts() (func(*options), error) { + if c.Endpoint == "" { + return func(_ *options) {}, nil + } + + headers := make(map[string]string) + if len(c.Headers) > 0 { + var err error + headers, err = stringToHeader(c.Headers) + if err != nil { + return nil, err + } + } + + return WithOTLP(c.Endpoint, headers), nil +} + +// stringToHeader converts a string of comma-separated header key-value pairs +// into a map[string]string. +// Copied from go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@v1.24.0/internal/envconfig/envconfig.go:167. +func stringToHeader(value string) (map[string]string, error) { + headersPairs := strings.Split(value, ",") + headers := make(map[string]string) + + for _, header := range headersPairs { + n, v, found := strings.Cut(header, "=") + if !found { + return nil, errors.New("missing '='", "header", header) + } + + name, err := url.PathUnescape(n) + if err != nil { + return nil, errors.Wrap(err, "escape header key", "key", n) + } + + trimmedName := strings.TrimSpace(name) + value, err := url.PathUnescape(v) + if err != nil { + return nil, errors.Wrap(err, "escape header value", "value", v) + } + + trimmedValue := strings.TrimSpace(value) + + headers[trimmedName] = trimmedValue + } + + return headers, nil +} diff --git a/lib/tracer/trace.go b/lib/tracer/trace.go new file mode 100644 index 00000000..224eeb8c --- /dev/null +++ b/lib/tracer/trace.go @@ -0,0 +1,154 @@ +// Package tracer provides a global OpenTelemetry tracer. +package tracer + +import ( + "context" + "io" + "sync" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/netconf" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" +) + +var ( + // tracer is the global app level tracer, it defaults to a noop tracer. + tracer = noop.NewTracerProvider().Tracer("") + tracerMu sync.RWMutex +) + +// Start creates a span and a context.Context containing the newly-created span from the global tracer. +// See go.opentelemetry.io/otel/trace#Start for more details. +func Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + tracerMu.RLock() + defer tracerMu.RUnlock() + + return tracer.Start(ctx, spanName, opts...) //nolint:spancheck // spanName is a string literal +} + +// RootedCtx returns a copy of the parent context containing a tracing span context +// rooted to the trace ID. All spans started from the context will be rooted to the trace ID. +func RootedCtx(ctx context.Context, traceID trace.TraceID) context.Context { + return trace.ContextWithSpanContext(ctx, trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + })) +} + +// Identifiers defines the global tracer attributes +// That uniquely identifies the network/service/instance +// that produced each trace. +type Identifiers struct { + Network netconf.ID + Service string // client + Instance string // validator01/seed01 +} + +// Init initializes the global tracer via the option(s) defaulting to a noop tracer. It returns a shutdown function. +func Init(ctx context.Context, ids Identifiers, cfg Config, opts ...func(*options)) (func(context.Context) error, error) { + tracerMu.Lock() + defer tracerMu.Unlock() + + cfgOpt, err := cfg.toOpts() + if err != nil { + return nil, err + } + + var o options + for _, opt := range append(opts, cfgOpt) { + opt(&o) + } + + if o.exporterFunc == nil { + return func(context.Context) error { + return nil + }, nil + } + + exporter, err := o.exporterFunc(ctx) + if err != nil { + return nil, err + } + + tp, err := newTraceProvider(exporter, ids) + if err != nil { + return nil, err + } + + // Set globals + otel.SetTracerProvider(tp) + tracer = tp.Tracer("") + + return tp.Shutdown, nil +} + +type options struct { + exporterFunc func(context.Context) (sdktrace.SpanExporter, error) +} + +// WithStdOut returns an option to configure an OpenTelemetry exporter for tracing +// telemetry to be written to an output destination as JSON. +func WithStdOut(w io.Writer) func(*options) { + return func(o *options) { + o.exporterFunc = func(context.Context) (sdktrace.SpanExporter, error) { + ex, err := stdouttrace.New(stdouttrace.WithWriter(w)) + if err != nil { + return nil, errors.Wrap(err, "stdout trace exporter") + } + + return ex, nil + } + } +} + +// WithOTLP returns an option to configure an OpenTelemetry tracing exporter for Jaeger. +func WithOTLP(endpoint string, headers map[string]string) func(*options) { + return func(o *options) { + o.exporterFunc = func(ctx context.Context) (sdktrace.SpanExporter, error) { + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpointURL(endpoint), + otlptracehttp.WithHeaders(headers), + } + + ex, err := otlptracehttp.New(ctx, opts...) + if err != nil { + return nil, errors.Wrap(err, "otlp exporter") + } + + return ex, nil + } + } +} + +func newTraceProvider(exp sdktrace.SpanExporter, ids Identifiers) (*sdktrace.TracerProvider, error) { + r, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes(semconv.SchemaURL, + semconv.ServiceName(ids.Service), + semconv.ServiceInstanceID(ids.Instance), + semconv.DeploymentEnvironment(ids.Network.String()), + )) + if err != nil { + return nil, errors.Wrap(err, "merge resource") + } + + return sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithBatcher(exp), + sdktrace.WithResource(r), + ), nil +} + +// AddEvent adds an event to the span in the given context with the specified name and attributes. +// See go.opentelemetry.io/otel/trace#Span.AddEvent for more details. +func AddEvent(ctx context.Context, name string, attrs ...attribute.KeyValue) { + trace.SpanFromContext(ctx).AddEvent(name, trace.WithAttributes(attrs...)) +} diff --git a/lib/tutil/golden.go b/lib/tutil/golden.go new file mode 100644 index 00000000..a3209e47 --- /dev/null +++ b/lib/tutil/golden.go @@ -0,0 +1,77 @@ +// Package tutil provides test utilities. +// +//nolint:gochecknoglobals,gomnd // These not an issue for simple testing library. +package tutil + +import ( + "encoding/json" + "flag" + "os" + "path" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + // Note, had to change flag from 'update' to 'golden' to avoid conflicts with cosmos-sdk. + update = flag.Bool("golden", false, "Create or update golden files, instead of comparing them") + clean = flag.Bool("clean", false, "Deletes the testdata folder before updating (noop of update==false)") +) + +var cleanOnce sync.Once + +// WithFilename configures a custom golden test filename. +func WithFilename(name string) func(*string) { + return func(filename *string) { + *filename = name + } +} + +// RequireGoldenBytes asserts that a golden testdata file exists containing the exact data. +// This is heavily inspired from https://github.com/sebdah/goldie. +func RequireGoldenBytes(t *testing.T, data []byte, opts ...func(*string)) { + t.Helper() + + filename := strings.ReplaceAll(t.Name(), "/", "_") + ".golden" + for _, opt := range opts { + opt(&filename) + } + filename = path.Join("testdata", filename) + + if *update { + if *clean { + cleanOnce.Do(func() { + _ = os.RemoveAll("testdata") + }) + } + + require.NoError(t, os.MkdirAll("testdata", 0o755)) + + _ = os.Remove(filename) + require.NoError(t, os.WriteFile(filename, data, 0o644)) + + return + } + + expected, err := os.ReadFile(filename) + if os.IsNotExist(err) { + t.Fatalf("golden file does not exist, %s, generate by running with -golden", filename) + return + } + + require.Equalf(t, string(expected), string(data), "Golden file mismatch, %s", filename) +} + +// RequireGoldenJSON asserts that a golden testdata file exists containing the JSON serialized form of the data object. +// This is heavily inspired from https://github.com/sebdah/goldie. +func RequireGoldenJSON(t *testing.T, data any, opts ...func(*string)) { + t.Helper() + + b, err := json.MarshalIndent(data, "", " ") + require.NoError(t, err) + + RequireGoldenBytes(t, b, opts...) +} diff --git a/lib/tutil/random.go b/lib/tutil/random.go new file mode 100644 index 00000000..46ffda2a --- /dev/null +++ b/lib/tutil/random.go @@ -0,0 +1,23 @@ +package tutil + +import ( + "crypto/rand" + + "github.com/ethereum/go-ethereum/common" +) + +// RandomBytes returns a random byte slice of length l. +func RandomBytes(l int) []byte { + resp := make([]byte, l) + _, _ = rand.Read(resp) + + return resp +} + +// RandomHash returns a random 32-byte 256-bit hash. +func RandomHash() common.Hash { + var resp common.Hash + _, _ = rand.Read(resp[:]) + + return resp +} diff --git a/lib/tutil/require.go b/lib/tutil/require.go new file mode 100644 index 00000000..87685781 --- /dev/null +++ b/lib/tutil/require.go @@ -0,0 +1,23 @@ +//nolint:testifylint // Using assert here to log error afterwards. +package tutil + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/piplabs/story/lib/log" +) + +// RequireNoError asserts that err is nil. It also logs the error to show the stacktrace. +// +// This can be used instead of require.NoError(t, err) to show the stacktrace in case of error. +func RequireNoError(tb testing.TB, err error) { + tb.Helper() + + if !assert.NoErrorf(tb, err, "See log line for error details") { + log.Error(context.Background(), "Unexpected error", err) + tb.FailNow() + } +} diff --git a/lib/tutil/rpctest.go b/lib/tutil/rpctest.go new file mode 100644 index 00000000..4a96f4e7 --- /dev/null +++ b/lib/tutil/rpctest.go @@ -0,0 +1,42 @@ +package tutil + +import ( + "os" + "testing" + + "github.com/cometbft/cometbft/config" + rpctest "github.com/cometbft/cometbft/rpc/test" + "github.com/stretchr/testify/require" + + _ "embed" +) + +var ( + //go:embed testdata/genesis.json + genesisJSON []byte + + //go:embed testdata/priv-validator-key.json + privValKeyJSON []byte + + //go:embed testdata/priv-validator-state.json + privValStateJSON []byte +) + +// PrepRPCTestConfig creates the require cometbft config on disk for rpctest package to work with iliad app. +func PrepRPCTestConfig(t *testing.T) *config.Config { + t.Helper() + + // Write genesis and priv validator files to temp dir. + conf := rpctest.GetConfig(true) + + err := os.WriteFile(conf.GenesisFile(), genesisJSON, 0o644) + require.NoError(t, err) + + err = os.WriteFile(conf.PrivValidatorKeyFile(), privValKeyJSON, 0o644) + require.NoError(t, err) + + err = os.WriteFile(conf.PrivValidatorStateFile(), privValStateJSON, 0o644) + require.NoError(t, err) + + return conf +} diff --git a/lib/tutil/testdata/genesis.json b/lib/tutil/testdata/genesis.json new file mode 100644 index 00000000..0b5da87f --- /dev/null +++ b/lib/tutil/testdata/genesis.json @@ -0,0 +1,37 @@ +{ + "genesis_time": "2018-10-10T08:20:13.695936996Z", + "chain_id": "123", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "10" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "secp256k1" + ] + }, + "abci": { + "vote_extensions_enable_height": "1" + }, + "version": {} + }, + "validators": [ + { + "pub_key": { + "type":"tendermint/PubKeySecp256k1", + "value":"Aq26clpbUpVA96jt8PY5IBsLwytiAyNOw+NDVqKfFGYd" + }, + "power": "10", + "name": "" + } + ], + "app_hash": "" +} diff --git a/lib/tutil/testdata/priv-validator-key.json b/lib/tutil/testdata/priv-validator-key.json new file mode 100644 index 00000000..e2e8a24d --- /dev/null +++ b/lib/tutil/testdata/priv-validator-key.json @@ -0,0 +1,11 @@ +{ + "address": "8F23A4296EC8125768246318C91D17E4267A4CA4", + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Aq26clpbUpVA96jt8PY5IBsLwytiAyNOw+NDVqKfFGYd" + }, + "priv_key": { + "type": "tendermint/PrivKeySecp256k1", + "value": "ORd7ywhSOc9urVM9bWKujsWy1yA1JAYNesXHaL6TYcU=" + } +} \ No newline at end of file diff --git a/lib/tutil/testdata/priv-validator-state.json b/lib/tutil/testdata/priv-validator-state.json new file mode 100644 index 00000000..48f3b67e --- /dev/null +++ b/lib/tutil/testdata/priv-validator-state.json @@ -0,0 +1,5 @@ +{ + "height": "0", + "round": 0, + "step": 0 +} \ No newline at end of file diff --git a/lib/tutil/tmpdir.go b/lib/tutil/tmpdir.go new file mode 100644 index 00000000..480c554a --- /dev/null +++ b/lib/tutil/tmpdir.go @@ -0,0 +1,31 @@ +package tutil + +import ( + "crypto/rand" + "math/big" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" +) + +// TempDir creates a temporary directory with a random name, as opposed to t.TempDir which +// uses consecutive numbers per test (001, 002 ...). +func TempDir(t *testing.T) string { + t.Helper() + + dir, err := os.MkdirTemp(t.TempDir(), randStr(t)) + require.NoError(t, err) + + return dir +} + +func randStr(t *testing.T) string { + t.Helper() + + n, err := rand.Int(rand.Reader, big.NewInt(1e18)) + require.NoError(t, err) + + return hexutil.EncodeBig(n) +} diff --git a/lib/txmgr/README.md b/lib/txmgr/README.md new file mode 100644 index 00000000..afef5d2b --- /dev/null +++ b/lib/txmgr/README.md @@ -0,0 +1,3 @@ +# txmgr package + +This package has been copied and modified from the [optimism](https://github.com/ethereum-optimism/optimism/tree/develop/op-service/txmgr) repository. diff --git a/lib/txmgr/config.go b/lib/txmgr/config.go new file mode 100644 index 00000000..7733c122 --- /dev/null +++ b/lib/txmgr/config.go @@ -0,0 +1,309 @@ +package txmgr + +import ( + "context" + "crypto/ecdsa" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" +) + +type DefaultFlagValues struct { + NumConfirmations uint64 + SafeAbortNonceTooLowCount uint64 + FeeLimitMultiplier uint64 + FeeLimitThresholdGwei float64 + ResubmissionTimeout time.Duration + NetworkTimeout time.Duration + TxSendTimeout time.Duration + TxNotInMempoolTimeout time.Duration +} + +type CLIConfig struct { + ChainID uint64 + NumConfirmations uint64 + SafeAbortNonceTooLowCount uint64 + FeeLimitMultiplier uint64 + FeeLimitThresholdGwei float64 + MinBaseFeeGwei float64 + MinTipCapGwei float64 + ResubmissionTimeout time.Duration + ReceiptQueryInterval time.Duration + NetworkTimeout time.Duration + TxSendTimeout time.Duration + TxNotInMempoolTimeout time.Duration +} + +var ( + //nolint:gochecknoglobals // should be configurable + DefaultSenderFlagValues = DefaultFlagValues{ + NumConfirmations: uint64(1), + SafeAbortNonceTooLowCount: uint64(3), + FeeLimitMultiplier: uint64(5), + FeeLimitThresholdGwei: 100.0, + ResubmissionTimeout: 48 * time.Second, + NetworkTimeout: 30 * time.Second, + TxSendTimeout: 20 * time.Minute, + TxNotInMempoolTimeout: 2 * time.Minute, + } +) + +func NewCLIConfig(chainID uint64, interval time.Duration, defaults DefaultFlagValues) CLIConfig { + return CLIConfig{ + NumConfirmations: defaults.NumConfirmations, + SafeAbortNonceTooLowCount: defaults.SafeAbortNonceTooLowCount, + FeeLimitMultiplier: defaults.FeeLimitMultiplier, + FeeLimitThresholdGwei: defaults.FeeLimitThresholdGwei, + ResubmissionTimeout: defaults.ResubmissionTimeout, + NetworkTimeout: defaults.NetworkTimeout, + TxSendTimeout: defaults.TxSendTimeout, + TxNotInMempoolTimeout: defaults.TxNotInMempoolTimeout, + ReceiptQueryInterval: interval, + ChainID: chainID, + } +} + +func (m CLIConfig) Check() error { + if m.NumConfirmations == 0 { + return errors.New("numConfirmations must not be 0") + } + if m.NetworkTimeout == 0 { + return errors.New("must provide NetworkTimeout") + } + if m.FeeLimitMultiplier == 0 { + return errors.New("must provide FeeLimitMultiplier") + } + if m.MinBaseFeeGwei < m.MinTipCapGwei { + return errors.New("minBaseFee smaller than minTipCap", + m.MinBaseFeeGwei, m.MinTipCapGwei) + } + if m.ResubmissionTimeout == 0 { + return errors.New("must provide ResubmissionTimeout") + } + if m.ReceiptQueryInterval == 0 { + return errors.New("must provide ReceiptQueryInterval") + } + if m.TxNotInMempoolTimeout == 0 { + return errors.New("must provide TxNotInMempoolTimeout") + } + if m.SafeAbortNonceTooLowCount == 0 { + return errors.New("safeAbortNonceTooLowCount must not be 0") + } + + return nil +} + +func externalSignerFn(external ExternalSigner, address common.Address, chainID uint64) SignerFn { + signer := types.LatestSignerForChainID(big.NewInt(int64(chainID))) + return func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { + if address != from { + return nil, bind.ErrNotAuthorized + } + + sig, err := external(ctx, signer.Hash(tx), from) + if err != nil { + return nil, errors.Wrap(err, "external sign") + } + + res, err := tx.WithSignature(signer, sig[:]) + if err != nil { + return nil, errors.Wrap(err, "set signature") + } + + return res, nil + } +} + +// privateKeySignerFn returns a SignerFn that signs transactions with the given private key. +func privateKeySignerFn(key *ecdsa.PrivateKey, chainID uint64) SignerFn { + from := crypto.PubkeyToAddress(key.PublicKey) + signer := types.LatestSignerForChainID(big.NewInt(int64(chainID))) + + return func(_ context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if address != from { + return nil, bind.ErrNotAuthorized + } + signature, err := crypto.Sign(signer.Hash(tx).Bytes(), key) + if err != nil { + return nil, errors.Wrap(err, "could not sign transaction") + } + res, err := tx.WithSignature(signer, signature) + if err != nil { + return nil, errors.Wrap(err, "could not sign transaction") + } + + return res, nil + } +} + +// ExternalSigner is a function that signs a transaction with a given address and returns the signature. +type ExternalSigner func(context.Context, common.Hash, common.Address) ([65]byte, error) + +// SignerFn is a generic transaction signing function. It may be a remote signer so it takes a context. +// It also takes the address that should be used to sign the transaction with. +type SignerFn func(context.Context, common.Address, *types.Transaction) (*types.Transaction, error) + +// SignerFactory creates a SignerFn that is bound to a specific chainID. +type SignerFactory func(chainID *big.Int) SignerFn + +// Config houses parameters for altering the behavior of a simple. +type Config struct { + Backend ethclient.Client + + // ResubmissionTimeout is the interval at which, if no previously + // published transaction has been mined, the new tx with a bumped gas + // price will be published. Only one publication at MaxGasPrice will be + // attempted. + ResubmissionTimeout time.Duration + + // The multiplier applied to fee suggestions to put a hard limit on fee increases. + FeeLimitMultiplier uint64 + + // Minimum threshold (in Wei) at which the FeeLimitMultiplier takes effect. + // On low-fee networks, like test networks, this allows for arbitrary fee bumps + // below this threshold. + FeeLimitThreshold *big.Int + + // Minimum base fee (in Wei) to assume when determining tx fees. + MinBaseFee *big.Int + + // Minimum tip cap (in Wei) to enforce when determining tx fees. + MinTipCap *big.Int + + // ChainID is the chain ID of the L1 chain. + ChainID *big.Int + + // TxSendTimeout is how long to wait for sending a transaction. + // By default, it is unbounded. If set, this is recommended to be at least 20 minutes. + TxSendTimeout time.Duration + + // TxNotInMempoolTimeout is how long to wait before aborting a transaction doSend if the transaction does not + // make it to the mempool. If the tx is in the mempool, TxSendTimeout is used instead. + TxNotInMempoolTimeout time.Duration + + // NetworkTimeout is the allowed duration for a single network request. + // This is intended to be used for network requests that can be replayed. + // todo(lazar): this should be handled by eth client + NetworkTimeout time.Duration + + // RequireQueryInterval is the interval at which the tx manager will + // query the backend to check for confirmations after a tx at a + // specific gas price has been published. + ReceiptQueryInterval time.Duration + + // NumConfirmations specifies how many blocks are need to consider a + // transaction confirmed. + NumConfirmations uint64 + + // SafeAbortNonceTooLowCount specifies how many ErrNonceTooLow observations + // are required to give up on a tx at a particular nonce without receiving + // confirmation. + SafeAbortNonceTooLowCount uint64 + + // Signer is used to sign transactions when the gas price is increased. + Signer SignerFn + + From common.Address +} + +// NewConfigWithSigner returns a new txmgr config from the given CLI config and external signer. +func NewConfigWithSigner(cfg CLIConfig, external ExternalSigner, from common.Address, client ethclient.Client) (Config, error) { + signer := externalSignerFn(external, from, cfg.ChainID) + + return newConfig(cfg, signer, from, client) +} + +// NewConfig returns a new txmgr config from the given CLI config and private key. +func NewConfig(cfg CLIConfig, privateKey *ecdsa.PrivateKey, client ethclient.Client) (Config, error) { + signer := privateKeySignerFn(privateKey, cfg.ChainID) + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + return newConfig(cfg, signer, addr, client) +} + +func newConfig(cfg CLIConfig, signer SignerFn, from common.Address, client ethclient.Client) (Config, error) { + if err := cfg.Check(); err != nil { + return Config{}, errors.New("invalid config", err) + } + + feeLimitThreshold, err := GweiToWei(cfg.FeeLimitThresholdGwei) + if err != nil { + return Config{}, errors.Wrap(err, "invalid fee limit threshold") + } + + minBaseFee, err := GweiToWei(cfg.MinBaseFeeGwei) + if err != nil { + return Config{}, errors.Wrap(err, "invalid min base fee") + } + + minTipCap, err := GweiToWei(cfg.MinTipCapGwei) + if err != nil { + return Config{}, errors.Wrap(err, "invalid min tip cap") + } + + chainID := big.NewInt(int64(cfg.ChainID)) + + return Config{ + Backend: client, + ResubmissionTimeout: cfg.ResubmissionTimeout, + FeeLimitMultiplier: cfg.FeeLimitMultiplier, + FeeLimitThreshold: feeLimitThreshold, + MinBaseFee: minBaseFee, + MinTipCap: minTipCap, + ChainID: chainID, + TxSendTimeout: cfg.TxSendTimeout, + TxNotInMempoolTimeout: cfg.TxNotInMempoolTimeout, + NetworkTimeout: cfg.NetworkTimeout, + ReceiptQueryInterval: cfg.ReceiptQueryInterval, + NumConfirmations: cfg.NumConfirmations, + SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount, + Signer: signer, + From: from, + }, nil +} + +func (m Config) Check() error { + if m.Backend == nil { + return errors.New("must provide the backend") + } + if m.NumConfirmations == 0 { + return errors.New("numConfirmations must not be 0") + } + if m.NetworkTimeout == 0 { + return errors.New("must provide NetworkTimeout") + } + if m.FeeLimitMultiplier == 0 { + return errors.New("must provide FeeLimitMultiplier") + } + if m.MinBaseFee != nil && m.MinTipCap != nil && m.MinBaseFee.Cmp(m.MinTipCap) == -1 { + return errors.New("minBaseFee smaller than minTipCap", + m.MinBaseFee, m.MinTipCap) + } + if m.ResubmissionTimeout == 0 { + return errors.New("must provide ResubmissionTimeout") + } + if m.ReceiptQueryInterval == 0 { + return errors.New("must provide ReceiptQueryInterval") + } + if m.TxNotInMempoolTimeout == 0 { + return errors.New("must provide TxNotInMempoolTimeout") + } + if m.SafeAbortNonceTooLowCount == 0 { + return errors.New("safeAbortNonceTooLowCount must not be 0") + } + if m.Signer == nil { + return errors.New("must provide the Signer") + } + if m.ChainID == nil { + return errors.New("must provide the chainID") + } + + return nil +} diff --git a/lib/txmgr/ether.go b/lib/txmgr/ether.go new file mode 100644 index 00000000..6a8d30c5 --- /dev/null +++ b/lib/txmgr/ether.go @@ -0,0 +1,30 @@ +package txmgr + +import ( + "math" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/params" + + "github.com/piplabs/story/lib/errors" +) + +// GweiToWei converts a float64 GWei value into a big.Int Wei value. +func GweiToWei(gwei float64) (*big.Int, error) { + if math.IsNaN(gwei) || math.IsInf(gwei, 0) { + return nil, errors.New("invalid gwei value", gwei) + } + + // convert float GWei value into integer Wei value + wei, _ := new(big.Float).Mul( + big.NewFloat(gwei), + big.NewFloat(params.GWei)). + Int(nil) + + if wei.Cmp(abi.MaxUint256) == 1 { + return nil, errors.New("gwei value larger than max uint256") + } + + return wei, nil +} diff --git a/lib/txmgr/price_bump_internal_test.go b/lib/txmgr/price_bump_internal_test.go new file mode 100644 index 00000000..7c003172 --- /dev/null +++ b/lib/txmgr/price_bump_internal_test.go @@ -0,0 +1,102 @@ +package txmgr + +import ( + "context" + "math/big" + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +type priceBumpTest struct { + prevGasTip int64 + prevBaseFee int64 + newGasTip int64 + newBaseFee int64 + expectedTip int64 + expectedFC int64 +} + +func (tc *priceBumpTest) run(t *testing.T) { + t.Helper() + prevFC := calcGasFeeCap(big.NewInt(tc.prevBaseFee), big.NewInt(tc.prevGasTip)) + + tip, fc := updateFees(context.Background(), big.NewInt(tc.prevGasTip), prevFC, big.NewInt(tc.newGasTip), + big.NewInt(tc.newBaseFee)) + + require.Equal(t, tc.expectedTip, tip.Int64(), "tip must be as expected") + require.Equal(t, tc.expectedFC, fc.Int64(), "fee cap must be as expected") +} + +func TestUpdateFees(t *testing.T) { + t.Parallel() + require.Equal(t, int64(10), PriceBump, "test must be updated if priceBump is adjusted") + tests := []priceBumpTest{ + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 90, newBaseFee: 900, + expectedTip: 110, expectedFC: 2310, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 101, newBaseFee: 1000, + expectedTip: 110, expectedFC: 2310, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 100, newBaseFee: 1001, + expectedTip: 110, expectedFC: 2310, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 101, newBaseFee: 900, + expectedTip: 110, expectedFC: 2310, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 90, newBaseFee: 1010, + expectedTip: 110, expectedFC: 2310, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 101, newBaseFee: 2000, + expectedTip: 110, expectedFC: 4110, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 120, newBaseFee: 900, + expectedTip: 120, expectedFC: 2310, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 120, newBaseFee: 1100, + expectedTip: 120, expectedFC: 2320, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 120, newBaseFee: 1140, + expectedTip: 120, expectedFC: 2400, + }, + + { + prevGasTip: 100, prevBaseFee: 1000, + newGasTip: 120, newBaseFee: 1200, + expectedTip: 120, expectedFC: 2520, + }, + } + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + test.run(t) + }) + } +} diff --git a/lib/txmgr/send_state.go b/lib/txmgr/send_state.go new file mode 100644 index 00000000..ce3759d3 --- /dev/null +++ b/lib/txmgr/send_state.go @@ -0,0 +1,127 @@ +package txmgr + +import ( + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" +) + +// SendState tracks information about the publication state of a given txn. In +// this context, a txn may correspond to multiple different txn hashes due to +// varying gas prices, though we treat them all as the same logical txn. This +// struct is primarily used to determine whether the txmgr should abort a +// given txn. +type SendState struct { + minedTxs map[common.Hash]struct{} + mu sync.RWMutex + now func() time.Time + + // Config + nonceTooLowCount uint64 + txInMempoolDeadline time.Time // deadline to abort at if no transactions are in the mempool + + // Counts of the different types of errors + successFullPublishCount uint64 // nil error => tx made it to the mempool + safeAbortNonceTooLowCount uint64 // nonce too low error + + // Miscellaneous tracking + bumpCount int // number of times we have bumped the gas price +} + +// NewSendStateWithNow creates a new doSend state with the provided clock. +func NewSendStateWithNow(nonceTooLowCount uint64, timeout time.Duration, + now func() time.Time) *SendState { + if nonceTooLowCount == 0 { + panic("txmgr: nonceTooLowCount cannot be zero") + } + + return &SendState{ + minedTxs: make(map[common.Hash]struct{}), + safeAbortNonceTooLowCount: nonceTooLowCount, + txInMempoolDeadline: now().Add(timeout), + now: now, + } +} + +// NewSendState creates a new doSend state. +func NewSendState(nonceTooLowCount uint64, timeout time.Duration) *SendState { + return NewSendStateWithNow(nonceTooLowCount, timeout, time.Now) +} + +// ProcessSendError should be invoked with the error returned for each +// publication. It is safe to call this method with nil or arbitrary errors. +func (s *SendState) ProcessSendError(err error) { + s.mu.Lock() + defer s.mu.Unlock() + + // Record the type of error + switch { + case err == nil: + s.successFullPublishCount++ + case strings.Contains(err.Error(), core.ErrNonceTooLow.Error()): + s.nonceTooLowCount++ + } +} + +// TxMined records that the txn with txnHash has been mined and is await +// confirmation. It is safe to call this function multiple times. +func (s *SendState) TxMined(txHash common.Hash) { + s.mu.Lock() + defer s.mu.Unlock() + + s.minedTxs[txHash] = struct{}{} +} + +// TxNotMined records that the txn with txnHash has not been mined or has been +// reorg'd out. It is safe to call this function multiple times. +func (s *SendState) TxNotMined(txHash common.Hash) { + s.mu.Lock() + defer s.mu.Unlock() + + _, wasMined := s.minedTxs[txHash] + delete(s.minedTxs, txHash) + + // If the txn got reorged and left us with no mined txns, reset the nonce + // too low count, otherwise we might abort too soon when processing the next + // error. If the nonce too low errors persist, we want to ensure we wait out + // the full safe abort count to ensure we have a sufficient number of + // observations. + if len(s.minedTxs) == 0 && wasMined { + s.nonceTooLowCount = 0 + } +} + +// ShouldAbortImmediately returns true if the txmgr should give up on trying a +// given txn with the target nonce. +// This occurs when the set of errors recorded indicates that no further progress can be made +// on this transaction. +func (s *SendState) ShouldAbortImmediately() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + // Never abort if our latest sample reports having at least one mined txn. + if len(s.minedTxs) > 0 { + return false + } + + // If we have exceeded the nonce too low count, abort + if s.nonceTooLowCount >= s.safeAbortNonceTooLowCount || + // If we have not published a transaction in the allotted time, abort + (s.successFullPublishCount == 0 && s.now().After(s.txInMempoolDeadline)) { + return true + } + + return false +} + +// IsWaitingForConfirmation returns true if we have at least one confirmation on +// one of our txs. +func (s *SendState) IsWaitingForConfirmation() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return len(s.minedTxs) > 0 +} diff --git a/lib/txmgr/send_state_test.go b/lib/txmgr/send_state_test.go new file mode 100644 index 00000000..0decc7cb --- /dev/null +++ b/lib/txmgr/send_state_test.go @@ -0,0 +1,205 @@ +package txmgr_test + +import ( + "errors" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/txmgr" +) + +var ( + testHash = common.HexToHash("0x01") //nolint:gochecknoglobals // This is okay used for testing +) + +const testSafeAbortNonceTooLowCount = 3 + +func newSendState() *txmgr.SendState { + return newSendStateWithTimeout(time.Hour, time.Now) +} + +func newSendStateWithTimeout(t time.Duration, now func() time.Time) *txmgr.SendState { + return txmgr.NewSendStateWithNow(testSafeAbortNonceTooLowCount, t, now) +} + +func processNSendErrors(sendState *txmgr.SendState, err error) { + for range testSafeAbortNonceTooLowCount { + sendState.ProcessSendError(err) + } +} + +// TestSendStateNoAbortAfterInit asserts that the default SendState won't +// trigger an abort even after the safe abort interval has elapsed. +func TestSendStateNoAbortAfterInit(t *testing.T) { + t.Parallel() + sendState := newSendState() + require.False(t, sendState.ShouldAbortImmediately()) + require.False(t, sendState.IsWaitingForConfirmation()) +} + +// TestSendStateNoAbortAfterProcessNilError asserts that nil errors are not +// considered for abort status. +func TestSendStateNoAbortAfterProcessNilError(t *testing.T) { + t.Parallel() + sendState := newSendState() + + processNSendErrors(sendState, nil) + require.False(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateNoAbortAfterProcessOtherError asserts that non-nil errors other +// than ErrNonceTooLow are not considered for abort status. +func TestSendStateNoAbortAfterProcessOtherError(t *testing.T) { + t.Parallel() + sendState := newSendState() + + otherError := errors.New("other error") + processNSendErrors(sendState, otherError) + require.False(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateAbortSafelyAfterNonceTooLowButNoTxMined asserts that we will +// abort after the safe abort interval has elapsed if we haven't mined a tx. +func TestSendStateAbortSafelyAfterNonceTooLowButNoTxMined(t *testing.T) { + t.Parallel() + sendState := newSendState() + + sendState.ProcessSendError(core.ErrNonceTooLow) + require.False(t, sendState.ShouldAbortImmediately()) + sendState.ProcessSendError(core.ErrNonceTooLow) + require.False(t, sendState.ShouldAbortImmediately()) + sendState.ProcessSendError(core.ErrNonceTooLow) + require.True(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateMiningTxCancelsAbort asserts that a tx getting mined after +// processing ErrNonceTooLow takes precedence and doesn't cause an abort. +func TestSendStateMiningTxCancelsAbort(t *testing.T) { + t.Parallel() + sendState := newSendState() + + sendState.ProcessSendError(core.ErrNonceTooLow) + sendState.ProcessSendError(core.ErrNonceTooLow) + sendState.TxMined(testHash) + require.False(t, sendState.ShouldAbortImmediately()) + sendState.ProcessSendError(core.ErrNonceTooLow) + require.False(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateReorgingTxResetsAbort asserts that unmining a tx does not +// consider ErrNonceTooLow's prior to being mined when determining whether +// to abort. +func TestSendStateReorgingTxResetsAbort(t *testing.T) { + t.Parallel() + sendState := newSendState() + + sendState.ProcessSendError(core.ErrNonceTooLow) + sendState.ProcessSendError(core.ErrNonceTooLow) + sendState.TxMined(testHash) + sendState.TxNotMined(testHash) + sendState.ProcessSendError(core.ErrNonceTooLow) + require.False(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateNoAbortEvenIfNonceTooLowAfterTxMined asserts that we will not +// abort if we continue to get ErrNonceTooLow after a tx has been mined. +// +// NOTE: This is the most crucial role of the SendState, as we _expect_ to get +// ErrNonceTooLow failures after one of our txs has been mined, but that +// shouldn't cause us to not continue waiting for confirmations. +func TestSendStateNoAbortEvenIfNonceTooLowAfterTxMined(t *testing.T) { + t.Parallel() + sendState := newSendState() + + sendState.TxMined(testHash) + processNSendErrors( + sendState, core.ErrNonceTooLow, + ) + require.False(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateSafeAbortIfNonceTooLowPersistsAfterUnmine asserts that we will +// correctly abort if we continue to get ErrNonceTooLow after a tx is unmined +// but not remined. +func TestSendStateSafeAbortIfNonceTooLowPersistsAfterUnmine(t *testing.T) { + t.Parallel() + sendState := newSendState() + + sendState.TxMined(testHash) + sendState.TxNotMined(testHash) + sendState.ProcessSendError(core.ErrNonceTooLow) + sendState.ProcessSendError(core.ErrNonceTooLow) + require.False(t, sendState.ShouldAbortImmediately()) + sendState.ProcessSendError(core.ErrNonceTooLow) + require.True(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateSafeAbortWhileCallingNotMinedOnUnminedTx asserts that we will +// correctly abort if we continue to call TxNotMined on txns that haven't been +// mined. +func TestSendStateSafeAbortWhileCallingNotMinedOnUnminedTx(t *testing.T) { + t.Parallel() + sendState := newSendState() + + processNSendErrors( + sendState, core.ErrNonceTooLow, + ) + sendState.TxNotMined(testHash) + require.True(t, sendState.ShouldAbortImmediately()) +} + +// TestSendStateIsWaitingForConfirmationAfterTxMined asserts that we are waiting +// for confirmation after a tx is mined. +func TestSendStateIsWaitingForConfirmationAfterTxMined(t *testing.T) { + t.Parallel() + sendState := newSendState() + + testHash2 := common.HexToHash("0x02") + + sendState.TxMined(testHash) + require.True(t, sendState.IsWaitingForConfirmation()) + sendState.TxMined(testHash2) + require.True(t, sendState.IsWaitingForConfirmation()) +} + +// TestSendStateIsNotWaitingForConfirmationAfterTxUnmined asserts that we are +// not waiting for confirmation after a tx is mined then unmined. +func TestSendStateIsNotWaitingForConfirmationAfterTxUnmined(t *testing.T) { + t.Parallel() + sendState := newSendState() + + sendState.TxMined(testHash) + sendState.TxNotMined(testHash) + require.False(t, sendState.IsWaitingForConfirmation()) +} + +func stepClock(step time.Duration) func() time.Time { + i := 0 + return func() time.Time { + var start time.Time + i += 1 + + return start.Add(time.Duration(i) * step) + } +} + +// TestSendStateTimeoutAbort ensure that this will abort if it passes the tx pool timeout +// when no successful transactions have been recorded. +func TestSendStateTimeoutAbort(t *testing.T) { + t.Parallel() + sendState := newSendStateWithTimeout(10*time.Millisecond, stepClock(20*time.Millisecond)) + require.True(t, sendState.ShouldAbortImmediately(), "Should abort after timing out") +} + +// TestSendStateNoTimeoutAbortIfPublishedTx ensure that this will not abort if there is +// a successful transaction doSend. +func TestSendStateNoTimeoutAbortIfPublishedTx(t *testing.T) { + t.Parallel() + sendState := newSendStateWithTimeout(10*time.Millisecond, stepClock(20*time.Millisecond)) + sendState.ProcessSendError(nil) + require.False(t, sendState.ShouldAbortImmediately(), "Should not abort if published transaction successfully") +} diff --git a/lib/txmgr/txmgr.go b/lib/txmgr/txmgr.go new file mode 100644 index 00000000..8fa8bc3f --- /dev/null +++ b/lib/txmgr/txmgr.go @@ -0,0 +1,652 @@ +package txmgr + +import ( + "context" + "log/slog" + "math/big" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/ethclient" + "github.com/piplabs/story/lib/expbackoff" + "github.com/piplabs/story/lib/log" +) + +const ( + // PriceBump geth requires a minimum fee bump of 10% for regular tx resubmission. + PriceBump int64 = 10 +) + +// TxManager is an interface that allows callers to reliably publish txs, +// bumping the gas price if needed, and obtain the receipt of the resulting tx. +type TxManager interface { + // Send is used to create & doSend a transaction. It will handle increasing + // the gas price & ensuring that the transaction remains in the transaction pool. + // It can be stopped by canceling the provided context; however, the transaction + // may be included on L1 even if the context is canceled. + // + // NOTE: Send can be called concurrently, the nonce will be managed internally. + Send(ctx context.Context, candidate TxCandidate) (*types.Transaction, *types.Receipt, error) + + // From returns the sending address associated with the instance of the transaction manager. + // It is static for a single instance of a TxManager. + From() common.Address + + // ReserveNextNonce returns the next available nonce and increments the available nonce. + ReserveNextNonce(ctx context.Context) (uint64, error) +} + +// simple is a implementation of TxManager that performs linear fee +// bumping of a tx until it confirms. +type simple struct { + cfg Config // embed the config directly + chainName string + chainID *big.Int + + backend ethclient.Client + + nonce *uint64 // nil == unset, 0 == unused account + nonceLock sync.Mutex +} + +// NewSimple initializes a new simple with the passed Config. +func NewSimple(chainName string, conf Config) (TxManager, error) { + if err := conf.Check(); err != nil { + return nil, errors.Wrap(err, "invalid config") + } + + return &simple{ + chainID: conf.ChainID, + chainName: chainName, + cfg: conf, + backend: conf.Backend, + }, nil +} + +func (m *simple) ReserveNextNonce(ctx context.Context) (uint64, error) { + m.nonceLock.Lock() + defer m.nonceLock.Unlock() + + if m.nonce == nil { + nonce, err := m.backend.NonceAt(ctx, m.cfg.From, nil) + if err != nil { + return 0, errors.Wrap(err, "failed to get nonce") + } + m.nonce = &nonce + } + + defer func() { + *m.nonce++ + }() + + return *m.nonce, nil +} + +func (m *simple) From() common.Address { + return m.cfg.From +} + +// txFields returns a logger with the transaction hash and nonce fields set. +func txFields(tx *types.Transaction, logGas bool) []any { + fields := []any{ + slog.Int64("nonce", int64(tx.Nonce())), + slog.String("tx", tx.Hash().String()), + } + if logGas { + fields = append(fields, + slog.String("gas_tip_cap", tx.GasTipCap().String()), + slog.String("gas_fee_cap", tx.GasFeeCap().String()), + slog.Int64("gas_limit", int64(tx.Gas())), + ) + } + + return fields +} + +// TxCandidate is a transaction candidate that can be submitted to ask the +// [TxManager] to construct a transaction with gas price bounds. +type TxCandidate struct { + // TxData is the transaction calldata to be used in the constructed tx. + TxData []byte + // To is the recipient of the constructed tx. Nil means contract creation. + To *common.Address + // GasLimit is the gas limit to be used in the constructed tx. + GasLimit uint64 + // Value is the value to be used in the constructed tx. + Value *big.Int + // Nonce to use for the transaction. If nil, the current nonce is used. + Nonce *uint64 +} + +// Send is used to publish a transaction with incrementally higher gas prices +// until the transaction eventually confirms. This method blocks until an +// invocation of sendTx returns (called with differing gas prices). The method +// may be canceled using the passed context. +// +// The transaction manager handles all signing. If and only if the gas limit is 0, the +// transaction manager will do a gas estimation. +// +// NOTE: Send can be called concurrently, the nonce will be managed internally. +func (m *simple) Send(ctx context.Context, candidate TxCandidate) (*types.Transaction, *types.Receipt, error) { + tx, rec, err := m.doSend(ctx, candidate) + if err != nil { + m.resetNonce() + return nil, nil, err + } + + return tx, rec, nil +} + +// doSend performs the actual transaction creation and sending. +func (m *simple) doSend(ctx context.Context, candidate TxCandidate) (*types.Transaction, *types.Receipt, error) { + ctx, cancel := maybeSetTimeout(ctx, m.cfg.TxSendTimeout) + defer cancel() + + backoff := expbackoff.New(ctx, expbackoff.WithFastConfig()) + for { + if ctx.Err() != nil { + return nil, nil, errors.Wrap(ctx.Err(), "send timeout") + } + + // Set a candidate nonce if not already set + if candidate.Nonce == nil { + nonce, err := m.ReserveNextNonce(ctx) + if err != nil { + log.Warn(ctx, "Failed to reserve nonce (will retry)", err) + backoff() + + continue + } + candidate.Nonce = &nonce + } + + // Create the initial transaction + tx, err := m.craftTx(ctx, candidate) + if err != nil { + log.Warn(ctx, "Failed to create transaction (will retry)", err) + backoff() + + continue + } + + // Send it (note this has internal retries bumping fees) + rec, err := m.sendTx(ctx, tx) + if err != nil { + return nil, nil, errors.Wrap(err, "send tx") + } + + return tx, rec, nil + } +} + +// craftTx creates the signed transaction +// It queries L1 for the current fee market conditions as well as for the nonce. +// NOTE: This method SHOULD NOT publish the resulting transaction. +// NOTE: If the [TxCandidate.GasLimit] is non-zero, it will be used as the transaction's gas. +// NOTE: Otherwise, the [simple] will query the specified backend for an estimate. +func (m *simple) craftTx(ctx context.Context, candidate TxCandidate) (*types.Transaction, error) { + if candidate.Nonce == nil { + return nil, errors.New("invalid nil nonce") + } + + gasTipCap, baseFee, err := m.suggestGasPriceCaps(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get gas price info") + } + gasFeeCap := calcGasFeeCap(baseFee, gasTipCap) + + gasLimit := candidate.GasLimit + + // If the gas limit is set, we can use that as the gas + if gasLimit == 0 { + // Calculate the intrinsic gas for the transaction + gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{ + From: m.cfg.From, + To: candidate.To, + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Data: candidate.TxData, + Value: candidate.Value, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to estimate gas") + } + gasLimit = gas + } + + txMessage := &types.DynamicFeeTx{ + ChainID: m.chainID, + To: candidate.To, + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Value: candidate.Value, + Data: candidate.TxData, + Gas: gasLimit, + Nonce: *candidate.Nonce, + } + + ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) + defer cancel() + + return m.cfg.Signer(ctx, m.cfg.From, types.NewTx(txMessage)) +} + +// resetNonce resets the internal nonce tracking. This is called if any pending doSend +// returns an error. +func (m *simple) resetNonce() { + m.nonceLock.Lock() + defer m.nonceLock.Unlock() + m.nonce = nil +} + +// sendTx submits the same transaction several times with increasing gas prices as necessary. +// It waits for the transaction to be confirmed on chain. +func (m *simple) sendTx(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) { + var wg sync.WaitGroup + defer wg.Wait() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + sendState := NewSendState(m.cfg.SafeAbortNonceTooLowCount, m.cfg.TxNotInMempoolTimeout) + receiptChan := make(chan *types.Receipt, 1) + publishAndWait := func(tx *types.Transaction, bumpFees bool) *types.Transaction { + wg.Add(1) + tx, published := m.publishTx(ctx, tx, sendState, bumpFees) + if published { + go func() { + defer wg.Done() + m.waitForTx(ctx, tx, sendState, receiptChan) + }() + } else { + wg.Done() + } + + return tx + } + + // Immediately publish a transaction before starting the resubmission loop + tx = publishAndWait(tx, false) + + ticker := time.NewTicker(m.cfg.ResubmissionTimeout) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Don't resubmit a transaction if it has been mined, but we are waiting for the conf depth. + if sendState.IsWaitingForConfirmation() { + continue + } + // If we see lots of unrecoverable errors (and no pending transactions) abort sending the transaction. + if sendState.ShouldAbortImmediately() { + return nil, errors.New("aborted transaction sending", txFields(tx, false)...) + } + tx = publishAndWait(tx, true) + + case <-ctx.Done(): + return nil, errors.Wrap(ctx.Err(), "timeout") + + case receipt := <-receiptChan: + return receipt, nil + } + } +} + +// publishTx publishes the transaction to the transaction pool. If it receives any underpriced errors +// it will bump the fees and retry. +// Returns the latest fee bumped tx, and a boolean indicating whether the tx was sent or not. +func (m *simple) publishTx(ctx context.Context, tx *types.Transaction, sendState *SendState, + bumpFeesImmediately bool) (*types.Transaction, bool) { + for { + if ctx.Err() != nil { + return tx, false + } + + if bumpFeesImmediately { + newTx, err := m.increaseGasPrice(ctx, tx) + if err != nil { + log.Info(ctx, "Unable to increase gas", err) + return tx, false + } + tx = newTx + sendState.bumpCount++ + } + bumpFeesImmediately = true // bump fees next loop + + if sendState.IsWaitingForConfirmation() { + // there is a chance the previous tx goes into "waiting for confirmation" state + // during the increaseGasPrice call; continue waiting rather than resubmit the tx + return tx, false + } + + cCtx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) + err := m.backend.SendTransaction(cCtx, tx) + cancel() + sendState.ProcessSendError(err) + + if err == nil { + return tx, true + } + + switch { + case errStringMatch(err, core.ErrNonceTooLow): + log.Warn(ctx, "Nonce too low", err) + case errStringMatch(err, context.Canceled) || errStringMatch(err, context.DeadlineExceeded): + log.Warn(ctx, "Transaction doSend canceled", err) + case errStringMatch(err, txpool.ErrAlreadyKnown): + log.Warn(ctx, "Resubmitted already known transaction", err) + case errStringMatch(err, txpool.ErrReplaceUnderpriced): + log.Warn(ctx, "Transaction replacement is underpriced", err) + continue // retry with fee bump + case errStringMatch(err, txpool.ErrUnderpriced): + log.Warn(ctx, "Transaction is underpriced", err) + continue // retry with fee bump + default: + log.Warn(ctx, "Unknown error publishing transaction", err) + } + + // on non-underpriced error return immediately; will retry on next resubmission timeout + return tx, false + } +} + +// waitForTx calls waitMined, and then sends the receipt to receiptChan in a non-blocking way if a receipt is found +// for the transaction. It should be called in a separate goroutine. +func (m *simple) waitForTx(ctx context.Context, tx *types.Transaction, sendState *SendState, receiptChan chan *types.Receipt) { + // Poll for the transaction to be ready & then doSend the result to receiptChan + receipt, err := m.waitMined(ctx, tx, sendState) + if ctx.Err() != nil { + return + } else if err != nil { + // this will happen if the tx was successfully replaced by a tx with bumped fees + log.Warn(ctx, "Transaction receipt not mined, probably replaced", err) + return + } + + select { + case receiptChan <- receipt: + default: + } +} + +// waitMined waits for the transaction to be mined or for the context to be canceled. +func (m *simple) waitMined(ctx context.Context, tx *types.Transaction, + sendState *SendState) (*types.Receipt, error) { + txHash := tx.Hash() + const logFreqFactor = 10 // Log every 10th attempt + attempt := 1 + + queryTicker := time.NewTicker(m.cfg.ReceiptQueryInterval) + defer queryTicker.Stop() + for { + select { + case <-ctx.Done(): + return nil, errors.Wrap(ctx.Err(), "context canceled") + case <-queryTicker.C: + receipt, ok, err := m.queryReceipt(ctx, txHash, sendState) + if err != nil { + return nil, err + } else if !ok && attempt%logFreqFactor == 0 { + log.Warn(ctx, "Transaction not yet mined", nil, "attempt", attempt) + } else if ok { + return receipt, nil + } + + attempt++ + } + } +} + +// queryReceipt queries for the receipt and returns the receipt if it has passed the confirmation depth. +func (m *simple) queryReceipt(ctx context.Context, txHash common.Hash, + sendState *SendState) (*types.Receipt, bool, error) { + ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) + defer cancel() + receipt, err := m.backend.TransactionReceipt(ctx, txHash) + if err != nil { + if strings.Contains(err.Error(), "transaction indexing is in progress") { + return nil, false, nil // Just back off here + } else if errors.Is(err, ethereum.NotFound) || strings.Contains(err.Error(), ethereum.NotFound.Error()) { + sendState.TxNotMined(txHash) + return nil, false, nil + } + + return nil, false, err + } + + // Receipt is confirmed to be valid from this point on + sendState.TxMined(txHash) + + txHeight := receipt.BlockNumber.Uint64() + tip, err := m.backend.HeaderByNumber(ctx, nil) + if err != nil { + return nil, false, err + } + + // The transaction is considered confirmed when + // txHeight+numConfirmations-1 <= tipHeight. Note that the -1 is + // needed to account for the fact that confirmations have an + // inherent off-by-one, i.e. when using 1 confirmation the + // transaction should be confirmed when txHeight is equal to + // tipHeight. The equation is rewritten in this form to avoid + // underflow'm. + tipHeight := tip.Number.Uint64() + if txHeight+m.cfg.NumConfirmations <= tipHeight+1 { + return receipt, true, nil + } + + return nil, false, nil +} + +// increaseGasPrice returns a new transaction that is equivalent to the input transaction but with +// higher fees that should satisfy geth's tx replacement rules. It also computes an updated gas +// limit estimate. To avoid runaway price increases, fees are capped at a `feeLimitMultiplier` +// multiple of the suggested values. +func (m *simple) increaseGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) { + log.Debug(ctx, "Bumping gas price") + tip, baseFee, err := m.suggestGasPriceCaps(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get gas price info", txFields(tx, true)...) + } + bumpedTip, bumpedFee := updateFees(ctx, tx.GasTipCap(), tx.GasFeeCap(), tip, baseFee) + + if err := m.checkLimits(tip, baseFee, bumpedTip, bumpedFee); err != nil { + return nil, err + } + + // Re-estimate gaslimit in case things have changed or a previous gaslimit estimate was wrong + gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{ + From: m.cfg.From, + To: tx.To(), + GasTipCap: bumpedTip, + GasFeeCap: bumpedFee, + Data: tx.Data(), + Value: tx.Value(), + }) + if err != nil { + // If this is a transaction resubmission, we sometimes see this outcome because the + // original tx can get included in a block just before the above call. In this case the + // error is due to the tx reverting with message "block number must be equal to next + // expected block number" + log.Warn(ctx, "Failed to re-estimate gas", err, + "gas_limit", tx.Gas(), + "gas_fee_cap", bumpedFee, + "gas_tip_cap", bumpedTip, + ) + // just log and carry on + gas = tx.Gas() + } + if tx.Gas() != gas { + log.Debug(ctx, "Re-estimated gas differs", + "old_gas", tx.Gas(), + "new_gas", gas, + "gas_fee_cap", bumpedFee, + "gas_tip_cap", bumpedTip, + ) + } + + newTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: tx.ChainId(), + Nonce: tx.Nonce(), + To: tx.To(), + GasTipCap: bumpedTip, + GasFeeCap: bumpedFee, + Value: tx.Value(), + Data: tx.Data(), + Gas: gas, + }) + + ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) + defer cancel() + signedTx, err := m.cfg.Signer(ctx, m.cfg.From, newTx) + if err != nil { + return nil, err + } + + return signedTx, nil +} + +// suggestGasPriceCaps suggests what the new tip, base fee, and blob base fee should be based on +// the current L1 conditions. blobfee will be nil if 4844 is not yet active. +func (m *simple) suggestGasPriceCaps(ctx context.Context) (*big.Int, *big.Int, error) { + cCtx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) + defer cancel() + tip, err := m.backend.SuggestGasTipCap(cCtx) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to fetch the suggested gas tip cap") + } else if tip == nil { + return nil, nil, errors.New("the suggested tip was nil") + } + cCtx, cancel = context.WithTimeout(ctx, m.cfg.NetworkTimeout) + defer cancel() + head, err := m.backend.HeaderByNumber(cCtx, nil) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to fetch the suggested base fee") + } else if head.BaseFee == nil { + return nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a base fee") + } + + baseFee := head.BaseFee + + // Enforce minimum base fee and tip cap + if minTipCap := m.cfg.MinTipCap; minTipCap != nil && tip.Cmp(minTipCap) == -1 { + log.Debug(ctx, "Enforcing min tip cap", "min_tip_cap", m.cfg.MinTipCap, "orig_tip_cap", tip) + tip = new(big.Int).Set(m.cfg.MinTipCap) + } + if minBaseFee := m.cfg.MinBaseFee; minBaseFee != nil && baseFee.Cmp(minBaseFee) == -1 { + log.Debug(ctx, "Enforcing min base fee", "min_base_fee", m.cfg.MinBaseFee, "orig_base_fee", baseFee) + baseFee = new(big.Int).Set(m.cfg.MinBaseFee) + } + + return tip, baseFee, nil +} + +// checkLimits checks that the tip and baseFee have not increased by more than the configured multipliers +// if FeeLimitThreshold is specified in config, any increase which stays under the threshold are allowed. +func (m *simple) checkLimits(tip, baseFee, bumpedTip, bumpedFee *big.Int) error { + threshold := m.cfg.FeeLimitThreshold + limit := big.NewInt(int64(m.cfg.FeeLimitMultiplier)) + maxTip := new(big.Int).Mul(tip, limit) + maxFee := calcGasFeeCap(new(big.Int).Mul(baseFee, limit), maxTip) + var errs error + // generic check function to check tip and fee, and build up an error + check := func(v, maxValue *big.Int, name string) { + // if threshold is specified and the value is under the threshold, no need to check the max + if threshold != nil && threshold.Cmp(v) > 0 { + return + } + // if the value is over the max, add an error message + if v.Cmp(maxValue) > 0 { + errs = errors.New("bumped cap is over multiple of the suggested value", name, v, limit) + } + } + check(bumpedTip, maxTip, "tip") + check(bumpedFee, maxFee, "fee") + + return errs +} + +// calcThresholdValue returns ceil(x * priceBumpPercent / 100) for non-blob txs, or +// It guarantees that x is increased by at least 1. +func calcThresholdValue(x *big.Int) *big.Int { + threshold := new(big.Int) + ninetyNine := big.NewInt(99) + oneHundred := big.NewInt(100) + priceBumpPercent := big.NewInt(100 + PriceBump) + threshold.Set(priceBumpPercent) + + return threshold.Mul(threshold, x).Add(threshold, ninetyNine).Div(threshold, oneHundred) +} + +// updateFees takes an old transaction's tip & fee cap plus a new tip & base fee, and returns +// a suggested tip and fee cap such that: +// +// (a) each satisfies geth's required tx-replacement fee bumps, and +// (b) gasTipCap is no less than new tip, and +// (c) gasFeeCap is no less than calcGasFee(newBaseFee, newTip) +func updateFees(ctx context.Context, oldTip, oldFeeCap, newTip, newBaseFee *big.Int) (*big.Int, *big.Int) { + newFeeCap := calcGasFeeCap(newBaseFee, newTip) + log.Debug(ctx, "Updating fees", "old_gas_tip_cap", oldTip, "old_gas_fee_cap", oldFeeCap, + "new_gas_tip_cap", newTip, "new_gas_fee_cap", newFeeCap, "new_base_fee", newBaseFee) + thresholdTip := calcThresholdValue(oldTip) + thresholdFeeCap := calcThresholdValue(oldFeeCap) + if newTip.Cmp(thresholdTip) >= 0 && newFeeCap.Cmp(thresholdFeeCap) >= 0 { + log.Debug(ctx, "Using new tip and feecap") + return newTip, newFeeCap + } else if newTip.Cmp(thresholdTip) >= 0 && newFeeCap.Cmp(thresholdFeeCap) < 0 { + // Tip has gone up, but base fee is flat or down. + // TODO(CLI-3714): Do we need to recalculate the FC here? + log.Debug(ctx, "Using new tip and threshold feecap") + return newTip, thresholdFeeCap + } else if newTip.Cmp(thresholdTip) < 0 && newFeeCap.Cmp(thresholdFeeCap) >= 0 { + // Base fee has gone up, but the tip hasn't. Recalculate the feecap because if the tip went up a lot + // not enough of the feecap may be dedicated to paying the base fee. + log.Debug(ctx, "Using threshold tip and recalculated feecap") + return thresholdTip, calcGasFeeCap(newBaseFee, thresholdTip) + } + + log.Debug(ctx, "Using threshold tip and threshold feecap") + + return thresholdTip, thresholdFeeCap +} + +// calcGasFeeCap deterministically computes the recommended gas fee cap given +// the base fee and gasTipCap. The resulting gasFeeCap is equal to: +// +// gasTipCap + 2*baseFee. +func calcGasFeeCap(baseFee, gasTipCap *big.Int) *big.Int { + return new(big.Int).Add( + gasTipCap, + new(big.Int).Mul(baseFee, big.NewInt(2)), + ) +} + +// errStringMatch returns true if err.Error() is a substring in target.Error() or if both are nil. +// It can accept nil errors without issue. +func errStringMatch(err, target error) bool { + if err == nil && target == nil { + return true + } else if err == nil || target == nil { + return false + } + + return strings.Contains(err.Error(), target.Error()) +} + +// maybeSetTimeout returns a copy of the context with the timeout set if timeout is not zero. +// If the timeout is zero, it doesn't set it and just returns the context. +func maybeSetTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { + if timeout == 0 { + return ctx, func() {} + } + + return context.WithTimeout(ctx, timeout) +} diff --git a/lib/txmgr/txmgr_internal_test.go b/lib/txmgr/txmgr_internal_test.go new file mode 100644 index 00000000..3e127330 --- /dev/null +++ b/lib/txmgr/txmgr_internal_test.go @@ -0,0 +1,1171 @@ +package txmgr + +import ( + "context" + "errors" + "math/big" + "strconv" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + + "github.com/piplabs/story/lib/ethclient" +) + +const ( + startingNonce uint64 = 1 // we pick something other than 0 so we can confirm nonces are getting set properly +) + +type sendTransactionFunc func(ctx context.Context, tx *types.Transaction) error + +func testSendState() *SendState { + return NewSendState(100, time.Hour) +} + +// testHarness houses the necessary resources to test the simple. +type testHarness struct { + cfg Config + mgr *simple + backend *mockBackend + gasPricer *gasPricer +} + +// newTestHarnessWithConfig initializes a testHarness with a specific +// configuration. +func newTestHarnessWithConfig(_ *testing.T, cfg Config) *testHarness { + g := newGasPricer(3) + backend := newMockBackend(g) + cfg.Backend = backend + mgr := &simple{ + chainID: cfg.ChainID, + chainName: "TEST", + cfg: cfg, + backend: cfg.Backend, + } + + return &testHarness{ + cfg: cfg, + mgr: mgr, + backend: backend, + gasPricer: g, + } +} + +// newTestHarness initializes a testHarness with a default configuration that is +// suitable for most tests. +func newTestHarness(t *testing.T) *testHarness { + t.Helper() + return newTestHarnessWithConfig(t, configWithNumConfs(1)) +} + +// createTxCandidate creates a mock [TxCandidate]. +func (h testHarness) createTxCandidate() TxCandidate { + inbox := common.HexToAddress("0x42000000000000000000000000000000000000ff") + return TxCandidate{ + To: &inbox, + TxData: []byte{0x00, 0x01, 0x02}, + GasLimit: uint64(1337), + } +} + +func configWithNumConfs(numConfirmations uint64) Config { + return Config{ + ResubmissionTimeout: time.Second, + ReceiptQueryInterval: 50 * time.Millisecond, + NumConfirmations: numConfirmations, + SafeAbortNonceTooLowCount: 3, + FeeLimitMultiplier: 5, + TxNotInMempoolTimeout: 1 * time.Hour, + Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { + return tx, nil + }, + From: common.Address{}, + } +} + +type gasPricer struct { + epoch int64 + mineAtEpoch int64 + baseGasTipFee *big.Int + baseBaseFee *big.Int + excessBlobGas uint64 + err error + mu sync.RWMutex +} + +func newGasPricer(mineAtEpoch int64) *gasPricer { + return &gasPricer{ + mineAtEpoch: mineAtEpoch, + baseGasTipFee: big.NewInt(5), + baseBaseFee: big.NewInt(7), + // Simulate 100 excess blobs, which results in a blobBaseFee of 50 wei. This default means + // blob txs will be subject to the geth minimum blobgas fee of 1 gwei. + excessBlobGas: 100 * (params.BlobTxBlobGasPerBlob), + } +} + +func (g *gasPricer) getEpoch() int64 { + g.mu.RLock() + defer g.mu.RUnlock() + + return g.epoch +} + +func (g *gasPricer) expGasFeeCap() *big.Int { + _, gasFeeCap := g.feesForEpoch(g.mineAtEpoch) + return gasFeeCap +} + +func (g *gasPricer) shouldMine(gasFeeCap *big.Int) bool { + return g.expGasFeeCap().Cmp(gasFeeCap) <= 0 +} + +func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) { + e := big.NewInt(epoch) + epochBaseFee := new(big.Int).Mul(g.baseBaseFee, e) + epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, e) + epochGasFeeCap := calcGasFeeCap(epochBaseFee, epochGasTipCap) + + return epochGasTipCap, epochGasFeeCap +} + +func (g *gasPricer) baseFee() *big.Int { + return new(big.Int).Mul(g.baseBaseFee, big.NewInt(g.getEpoch())) +} + +func (g *gasPricer) sample() (*big.Int, *big.Int) { + g.mu.Lock() + defer g.mu.Unlock() + + g.epoch++ + epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch) + + return epochGasTipCap, epochGasFeeCap +} + +type minedTxInfo struct { + gasFeeCap *big.Int + blobFeeCap *big.Int + blockNumber uint64 +} + +// mockBackend implements ReceiptSource that tracks mined transactions +// along with the gas price used. +type mockBackend struct { + ethclient.Client + mu sync.RWMutex + + g *gasPricer + send sendTransactionFunc + + // blockHeight tracks the current height of the chain. + blockHeight uint64 + + // minedTxs maps the hash of a mined transaction to its details. + minedTxs map[common.Hash]minedTxInfo +} + +// newMockBackend initializes a new mockBackend. +func newMockBackend(g *gasPricer) *mockBackend { + return &mockBackend{ + g: g, + minedTxs: make(map[common.Hash]minedTxInfo), + } +} + +// setTxSender sets the implementation for the sendTransactionFunction. +func (b *mockBackend) setTxSender(s sendTransactionFunc) { + b.send = s +} + +// mine records a (txHash, gasFeeCap) as confirmed. Subsequent calls to +// TransactionReceipt with a matching txHash will result in a non-nil receipt. +// If a nil txHash is supplied this has the effect of mining an empty block. +func (b *mockBackend) mine(txHash *common.Hash, gasFeeCap *big.Int) { + b.mu.Lock() + defer b.mu.Unlock() + + b.blockHeight++ + if txHash != nil { + b.minedTxs[*txHash] = minedTxInfo{ + gasFeeCap: gasFeeCap, + blockNumber: b.blockHeight, + } + } +} + +// BlockNumber returns the most recent block number. +func (b *mockBackend) BlockNumber(ctx context.Context) (uint64, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + return b.blockHeight, nil +} + +func (b *mockBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + num := big.NewInt(int64(b.blockHeight)) + if number != nil { + num.Set(number) + } + + bg := b.g.excessBlobGas + uint64(b.g.getEpoch()) + + return &types.Header{ + Number: num, + BaseFee: b.g.baseFee(), + ExcessBlobGas: &bg, + }, nil +} + +func (b *mockBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { + if b.g.err != nil { + return 0, b.g.err + } + if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { + return 0, core.ErrTipAboveFeeCap + } + + return b.g.baseFee().Uint64(), nil +} + +func (b *mockBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + tip, _ := b.g.sample() + return tip, nil +} + +func (b *mockBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error { + if b.send == nil { + panic("set sender function was not set") + } + + return b.send(ctx, tx) +} + +func (b *mockBackend) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + return startingNonce, nil +} + +func (b *mockBackend) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + return startingNonce, nil +} + +func (*mockBackend) ChainID(ctx context.Context) (*big.Int, error) { + return big.NewInt(1), nil +} + +// TransactionReceipt queries the mockBackend for a mined txHash. If none is found, nil is returned +// for both return values. Otherwise, it returns a receipt containing the txHash, the gasFeeCap +// used in GasUsed, and the blobFeeCap in CumuluativeGasUsed to make the values accessible from our +// test framework. +func (b *mockBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + txInfo, ok := b.minedTxs[txHash] + if !ok { + return nil, errors.New("not found") + } + + // Return the gas fee cap for the transaction in the GasUsed field so that + // we can assert the proper tx confirmed in our tests. + var blobFeeCap uint64 + if txInfo.blobFeeCap != nil { + blobFeeCap = txInfo.blobFeeCap.Uint64() + } + + return &types.Receipt{ + TxHash: txHash, + GasUsed: txInfo.gasFeeCap.Uint64(), + CumulativeGasUsed: blobFeeCap, + BlockNumber: big.NewInt(int64(txInfo.blockNumber)), + }, nil +} + +func (b *mockBackend) Close() { +} + +// TestTxMgrConfirmAtMinGasPrice asserts that Send returns the min gas price tx +// if the tx is mined instantly. +func TestTxMgrConfirmAtMinGasPrice(t *testing.T) { + t.Parallel() + + h := newTestHarness(t) + + gasPricer := newGasPricer(1) + + gasTipCap, gasFeeCap := gasPricer.sample() + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + + sendTx := func(ctx context.Context, tx *types.Transaction) error { + if gasPricer.shouldMine(tx.GasFeeCap()) { + txHash := tx.Hash() + h.backend.mine(&txHash, tx.GasFeeCap()) + } + + return nil + } + h.backend.setTxSender(sendTx) + + ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) + defer cancel() + receipt, err := h.mgr.sendTx(ctx, tx) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) +} + +// TestTxMgrNeverConfirmCancel asserts that a Send can be canceled even if no +// transaction is mined. This is done to ensure the tx mgr can properly +// abort on shutdown, even if a txn is in the process of being published. +func TestTxMgrNeverConfirmCancel(t *testing.T) { + // t.SkipNow() + t.Parallel() + + h := newTestHarness(t) + + gasTipCap, gasFeeCap := h.gasPricer.sample() + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + sendTx := func(ctx context.Context, tx *types.Transaction) error { + // Don't publish tx to backend, simulating never being mined. + return nil + } + h.backend.setTxSender(sendTx) + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + receipt, err := h.mgr.sendTx(ctx, tx) + require.Error(t, err) + require.Nil(t, receipt) +} + +// TestTxMgrConfirmsAtMaxGasPrice asserts that Send properly returns the max gas +// price receipt if none of the lower gas price txs were mined. +func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) { + t.Parallel() + + h := newTestHarness(t) + + gasTipCap, gasFeeCap := h.gasPricer.sample() + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + sendTx := func(ctx context.Context, tx *types.Transaction) error { + if h.gasPricer.shouldMine(tx.GasFeeCap()) { + txHash := tx.Hash() + h.backend.mine(&txHash, tx.GasFeeCap()) + } + + return nil + } + h.backend.setTxSender(sendTx) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + receipt, err := h.mgr.sendTx(ctx, tx) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) +} + +// errRPCFailure is a sentinel error used in testing to fail publications. +var errRPCFailure = errors.New("rpc failure") + +// TestTxMgrBlocksOnFailingRpcCalls asserts that if all the publication +// attempts fail due to rpc failures, that the tx manager will return +// ErrPublishTimeout. +func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) { + t.Parallel() + h := newTestHarness(t) + gasTipCap, gasFeeCap := h.gasPricer.sample() + orig := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + errFirst := true + var sent *types.Transaction + sendTx := func(ctx context.Context, tx *types.Transaction) error { + if errFirst { + errFirst = false + return errRPCFailure + } + sent = tx + hash := tx.Hash() + h.backend.mine(&hash, tx.GasFeeCap()) + + return nil + } + h.backend.setTxSender(sendTx) + receipt, err := h.mgr.sendTx(context.Background(), orig) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, receipt.TxHash, sent.Hash()) + require.GreaterOrEqual(t, sent.GasTipCap().Uint64(), orig.GasTipCap().Uint64()) +} + +// TestTxMgr_CraftTx ensures that the tx manager will create transactions as expected. +func TestTxMgr_CraftTx(t *testing.T) { + t.Parallel() + h := newTestHarness(t) + candidate := h.createTxCandidate() + candidate.Nonce = uint64Ptr(startingNonce) + + // Craft the transaction. + gasTipCap, gasFeeCap := h.gasPricer.feesForEpoch(h.gasPricer.getEpoch() + 1) + tx, err := h.mgr.craftTx(context.Background(), candidate) + require.NoError(t, err) + require.NotNil(t, tx) + require.Equal(t, byte(types.DynamicFeeTxType), tx.Type()) + + // Validate the gas tip cap and fee cap. + require.Equal(t, gasTipCap, tx.GasTipCap()) + require.Equal(t, gasFeeCap, tx.GasFeeCap()) + + // Craft transaction uses candidate nonce. + require.EqualValues(t, startingNonce, tx.Nonce()) + + // Check that the gas was set using the gas limit. + require.Equal(t, candidate.GasLimit, tx.Gas()) +} + +// TestTxMgr_EstimateGas ensures that the tx manager will estimate +// the gas when candidate gas limit is zero in [craftTx]. +func TestTxMgr_EstimateGas(t *testing.T) { + t.Parallel() + h := newTestHarness(t) + candidate := h.createTxCandidate() + candidate.Nonce = uint64Ptr(startingNonce) + + // Set the gas limit to zero to trigger gas estimation. + candidate.GasLimit = 0 + + // Gas estimate + gasEstimate := h.gasPricer.baseBaseFee.Uint64() + + // Craft the transaction. + tx, err := h.mgr.craftTx(context.Background(), candidate) + require.NoError(t, err) + require.NotNil(t, tx) + + // Check that the gas was estimated correctly. + require.Equal(t, gasEstimate, tx.Gas()) +} + +func TestTxMgr_EstimateGasFails(t *testing.T) { + t.Parallel() + h := newTestHarness(t) + candidate := h.createTxCandidate() + + // Set the gas limit to zero to trigger gas estimation. + candidate.GasLimit = 0 + candidate.Nonce = uint64Ptr(startingNonce) + + // Craft a successful transaction. + tx, err := h.mgr.craftTx(context.Background(), candidate) + require.NoError(t, err) + require.EqualValues(t, startingNonce, tx.Nonce()) + + // Mock gas estimation failure. + h.gasPricer.err = errors.New("execution error") + _, err = h.mgr.craftTx(context.Background(), candidate) + require.ErrorContains(t, err, "failed to estimate gas") + + // Ensure successful craft uses the correct nonce + h.gasPricer.err = nil + tx, err = h.mgr.craftTx(context.Background(), candidate) + require.NoError(t, err) + require.EqualValues(t, startingNonce, tx.Nonce()) +} + +func TestTxMgr_SigningFails(t *testing.T) { + t.Parallel() + errorSigning := false + cfg := configWithNumConfs(1) + cfg.Signer = func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { + if errorSigning { + return nil, errors.New("signer error") + } else { + return tx, nil + } + } + h := newTestHarnessWithConfig(t, cfg) + candidate := h.createTxCandidate() + + const nonce uint64 = 99 + candidate.Nonce = uint64Ptr(nonce) + // Set the gas limit to zero to trigger gas estimation. + candidate.GasLimit = 0 + + // Craft a successful transaction. + tx, err := h.mgr.craftTx(context.Background(), candidate) + require.NoError(t, err) + require.Equal(t, nonce, tx.Nonce()) + + // Mock signer failure. + errorSigning = true + _, err = h.mgr.craftTx(context.Background(), candidate) + require.ErrorContains(t, err, "signer error") + + // Ensure successful craft uses the correct nonce + errorSigning = false + tx, err = h.mgr.craftTx(context.Background(), candidate) + require.NoError(t, err) + require.Equal(t, nonce, tx.Nonce()) +} + +// TestTxMgrOnlyOnePublicationSucceeds asserts that the tx manager will return a +// receipt so long as at least one of the publications is able to succeed with a +// simulated rpc failure. +func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) { + t.Parallel() + + h := newTestHarness(t) + + gasTipCap, gasFeeCap := h.gasPricer.sample() + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + + sendTx := func(ctx context.Context, tx *types.Transaction) error { + // Fail all but the final attempt. + if !h.gasPricer.shouldMine(tx.GasFeeCap()) { + return errRPCFailure + } + + txHash := tx.Hash() + h.backend.mine(&txHash, tx.GasFeeCap()) + + return nil + } + h.backend.setTxSender(sendTx) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + receipt, err := h.mgr.sendTx(ctx, tx) + require.NoError(t, err) + + require.NotNil(t, receipt) + require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) +} + +// TestTxMgrConfirmsMinGasPriceAfterBumping delays the mining of the initial tx +// with the minimum gas price, and asserts that its receipt is returned even +// though if the gas price has been bumped in other goroutines. +func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) { + t.Parallel() + + h := newTestHarness(t) + + gasTipCap, gasFeeCap := h.gasPricer.sample() + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + + sendTx := func(ctx context.Context, tx *types.Transaction) error { + // Delay mining the tx with the min gas price. + if h.gasPricer.shouldMine(tx.GasFeeCap()) { + time.AfterFunc(1*time.Second, func() { + txHash := tx.Hash() + h.backend.mine(&txHash, tx.GasFeeCap()) + }) + } + + return nil + } + h.backend.setTxSender(sendTx) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + receipt, err := h.mgr.sendTx(ctx, tx) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) +} + +// TestTxMgrDoesntAbortNonceTooLowAfterMiningTx. +func TestTxMgrDoesntAbortNonceTooLowAfterMiningTx(t *testing.T) { + t.Parallel() + + h := newTestHarnessWithConfig(t, configWithNumConfs(2)) + + gasTipCap, gasFeeCap := h.gasPricer.sample() + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + }) + + sendTx := func(ctx context.Context, tx *types.Transaction) error { + switch { + // If the txn's gas fee cap is less than the one we expect to mine, + // accept the txn to the mempool. + case tx.GasFeeCap().Cmp(h.gasPricer.expGasFeeCap()) < 0: + return nil + + // Accept and mine the actual txn we expect to confirm. + case h.gasPricer.shouldMine(tx.GasFeeCap()): + txHash := tx.Hash() + h.backend.mine(&txHash, tx.GasFeeCap()) + time.AfterFunc(1*time.Second, func() { + h.backend.mine(nil, nil) + }) + + return nil + + // For gas prices greater than our expected, return ErrNonceTooLow since + // the prior txn confirmed and will invalidate subsequent publications. + default: + return core.ErrNonceTooLow + } + } + h.backend.setTxSender(sendTx) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + receipt, err := h.mgr.sendTx(ctx, tx) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) +} + +// TestWaitMinedReturnsReceiptOnFirstSuccess insta-mines a transaction and +// asserts that waitMined returns the appropriate receipt. +func TestWaitMinedReturnsReceiptOnFirstSuccess(t *testing.T) { + t.Parallel() + + h := newTestHarnessWithConfig(t, configWithNumConfs(1)) + + // Create a tx and mine it immediately using the default backend. + tx := types.NewTx(&types.LegacyTx{}) + txHash := tx.Hash() + h.backend.mine(&txHash, new(big.Int)) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + receipt, err := h.mgr.waitMined(ctx, tx, testSendState()) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, receipt.TxHash, txHash) +} + +// TestWaitMinedCanBeCanceled ensures that waitMined exits of the passed context +// is canceled before a receipt is found. +func TestWaitMinedCanBeCanceled(t *testing.T) { + t.Parallel() + + h := newTestHarness(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + // Create an unimined tx. + tx := types.NewTx(&types.LegacyTx{}) + + receipt, err := h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour)) + require.Error(t, err) + require.Nil(t, receipt) +} + +// TestWaitMinedMultipleConfs asserts that waitMined will properly wait for more +// than one confirmation. +func TestWaitMinedMultipleConfs(t *testing.T) { + t.Parallel() + + const numConfs = 2 + + h := newTestHarnessWithConfig(t, configWithNumConfs(numConfs)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + // Create an unimined tx. + tx := types.NewTx(&types.LegacyTx{}) + txHash := tx.Hash() + h.backend.mine(&txHash, new(big.Int)) + + receipt, err := h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour)) + require.Error(t, err) + require.Nil(t, receipt) + + ctx, cancel = context.WithTimeout(context.Background(), time.Second) + defer cancel() + + // Mine an empty block, tx should now be confirmed. + h.backend.mine(nil, nil) + receipt, err = h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour)) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, txHash, receipt.TxHash) +} + +// failingBackend implements ReceiptSource, returning a failure on the +// first call but a success on the second call. This allows us to test that the +// inner loop of waitMined properly handles this case. +type failingBackend struct { + ethclient.Client + returnSuccessBlockNumber bool + returnSuccessHeader bool + returnSuccessReceipt bool + baseFee, gasTip *big.Int + excessBlobGas *uint64 +} + +// BlockNumber for the failingBackend returns errRPCFailure on the first +// invocation and a fixed block height on subsequent calls. +func (b *failingBackend) BlockNumber(_ context.Context) (uint64, error) { + if !b.returnSuccessBlockNumber { + b.returnSuccessBlockNumber = true + return 0, errRPCFailure + } + + return 1, nil +} + +// TransactionReceipt for the failingBackend returns errRPCFailure on the first +// invocation, and a receipt containing the passed TxHash on the second. +func (b *failingBackend) TransactionReceipt( + _ context.Context, txHash common.Hash, +) (*types.Receipt, error) { + if !b.returnSuccessReceipt { + b.returnSuccessReceipt = true + return nil, ethereum.NotFound + } + + return &types.Receipt{ + TxHash: txHash, + BlockNumber: big.NewInt(1), + }, nil +} + +func (b *failingBackend) HeaderByNumber(_ context.Context, _ *big.Int) (*types.Header, error) { + return &types.Header{ + Number: big.NewInt(1), + BaseFee: b.baseFee, + ExcessBlobGas: b.excessBlobGas, + }, nil +} + +func (b *failingBackend) SuggestGasTipCap(_ context.Context) (*big.Int, error) { + return b.gasTip, nil +} + +func (b *failingBackend) EstimateGas(_ context.Context, msg ethereum.CallMsg) (uint64, error) { + return b.baseFee.Uint64(), nil +} + +// TestWaitMinedReturnsReceiptAfterNotFound asserts that waitMined is able to +// recover from failed calls to the backend. It uses the failedBackend to +// simulate an rpc call failure, followed by the successful return of a receipt. +func TestWaitMinedReturnsReceiptAfterNotFound(t *testing.T) { + t.Parallel() + + var borkedBackend failingBackend + + mgr := &simple{ + cfg: Config{ + ResubmissionTimeout: time.Second, + ReceiptQueryInterval: 50 * time.Millisecond, + NumConfirmations: 1, + SafeAbortNonceTooLowCount: 3, + }, + chainName: "TEST", + backend: &borkedBackend, + } + + // Don't mine the tx with the default backend. The failingBackend will + // return the txHash on the second call. + tx := types.NewTx(&types.LegacyTx{}) + txHash := tx.Hash() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + receipt, err := mgr.waitMined(ctx, tx, testSendState()) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, receipt.TxHash, txHash) +} + +func doGasPriceIncrease(_ *testing.T, txTipCap, txFeeCap, newTip, + newBaseFee int64) (*types.Transaction, *types.Transaction, error) { + borkedBackend := failingBackend{ + gasTip: big.NewInt(newTip), + baseFee: big.NewInt(newBaseFee), + returnSuccessHeader: true, + } + + mgr := &simple{ + cfg: Config{ + ResubmissionTimeout: time.Second, + ReceiptQueryInterval: 50 * time.Millisecond, + NumConfirmations: 1, + SafeAbortNonceTooLowCount: 3, + FeeLimitMultiplier: 5, + Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { + return tx, nil + }, + From: common.Address{}, + }, + chainName: "TEST", + backend: &borkedBackend, + } + + tx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: big.NewInt(txTipCap), + GasFeeCap: big.NewInt(txFeeCap), + }) + newTx, err := mgr.increaseGasPrice(context.Background(), tx) + + return tx, newTx, err +} + +func TestIncreaseGasPrice(t *testing.T) { + t.Parallel() + require.Equal(t, int64(10), PriceBump, "test must be updated if priceBump is adjusted") + tests := []struct { + name string + run func(t *testing.T) + }{ + { + name: "bump at least 1", + run: func(t *testing.T) { + t.Helper() + tx, newTx, err := doGasPriceIncrease(t, 1, 3, 1, 1) + require.Greater(t, newTx.GasFeeCap().Uint64(), tx.GasFeeCap().Uint64(), "new tx fee cap must be larger") + require.Greater(t, newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64(), "new tx tip must be larger") + require.NoError(t, err) + }, + }, + { + name: "enforces min bump", + run: func(t *testing.T) { + t.Helper() + tx, newTx, err := doGasPriceIncrease(t, 100, 1000, 101, 460) + require.Greater(t, newTx.GasFeeCap().Uint64(), tx.GasFeeCap().Uint64(), "new tx fee cap must be larger") + require.Greater(t, newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64(), "new tx tip must be larger") + require.NoError(t, err) + }, + }, + { + name: "enforces min bump on only tip increase", + run: func(t *testing.T) { + t.Helper() + tx, newTx, err := doGasPriceIncrease(t, 100, 1000, 101, 440) + require.Greater(t, newTx.GasFeeCap().Uint64(), tx.GasFeeCap().Uint64(), "new tx fee cap must be larger") + require.Greater(t, newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64(), "new tx tip must be larger") + require.NoError(t, err) + }, + }, + { + name: "enforces min bump on only base fee increase", + run: func(t *testing.T) { + t.Helper() + tx, newTx, err := doGasPriceIncrease(t, 100, 1000, 99, 460) + require.Greater(t, newTx.GasFeeCap().Uint64(), tx.GasFeeCap().Uint64(), "new tx fee cap must be larger") + require.Greater(t, newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64(), "new tx tip must be larger") + require.NoError(t, err) + }, + }, + { + name: "uses L1 values when larger", + run: func(t *testing.T) { + t.Helper() + _, newTx, err := doGasPriceIncrease(t, 10, 100, 50, 200) + require.Equal(t, newTx.GasFeeCap().Uint64(), big.NewInt(450).Uint64(), "new tx fee cap must be equal L1") + require.Equal(t, newTx.GasTipCap().Uint64(), big.NewInt(50).Uint64(), "new tx tip must be equal L1") + require.NoError(t, err) + }, + }, + { + name: "uses L1 tip when larger and threshold FC", + run: func(t *testing.T) { + t.Helper() + _, newTx, err := doGasPriceIncrease(t, 100, 2200, 120, 1050) + require.Equal(t, newTx.GasTipCap().Uint64(), big.NewInt(120).Uint64(), "new tx tip must be equal L1") + require.Equal(t, newTx.GasFeeCap().Uint64(), big.NewInt(2420).Uint64(), + "new tx fee cap must be equal to the threshold value") + require.NoError(t, err) + }, + }, + { + name: "bumped fee above multiplier limit", + run: func(t *testing.T) { + t.Helper() + _, _, err := doGasPriceIncrease(t, 1, 9999, 1, 1) + require.ErrorContains(t, err, "bumped cap") + require.NotContains(t, err.Error(), "tip cap") + }, + }, + { + name: "bumped tip above multiplier limit", + run: func(t *testing.T) { + t.Helper() + _, _, err := doGasPriceIncrease(t, 9999, 0, 0, 9999) + require.ErrorContains(t, err, "bumped cap") + require.NotContains(t, err.Error(), "fee cap") + }, + }, + { + name: "bumped fee and tip above multiplier limit", + run: func(t *testing.T) { + t.Helper() + _, _, err := doGasPriceIncrease(t, 9999, 9999, 1, 1) + require.ErrorContains(t, err, "bumped cap") + }, + }, + { + name: "uses L1 FC when larger and threshold tip", + run: func(t *testing.T) { + t.Helper() + _, newTx, err := doGasPriceIncrease(t, 100, 2200, 100, 2000) + require.Equal(t, big.NewInt(110).Uint64(), + newTx.GasTipCap().Uint64(), "new tx tip must be equal the threshold value") + require.Equal(t, big.NewInt(4110).Uint64(), + newTx.GasFeeCap().Uint64(), "new tx fee cap must be equal L1") + + require.NoError(t, err) + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + test.run(t) + }) + } +} + +// TestIncreaseGasPriceLimits asserts that if the L1 base fee & tip remain the +// same, repeated calls to increaseGasPrice eventually hit a limit. +func TestIncreaseGasPriceLimits(t *testing.T) { + t.Parallel() + t.Run("no-threshold", func(t *testing.T) { + t.Parallel() + testIncreaseGasPriceLimit(t, gasPriceLimitTest{ + expTipCap: 46, + expFeeCap: 354, // just below 5*100 + expBlobFeeCap: 4 * params.GWei, + }) + }) + t.Run("with-threshold", func(t *testing.T) { + t.Parallel() + testIncreaseGasPriceLimit(t, gasPriceLimitTest{ + thr: big.NewInt(params.GWei * 10), + expTipCap: 1_293_535_754, + expFeeCap: 9_192_620_686, // just below 10 gwei + expBlobFeeCap: 8 * params.GWei, + }) + }) +} + +type gasPriceLimitTest struct { + thr *big.Int + expTipCap, expFeeCap int64 + expBlobFeeCap int64 +} + +// testIncreaseGasPriceLimit runs a gas bumping test that increases the gas price until it hits an error. +// It starts with a tx that has a tip cap of 10 wei and fee cap of 100 wei. +func testIncreaseGasPriceLimit(t *testing.T, lt gasPriceLimitTest) { + t.Helper() + + borkedTip := int64(10) + borkedFee := int64(45) + // simulate 100 excess blobs which yields a 50 wei blob base fee + borkedExcessBlobGas := uint64(100 * params.BlobTxBlobGasPerBlob) + borkedBackend := failingBackend{ + gasTip: big.NewInt(borkedTip), + baseFee: big.NewInt(borkedFee), + excessBlobGas: &borkedExcessBlobGas, + returnSuccessHeader: true, + } + + mgr := &simple{ + cfg: Config{ + ResubmissionTimeout: time.Second, + ReceiptQueryInterval: 50 * time.Millisecond, + NumConfirmations: 1, + SafeAbortNonceTooLowCount: 3, + FeeLimitMultiplier: 5, + FeeLimitThreshold: lt.thr, + Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { + return tx, nil + }, + From: common.Address{}, + }, + chainName: "TEST", + backend: &borkedBackend, + } + lastGoodTx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: big.NewInt(10), + GasFeeCap: big.NewInt(100), + }) + + // Run increaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop to make + // sure it errors out without a runaway fee increase. + ctx := context.Background() + var err error + for { + var tmpTx *types.Transaction + tmpTx, err = mgr.increaseGasPrice(ctx, lastGoodTx) + if err != nil { + break + } + lastGoodTx = tmpTx + } + require.Error(t, err) + + // Confirm that fees only rose until expected threshold + require.Equal(t, lt.expTipCap, lastGoodTx.GasTipCap().Int64()) + require.Equal(t, lt.expFeeCap, lastGoodTx.GasFeeCap().Int64()) +} + +func TestErrStringMatch(t *testing.T) { + t.Parallel() + tests := []struct { + err error + target error + match bool + }{ + {err: nil, target: nil, match: true}, + {err: errors.New("exists"), target: nil, match: false}, + {err: nil, target: errors.New("exists"), match: false}, + {err: errors.New("exact match"), target: errors.New("exact match"), match: true}, + {err: errors.New("partial: match"), target: errors.New("match"), match: true}, + } + + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + require.Equal(t, test.match, errStringMatch(test.err, test.target)) + }) + } +} + +func TestNonceReset(t *testing.T) { + t.Parallel() + conf := configWithNumConfs(1) + conf.SafeAbortNonceTooLowCount = 1 + h := newTestHarnessWithConfig(t, conf) + + index := -1 + var nonces []uint64 + sendTx := func(ctx context.Context, tx *types.Transaction) error { + index++ + nonces = append(nonces, tx.Nonce()) + // fail every 3rd tx + if index%3 == 0 { + return core.ErrNonceTooLow + } + txHash := tx.Hash() + h.backend.mine(&txHash, tx.GasFeeCap()) + + return nil + } + h.backend.setTxSender(sendTx) + + ctx := context.Background() + for i := range 8 { + _, _, err := h.mgr.Send(ctx, TxCandidate{ + To: &common.Address{}, + }) + // expect every 3rd tx to fail + if i%3 == 0 { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + + // internal nonce tracking should be reset to startingNonce value every 3rd tx + require.Equal(t, []uint64{1, 1, 2, 3, 1, 2, 3, 1}, nonces) +} + +func TestMinFees(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + desc string + minBaseFee *big.Int + minTipCap *big.Int + expectMinBaseFee bool + expectMinTipCap bool + }{ + { + desc: "no-mins", + }, + { + desc: "high-min-basefee", + minBaseFee: big.NewInt(10_000_000), + expectMinBaseFee: true, + }, + { + desc: "high-min-tipcap", + minTipCap: big.NewInt(1_000_000), + expectMinTipCap: true, + }, + { + desc: "high-mins", + minBaseFee: big.NewInt(10_000_000), + minTipCap: big.NewInt(1_000_000), + expectMinBaseFee: true, + expectMinTipCap: true, + }, + { + desc: "low-min-basefee", + minBaseFee: big.NewInt(1), + }, + { + desc: "low-min-tipcap", + minTipCap: big.NewInt(1), + }, + { + desc: "low-mins", + minBaseFee: big.NewInt(1), + minTipCap: big.NewInt(1), + }, + } { + t.Run(tt.desc, func(t *testing.T) { + t.Parallel() + conf := configWithNumConfs(1) + conf.MinBaseFee = tt.minBaseFee + conf.MinTipCap = tt.minTipCap + h := newTestHarnessWithConfig(t, conf) + + tip, baseFee, err := h.mgr.suggestGasPriceCaps(context.TODO()) + require.NoError(t, err) + + if tt.expectMinBaseFee { + require.Equal(t, tt.minBaseFee, baseFee, "expect suggested base fee to equal MinBaseFee") + } else { + require.Equal(t, h.gasPricer.baseBaseFee, baseFee, "expect suggested base fee to equal mock base fee") + } + + if tt.expectMinTipCap { + require.Equal(t, tt.minTipCap, tip, "expect suggested tip to equal MinTipCap") + } else { + require.Equal(t, h.gasPricer.baseGasTipFee, tip, "expect suggested tip to equal mock tip") + } + }) + } +} + +func uint64Ptr(i uint64) *uint64 { + return &i +} diff --git a/scripts/buf.gen.gogo.yaml b/scripts/buf.gen.gogo.yaml new file mode 100644 index 00000000..81e5929d --- /dev/null +++ b/scripts/buf.gen.gogo.yaml @@ -0,0 +1,8 @@ +version: v1 +plugins: + - name: gocosmos + out: . + opt: + - plugins=grpc + - Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types + - Mcosmos/orm/v1/orm.proto=cosmossdk.io/orm diff --git a/scripts/buf.gen.orm.yaml b/scripts/buf.gen.orm.yaml new file mode 100644 index 00000000..9d72e9b8 --- /dev/null +++ b/scripts/buf.gen.orm.yaml @@ -0,0 +1,17 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: github.com/piplabs/story + override: + buf.build/cosmos/cosmos-sdk: cosmossdk.io/api # Required to import the Cosmos SDK api module +plugins: + - name: go + out: . + opt: paths=source_relative + - plugin: go-grpc + out: . + opt: paths=source_relative + - name: go-cosmos-orm + out: . + opt: paths=source_relative diff --git a/scripts/buf.gen.pulsar.yaml b/scripts/buf.gen.pulsar.yaml new file mode 100644 index 00000000..61b1971b --- /dev/null +++ b/scripts/buf.gen.pulsar.yaml @@ -0,0 +1,17 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: cosmossdk.io/api + except: + - buf.build/googleapis/googleapis + - buf.build/cosmos/gogo-proto + - buf.build/cosmos/cosmos-proto + override: +plugins: + - name: go-pulsar + out: . + opt: paths=source_relative + - name: go-grpc + out: . + opt: paths=source_relative diff --git a/scripts/buf_generate.sh b/scripts/buf_generate.sh new file mode 100755 index 00000000..d1434ed7 --- /dev/null +++ b/scripts/buf_generate.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# This scripts generates all the protobufs using 'buf generate'. +# Cosmos however uses a mix of gogo, pulsar and orm plugins for code generation. +# So we manually call buf generate for each type of plugin. + +function bufgen() { + TYPE=$1 # Either orm,pulsar,proto + DIR=$2 # Path to dir containing protos to generate + + # Skip if ${DIR}/*.proto does not exist + if ! test -n "$(find "${DIR}" -maxdepth 1 -name '*.proto')"; then + return + fi + + + echo " ${TYPE}: ${DIR}" + + buf generate \ + --template="scripts/buf.gen.${TYPE}.yaml" \ + --path="${DIR}" +} + +# Ensure we are in the root of the repo, so ${pwd}/go.mod must exit +if [ ! -f go.mod ]; then + echo "Please run this script from the root of the repository" + exit 1 +fi + +echo "Generating pulsar protos for cosmos module config" +for DIR in client/x/*/module/ +do + bufgen pulsar "${DIR}" +done + +echo "Generating gogo protos for cosmos module types" +for DIR in client/x/*/types/ +do + bufgen gogo "${DIR}" +done + +echo "Generating orm protos for cosmos keeper orm" +for DIR in client/x/*/keeper/ +do + bufgen orm "${DIR}" +done diff --git a/scripts/build_docker.sh b/scripts/build_docker.sh new file mode 100755 index 00000000..ae873474 --- /dev/null +++ b/scripts/build_docker.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# ./build_docker.sh +# Builds a single docker image for the provided app using local git ref. + +APP=$1 + +if [ -z "$APP" ]; then + echo "Please provide the app name: ./single_docker.sh " + exit 1 +fi + +GOOS=linux goreleaser build --single-target --snapshot --clean --id="${APP}" + +GITREF=$(git rev-parse --short=7 HEAD) + +cd dist/${APP}* || exit 1 + +docker build -f "../../${APP}/Dockerfile" . -t "iliadops/${APP}:${GITREF}" + +echo "Built docker image: iliadops/${APP}:${GITREF}" diff --git a/scripts/dummy.go b/scripts/dummy.go new file mode 100644 index 00000000..96e73084 --- /dev/null +++ b/scripts/dummy.go @@ -0,0 +1,3 @@ +// Package scripts is a empty go package only used for the tools.go pattern. +// This file is required for go test ./... to work. +package scripts diff --git a/scripts/fix_golden_tests.sh b/scripts/fix_golden_tests.sh new file mode 100755 index 00000000..024c014e --- /dev/null +++ b/scripts/fix_golden_tests.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# fix_golden_tests.sh fixes all golden test fixtures in the repo. + +# Find all unique directories with _test.go files with tutil.RequireGolden* calls. +DIRS=$(grep -rl 'tutil.RequireGolden' . | xargs dirname | uniq) + +# Run `go:generate` for each directory. +for DIR in $DIRS; do + cd "${DIR}" || exit 1 + echo "Fixing golden tests in: ${DIR}" + go generate + cd - || exit 1 +done diff --git a/scripts/install_foundry.sh b/scripts/install_foundry.sh new file mode 100644 index 00000000..a13e8d70 --- /dev/null +++ b/scripts/install_foundry.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Install foundryup and foundry toolkit (forge, cast, anvil) + + +# If already forge installed, exit. (we do not care about cast or anvil +if which forge 1>/dev/null; then + echo "Foundry already installed: $(forge --version)." + echo "Run 'foundryup' to update." + return +fi + +# This tells https://foundry.paradigm.xyz where to install foundryup - +# $FOUNDRY_DIR/bin. This dir is added to $GITHUB_PATH in +# .github/workflows/pre-commit.yml +export FOUNDRY_DIR="$HOME/.foundry"; + + +# If foundryup not installed, install it +if ! which foundryup 1>/dev/null; then + echo "Installing foundryup" + curl -L https://foundry.paradigm.xyz | bash +fi + + +# We use the nightly version, rather than pinning to a specific version. +# foundryup does allow pinning to a specific version, via github tag: +# foundryup --version nightly-24abca6c9133618e0c355842d2be2dd4f36da46d +# Or via git commit: +# foundryup --commit 24abca6 +# But they delete github tags frequently, and installation via commit requires +# rust and lenghty builds. So we use nightly, until versioning becomes an +# issue. + +foundryup --version nightly diff --git a/scripts/install_pnpm.sh b/scripts/install_pnpm.sh new file mode 100644 index 00000000..a13973c5 --- /dev/null +++ b/scripts/install_pnpm.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +VERSION=9.1.0 + +if ! which pnpm 1>/dev/null || [[ $(pnpm --version) != "$VERSION" ]]; then + echo "Installing pnpm@$VERSION" + npm install -g pnpm@$VERSION +fi diff --git a/scripts/mockgen.sh b/scripts/mockgen.sh new file mode 100644 index 00000000..bd9ddab0 --- /dev/null +++ b/scripts/mockgen.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +mockgen_cmd="mockgen" +$mockgen_cmd -package mock -destination testutil/mock/grpc_server.go github.com/cosmos/gogoproto/grpc Server +$mockgen_cmd -package mock -destination testutil/mock/logger.go cosmossdk.io/log Logger +$mockgen_cmd -source=client/x/evmengine/types/expected_keepers.go -package testutil -destination client/x/evmengine/testutil/expected_keepers_mocks.go +$mockgen_cmd -source=client/x/evmstaking/types/expected_keepers.go -package testutil -destination client/x/evmstaking/testutil/expected_keepers_mocks.go diff --git a/scripts/pubkey_to_bech32.go b/scripts/pubkey_to_bech32.go new file mode 100644 index 00000000..6185de29 --- /dev/null +++ b/scripts/pubkey_to_bech32.go @@ -0,0 +1,49 @@ +package scripts + +import ( + "encoding/base64" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/piplabs/story/lib/k1util" +) + +//nolint:unused // main for go run +func main() { + Bech32HRP := "story" + accountPubKeyPrefix := Bech32HRP + "pub" + validatorAddressPrefix := Bech32HRP + "valoper" + validatorPubKeyPrefix := Bech32HRP + "valoperpub" + consNodeAddressPrefix := Bech32HRP + "valcons" + consNodePubKeyPrefix := Bech32HRP + "valconspub" + + // Set and seal config + cfg := sdk.GetConfig() + cfg.SetBech32PrefixForAccount(Bech32HRP, accountPubKeyPrefix) + cfg.SetBech32PrefixForValidator(validatorAddressPrefix, validatorPubKeyPrefix) + cfg.SetBech32PrefixForConsensusNode(consNodeAddressPrefix, consNodePubKeyPrefix) + cfg.Seal() + + // Input the base64 encoded public key (secp256k1) + b64e := "A+m9C7PAkf55wktLV5JfSLir1Hl2u7rKx1Vuv5ZZ53Cl" // pragma: allowlist secret + b64d, _ := base64.StdEncoding.DecodeString(b64e) + + pubkey, err := k1util.PubKeyBytesToCosmos(b64d) + if err != nil { + println(err.Error()) + return + } + println("pubkey", pubkey.String()) + + accAddr := sdk.AccAddress(pubkey.Address().Bytes()) + valAddr := sdk.ValAddress(pubkey.Address().Bytes()) + println("accAddr", accAddr.String()) + println("valAddr", valAddr.String()) + + evmAddr, err := k1util.CosmosPubkeyToEVMAddress(pubkey.Bytes()) + if err != nil { + println(err.Error()) + return + } + println("evmAddr", evmAddr.String()) +} diff --git a/scripts/tools.go b/scripts/tools.go new file mode 100644 index 00000000..a7f8edac --- /dev/null +++ b/scripts/tools.go @@ -0,0 +1,27 @@ +// This file contains build time developer tools used in the iliad repo. +// We use go.mod to control the versions of these tools and dependabot to keep them up to date. +// To install all the tools run: go generate tools.go +// For more info on this pattern, see https://www.jvt.me/posts/2022/06/15/go-tools-dependency-management/. + +// Build tag is require to fool go.mod into adding main packages as dependencies. +// See reference: https://github.com/golang/go/issues/32504 +//go:build tools + +package scripts + +import ( + _ "github.com/bufbuild/buf/cmd/buf" + _ "github.com/cosmos/gogoproto/protoc-gen-gocosmos" + _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +) + +//go:generate echo Installing tools: protobuf +//go:generate go install github.com/bufbuild/buf/cmd/buf +//go:generate go install github.com/bufbuild/buf/cmd/protoc-gen-buf-lint +//go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go +//go:generate go install google.golang.org/grpc/cmd/protoc-gen-go-grpc +//go:generate go install github.com/cosmos/cosmos-proto/cmd/protoc-gen-go-pulsar +//go:generate go install github.com/cosmos/gogoproto/protoc-gen-gocosmos +//go:generate go install cosmossdk.io/orm/cmd/protoc-gen-go-cosmos-orm +//go:generate go install go.uber.org/mock/mockgen@latest diff --git a/scripts/verifypr/README.md b/scripts/verifypr/README.md new file mode 100644 index 00000000..3daad59c --- /dev/null +++ b/scripts/verifypr/README.md @@ -0,0 +1,6 @@ +# verifypr + +Simple script that is called by [verifypr](../../.github/workflows/verifypr.yml) github action +that verifies iliad PRs against the conventional commit template. + +See supported types [here](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#type-enum) diff --git a/scripts/verifypr/main.go b/scripts/verifypr/main.go new file mode 100644 index 00000000..842f7077 --- /dev/null +++ b/scripts/verifypr/main.go @@ -0,0 +1,19 @@ +// Copyright Ā© 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +// Command verifypr provides a tool to verify iliad PRs against our specific conventional commit template. +package main + +import ( + "log" + "os" +) + +func main() { + err := run() + if err != nil { + log.Printf("āŒ Verification failed: %+v\n", err) + os.Exit(1) + } + + log.Println("āœ… Verification Success") +} diff --git a/scripts/verifypr/verify.go b/scripts/verifypr/verify.go new file mode 100644 index 00000000..3fefcaae --- /dev/null +++ b/scripts/verifypr/verify.go @@ -0,0 +1,177 @@ +// Command verifypr provides a tool to verify iliad PRs against the conventional commit template. +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "regexp" + "strings" + + cc "github.com/leodido/go-conventionalcommits" + "github.com/leodido/go-conventionalcommits/parser" +) + +var ( + optionalLink = `(fix\w*\s|close\w*\s|resolve\w*\s)?` // Optional issue linking prefix, see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue. + descRegex = regexp.MustCompile(`^[a-z][-\w\s]+$`) // e.g. "add foo-bar" + scopeRegex = regexp.MustCompile(`^[*\w]+(/[*\w]+)?$`) // e.g. "*" or "foo" or "foo/bar" + issueRegexFull = regexp.MustCompile(`^` + optionalLink + `https://github.com/piplabs/story/issues/\d+$`) + issueRegexShort = regexp.MustCompile(`^` + optionalLink + `#\d+$`) // e.g. "#1334" +) + +// run runs the verification. +func run() error { + pr, err := prFromEnv() + if err != nil { + return err + } + + // Skip dependabot PRs. + if strings.Contains(pr.Title, "deps") && strings.Contains(pr.Body, "dependabot") { + return nil + } + + log.Printf("Verifying iliad PR against template\n") + log.Printf("PR Title: %s\n", pr.Title) + log.Printf("## PR Body:\n%s\n####\n", pr.Body) + + // Convert PR title and body to conventional commit message. + commitMsg := fmt.Sprintf("%s\n\n%s", pr.Title, pr.Body) + + return verify(commitMsg) +} + +type PR struct { + Title string `json:"title"` + Body string `json:"body"` + ID string `json:"node_id"` +} + +// prFromEnv returns the PR by parsing it from "GITHUB_PR" env var or an error. +func prFromEnv() (PR, error) { + const prEnv = "GITHUB_PR" + prJSON, ok := os.LookupEnv(prEnv) + if !ok || strings.TrimSpace(prJSON) == "" { + return PR{}, errors.New("env variable not set") + } + + var pr PR + if err := json.Unmarshal([]byte(prJSON), &pr); err != nil { + return PR{}, err + } + + if pr.Title == "" || pr.Body == "" || pr.ID == "" { + return PR{}, errors.New("pr field not set") + } + + return pr, nil +} + +// verify returns an error if the commit message doesn't correspond to the iliad conventional commit template. +func verify(commitMsg string) error { + // Fix line endings, since conventional commit parser doesn't support CRLF. + commitMsg = strings.ReplaceAll(commitMsg, "\r\n", "\n") + + // Parse conventional commit message. + m := parser.NewMachine() + m.WithTypes(cc.TypesConventional) + msg, err := m.Parse([]byte(commitMsg)) + if err != nil { + return fmt.Errorf("parse conventional commit message: %w", err) + } + + commit, ok := msg.(*cc.ConventionalCommit) + if !ok { + return errors.New("message is not a conventional commit") + } + + // Verify conventional commit message is valid. + if !commit.Ok() { + return errors.New("conventional commit not ok") + } + + // Verify title is valid. + if err := verifyDescription(commit.Description); err != nil { + return err + } + + // Verify body is non-empty. + if commit.Body == nil || *commit.Body == "" { + return errors.New("body empty") + } + + // Verify footer is valid. + if err := verifyFooter(commit); err != nil { + return err + } + + // Verify scope is valid. + if err := verifyScope(commit); err != nil { + return err + } + + return nil +} + +func verifyDescription(description string) error { + const maxLen = 50 + if len(description) > maxLen { + return errors.New("description too long") + } + + if !descRegex.MatchString(description) { + return errors.New("description doesn't match regex") + } + + return nil +} + +func verifyFooter(commit *cc.ConventionalCommit) error { + const issueFooter = "issue" + if len(commit.Footers) == 0 { + return errors.New("missing `issue` section. Please add a github issue for this PR") + } + if len(commit.Footers[issueFooter]) == 0 { + return errors.New("missing `issue` section. Please add a github issue for this PR") + } + + if len(commit.Footers[issueFooter]) != 1 { + return errors.New("invalid number of issue sections, only one allowed") + } + + issue := strings.TrimSpace(commit.Footers[issueFooter][0]) + if issue == "" { + return errors.New("issue section empty") + } else if issue == "none" { + // None is fine + } else if issueRegexFull.MatchString(issue) { + // Full issue URL + } else if issueRegexShort.MatchString(issue) { + // Short issue URL + } else { + return errors.New("invalid issue section") + } + + return nil +} + +func verifyScope(commit *cc.ConventionalCommit) error { + if commit.Scope == nil { + return errors.New("scope not set") + } + + scope := *commit.Scope + + if scope == "" { + return errors.New("scope empty") + } + + if !scopeRegex.MatchString(scope) { + return errors.New("scope doesn't match regex") + } + + return nil +}