diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 1de973a959..17b01d4385 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -135,6 +135,45 @@ jobs: with: environment: ${{ github.event.inputs.environment }} + # TODO(https://github.com/threshold-network/tbtc-v2/pull/1112): remove once + # @keep-network/tbtc-v2 publishes the reservation-router surface (Bridge, + # WalletProposalValidator, RedemptionWatchtower, ReservationRouter, and the + # rest of the tbtc module's required_contracts) under the `development` npm + # tag. Until then, PRs targeting `reservations-epic` compile tbtc-v2 PR #1112 + # (pinned SHA, not a moving branch ref) locally to produce the artifacts + # `make get_artifacts` can't fetch from npm yet. `environment=development` + # only ever reads the `.abi` field off these files (the gen Makefile writes + # a hardcoded zero address rather than reading one - see + # pkg/chain/ethereum/common/gen/Makefile's `_address/%` rule), so a plain + # `hardhat compile` artifact is sufficient; no local deployment needed. See + # ./ci-shims/tbtc-artifacts and the matching Dockerfile step. + - name: Prepare tbtc-v2 artifact shim directory + run: mkdir -p ci-shims/tbtc-artifacts + + - name: Set up Node.js for tbtc-v2 shim build + if: github.base_ref == 'reservations-epic' + uses: actions/setup-node@v4 + with: + node-version: "22.23.1" + + - name: Build tbtc-v2 module artifacts from PR #1112 (temporary shim) + if: github.base_ref == 'reservations-epic' + run: | + set -euo pipefail + git clone --quiet https://github.com/threshold-network/tbtc-v2.git /tmp/tbtc-v2-shim + cd /tmp/tbtc-v2-shim + git checkout --quiet 1c8c1cd1437c077700b372544677aa0f9b08ef87 + cd solidity + corepack enable + git config --global url."https://".insteadOf git:// + yarn install --immutable + yarn build + for contract in Bridge MaintainerProxy LightRelay LightRelayMaintainerProxy \ + WalletProposalValidator RedemptionWatchtower ReservationRouter; do + artifact="$(find build/contracts -iname "$contract.json" -path "*/$contract.sol/*")" + cp "$artifact" "$GITHUB_WORKSPACE/ci-shims/tbtc-artifacts/$contract.json" + done + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.gitignore b/.gitignore index 0c2c04268b..4d015b993b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ # Executables /keep-client +# Temporary CI-only artifact injected before the Docker build; see Dockerfile +# and .github/workflows/client.yml (ReservationRouter tbtc-v2 PR #1112 shim). +/ci-shims/ + # IDEs .vscode/ .idea/ diff --git a/Dockerfile b/Dockerfile index dce9eba139..58e6cefe8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,11 +62,31 @@ COPY ./pkg/protocol/inactivity/gen $APP_DIR/pkg/protocol/inactivity/gen RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.32.0 # Environment is to download published and tagged NPM packages versions. -ARG ENVIRONMENT +# Defaults to `development` to mirror the root Makefile's `ifndef environment` +# fallback (the "Build Docker Build Image" CI step never passes this build-arg). +ARG ENVIRONMENT=development COPY ./Makefile $APP_DIR/Makefile RUN make get_artifacts environment=$ENVIRONMENT +# TODO(https://github.com/threshold-network/tbtc-v2/pull/1112): remove once +# @keep-network/tbtc-v2 publishes Bridge/WalletProposalValidator/RedemptionWatchtower/ +# ReservationRouter (and the rest of the tbtc module's required_contracts) under the +# `development` npm tag. Until then, `get_artifacts` fetches a tbtc-v2 package whose +# Bridge/WalletProposalValidator/RedemptionWatchtower don't yet expose the reservation +# methods this PR binds against, and has no ReservationRouter artifact at all. The +# `client.yml` workflow locally compiles tbtc-v2 PR #1112 (pinned SHA) and drops its +# compiled ABI artifacts for the tbtc module's required_contracts at +# ./ci-shims/tbtc-artifacts/*.json when it runs; this only overrides the tbtc module's +# artifacts, and only for `environment=development` (PR CI) builds - sepolia/mainnet +# builds and the beacon/ecdsa/threshold modules are untouched. +COPY ./ci-shims/tbtc-artifacts /tmp/tbtc-artifacts +RUN if { [ -z "$ENVIRONMENT" ] || [ "$ENVIRONMENT" = "development" ]; } && [ -n "$(ls -A /tmp/tbtc-artifacts 2>/dev/null)" ]; then \ + echo "Using tbtc-v2 module artifacts built from tbtc-v2 PR #1112 (temporary shim)"; \ + cp /tmp/tbtc-artifacts/*.json \ + $APP_DIR/tmp/contracts/development/@keep-network/tbtc-v2/artifacts/; \ +fi + # Need this to resolve imports in generated Ethereum commands. COPY ./config $APP_DIR/config RUN make generate environment=$ENVIRONMENT diff --git a/cmd/start.go b/cmd/start.go index c5bc8902f2..47a82626cf 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -8,6 +8,7 @@ import ( "github.com/keep-network/keep-core/pkg/tbtcpg" "github.com/keep-network/keep-common/pkg/persistence" + "github.com/keep-network/keep-core/build" "github.com/keep-network/keep-core/pkg/bitcoin/electrum" "github.com/keep-network/keep-core/pkg/operator" @@ -22,6 +23,7 @@ import ( "github.com/keep-network/keep-core/pkg/clientinfo" "github.com/keep-network/keep-core/pkg/firewall" "github.com/keep-network/keep-core/pkg/generator" + "github.com/keep-network/keep-core/pkg/maintainer/spv" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/net/libp2p" "github.com/keep-network/keep-core/pkg/net/retransmission" @@ -92,7 +94,11 @@ func start(cmd *cobra.Command) error { // Wire performance metrics into network provider if available var perfMetrics *clientinfo.PerformanceMetrics if clientInfoRegistry != nil { - perfMetrics = clientinfo.NewPerformanceMetrics(ctx, clientInfoRegistry) + perfMetrics = clientinfo.NewPerformanceMetrics( + ctx, + clientInfoRegistry, + clientConfig.Tbtc.Reservations.Enabled, + ) // Type assert to libp2p provider to set metrics recorder // The provider struct is not exported, so we use interface assertion if setter, ok := netProvider.(interface { @@ -160,9 +166,10 @@ func start(cmd *cobra.Command) error { proposalGenerator := tbtcpg.NewProposalGenerator( tbtcChain, btcChain, + clientConfig.Tbtc.Reservations.Enabled, ) - err = tbtc.Initialize( + resolver, err := tbtc.Initialize( ctx, tbtcChain, btcChain, @@ -177,7 +184,29 @@ func start(cmd *cobra.Command) error { clientConfig.Ethereum.Network, ) if err != nil { - return fmt.Errorf("error initializing TBTC: [%v]", err) + return fmt.Errorf("cannot initialize TBTC: [%v]", err) + } + + // Wire the reservation watchers (stranding, stale-deposit, + // action-timeout) directly against the same tbtcChain handle: + // cmd/start.go already imports both tbtc and spv, so there is no + // import-cycle reason to thread this through tbtc.Initialize via a + // callback type. Gated on the same flag that gates the reservation + // proposal generator tasks above. Failing to wire the watchers is + // fatal: the operator opted into reservations, so a missing + // watcher would silently strand anchors. + if clientConfig.Tbtc.Reservations.Enabled { + if err := spv.WireReservationWatchers( + ctx, + tbtcChain, + tbtcChain, + resolver, + ); err != nil { + return fmt.Errorf( + "failed to wire reservation watchers: [%v]", + err, + ) + } } } diff --git a/config/config_test.go b/config/config_test.go index f8de558c4e..117c0dbe5f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -233,6 +233,14 @@ func TestReadConfigFromFile(t *testing.T) { readValueFunc: func(c *Config) interface{} { return c.Maintainer.Spv.IdleBackoffTime }, expectedValue: 15 * time.Minute, }, + "Maintainer.Spv.Reservations.Enabled": { + readValueFunc: func(c *Config) interface{} { return c.Maintainer.Spv.Reservations.Enabled }, + expectedValue: true, + }, + "Tbtc.Reservations.Enabled": { + readValueFunc: func(c *Config) interface{} { return c.Tbtc.Reservations.Enabled }, + expectedValue: true, + }, } for _, filePath := range filePaths { diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 50e42be20e..5f02098d1b 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -17,9 +17,11 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-common/pkg/chain/ethereum" + "github.com/keep-network/keep-core/pkg/chain" ecdsaabi "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/abi" ecdsacontract "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/contract" @@ -64,6 +66,10 @@ type TbtcChain struct { sortitionPool *ecdsacontract.EcdsaSortitionPool walletProposalValidator *tbtccontract.WalletProposalValidator redemptionWatchtower *tbtccontract.RedemptionWatchtower + // reservationRouter is the abigen binding for ReservationRouter.sol's ABI + // constructed against the Bridge address (see reservationRouterBinding for + // the address invariant explanation). + reservationRouter *tbtccontract.ReservationRouter // ecdsaDkgValidatorAddress optional; when zero, TBTC uses defaultGroupParameters(network). ecdsaDkgValidatorAddress common.Address @@ -266,6 +272,14 @@ func newTbtcChain( ) } + reservationRouter, err := reservationRouterBinding(bridgeAddress, baseChain) + if err != nil { + return nil, fmt.Errorf( + "failed to attach to ReservationRouter binding: [%v]", + err, + ) + } + return &TbtcChain{ baseChain: baseChain, bridge: bridge, @@ -274,11 +288,38 @@ func newTbtcChain( sortitionPool: sortitionPool, walletProposalValidator: walletProposalValidator, redemptionWatchtower: redemptionWatchtower, + reservationRouter: reservationRouter, ecdsaDkgValidatorAddress: ecdsaDkgValidatorAddress, sweptDepositsCache: cache.NewGenericTimeCache[*tbtc.DepositChainRequest](sweptDepositsCachePeriod), }, nil } +// reservationRouterBinding constructs the ReservationRouter abigen binding +// pointed at the Bridge address. The router code only ever executes via +// Bridge.fallback's delegatecall, so the binding MUST be constructed against +// the Bridge address: the deployed router contract holds its own empty +// storage, so any call routed to its standalone address would either revert +// (writes) or return zeros (views); events emitted by router code carry the +// Bridge's address in their log because delegatecall preserves the caller's +// address context. The router's own deployment address is only needed for +// the one-time governance Bridge.setReservationRouter(routerAddress) call, +// which is out of scope here. +func reservationRouterBinding( + bridgeAddress common.Address, + baseChain *baseChain, +) (*tbtccontract.ReservationRouter, error) { + return tbtccontract.NewReservationRouter( + bridgeAddress, + baseChain.chainID, + baseChain.key, + baseChain.client, + baseChain.nonceManager, + baseChain.miningWaiter, + baseChain.blockCounter, + baseChain.transactionMutex, + ) +} + // EcdsaWalletGroupParametersFromChain mirrors EcdsaDkgValidator sizing constants // when EcdsaDkgValidator contract address was configured under [ethereum] // contract addresses or developer.ecdsaDkgValidatorAddress alias. When absent, @@ -2408,3 +2449,858 @@ func (tc *TbtcChain) GetRedemptionDelay( func (tc *TbtcChain) GetDepositMinAge() (uint32, error) { return tc.walletProposalValidator.DEPOSITMINAGE() } + +// GetReservation returns the on-chain reservation record for the given +// reservation key via the reservationRouter binding (see reservationRouterBinding). +func (tc *TbtcChain) GetReservation( + reservationKey *big.Int, +) (*tbtc.Reservation, error) { + abiReservation, err := tc.reservationRouter.Reservations(reservationKey) + if err != nil { + return nil, fmt.Errorf( + "cannot get reservation [0x%x]: [%v]", + reservationKey, + err, + ) + } + + reservation, err := convertReservationFromAbiType(abiReservation) + if err != nil { + return nil, fmt.Errorf( + "cannot convert reservation [0x%x] from abi type: [%v]", + reservationKey, + err, + ) + } + + return reservation, nil +} + +// GetReservationAction returns the on-chain action record for the given +// reservation key and request nonce. +func (tc *TbtcChain) GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, +) (*tbtc.ReservationAction, error) { + abiAction, err := tc.reservationRouter.ReservationActions( + reservationKey, + requestNonce, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot get reservation action [0x%x:%d]: [%v]", + reservationKey, + requestNonce, + err, + ) + } + + action, err := convertReservationActionFromAbiType(abiAction) + if err != nil { + return nil, fmt.Errorf( + "cannot convert reservation action [0x%x:%d] from abi type: [%v]", + reservationKey, + requestNonce, + err, + ) + } + + return action, nil +} + +// ReservationParameters returns the current on-chain Bridge reservation +// parameters (10-tuple) via the reservationRouter binding (see reservationRouterBinding). +func (tc *TbtcChain) ReservationParameters() ( + *tbtc.ReservationParameters, + error, +) { + abiParameters, err := tc.reservationRouter.ReservationParameters() + if err != nil { + return nil, fmt.Errorf( + "cannot get reservation parameters: [%v]", + err, + ) + } + + return convertReservationParametersFromAbiType(abiParameters), nil +} + +// ValidateReservationAnchorProposal asks the WalletProposalValidator +// whether the given anchor proposal is valid for the given wallet and +// reserved deposit. The validator is a separate contract reached at its +// own deployed address. +func (tc *TbtcChain) ValidateReservationAnchorProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.ReservationAnchorProposal, + depositExtraInfo struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, +) error { + // WalletProposalValidator's DepositExtraInfo.FundingTx is typed as + // BitcoinTxInfo2 because the BitcoinTxInfo struct is renamed via the + // collision hook in gen/Makefile (Bridge keeps the un-suffixed name; + // WalletProposalValidator gets the 2-suffix; MaintainerProxy the 3; + // ReservationRouter the 4). Mirroring the existing + // ValidateDepositSweepProposal pattern. + fundingTx := tbtcabi.BitcoinTxInfo2{ + Version: depositExtraInfo.FundingTx.SerializeVersion(), + InputVector: depositExtraInfo.FundingTx.SerializeInputs(), + OutputVector: depositExtraInfo.FundingTx.SerializeOutputs(), + Locktime: depositExtraInfo.FundingTx.SerializeLocktime(), + } + + depositKey := tbtcabi.WalletProposalValidatorDepositKey{ + FundingTxHash: proposal.DepositFundingTxHash, + FundingOutputIndex: proposal.DepositFundingOutputIndex, + } + + abiExtraInfo := tbtcabi.WalletProposalValidatorDepositExtraInfo{ + FundingTx: fundingTx, + BlindingFactor: depositExtraInfo.Deposit.BlindingFactor, + WalletPubKeyHash: depositExtraInfo.Deposit.WalletPublicKeyHash, + RefundPubKeyHash: depositExtraInfo.Deposit.RefundPublicKeyHash, + RefundLocktime: depositExtraInfo.Deposit.RefundLocktime, + } + + abiProposal := tbtcabi.WalletProposalValidatorReservationAnchorProposal{ + WalletPubKeyHash: walletPublicKeyHash, + DepositKey: depositKey, + AnchorTxFee: proposal.AnchorTxFee, + } + + valid, err := tc.walletProposalValidator.ValidateReservationAnchorProposal( + abiProposal, + abiExtraInfo, + ) + if err != nil { + return fmt.Errorf("validation failed: [%v]", err) + } + + // Should never happen because `validateReservationAnchorProposal` + // returns true or reverts (returns an error) but do the check just in + // case. + if !valid { + return fmt.Errorf("unexpected validation result") + } + + return nil +} + +// ValidateReservationReanchorProposal asks the WalletProposalValidator +// whether the given re-anchor proposal is valid for the given source +// wallet. The validator is a separate contract reached at its own deployed +// address. +func (tc *TbtcChain) ValidateReservationReanchorProposal( + sourceWalletPublicKeyHash [20]byte, + proposal *tbtc.ReservationReanchorProposal, +) error { + abiProposal := tbtcabi.WalletProposalValidatorReservationReanchorProposal{ + SourceWalletPubKeyHash: sourceWalletPublicKeyHash, + ReservationKey: proposal.ReservationKey, + TargetWalletPubKeyHash: proposal.TargetWalletPublicKeyHash, + ReanchorTxFee: proposal.ReanchorTxFee, + } + + valid, err := tc.walletProposalValidator.ValidateReservationReanchorProposal( + abiProposal, + ) + if err != nil { + return fmt.Errorf("validation failed: [%v]", err) + } + + // Should never happen because `validateReservationReanchorProposal` + // returns true or reverts (returns an error) but do the check just in + // case. + if !valid { + return fmt.Errorf("unexpected validation result") + } + + return nil +} + +// convertReservationFromAbiType converts the ReservationRouter-specific +// Reservation.ReservationRequest ABI struct to the TBTC application +// `tbtc.Reservation` representation. +// +// Field omissions (intentional, mirroring the Solidity-to-Go struct shrink): +// +// - `CumulativeReanchorFee`: written by every re-anchor hop but not +// exposed through the Go-side reservation; m1 has no fee-ceiling +// enforcement, so the field is dropped on the Go boundary. A later +// milestone that adds a fee ceiling should re-export this field on +// `tbtc.Reservation`. +// +// Anchor shape reassembly: the on-chain request splits the anchor UTXO into +// `anchorAmount`, `anchorTxHash`, and `anchorTxOutputIndex`; the Go-side +// representation folds those three back into a single +// `bitcoin.UnspentTransactionOutput` for consistency with the rest of the +// reservation API. +func convertReservationFromAbiType( + abiReservation tbtcabi.ReservationReservationRequest, +) (*tbtc.Reservation, error) { + state, err := parseReservationState(abiReservation.State) + if err != nil { + return nil, fmt.Errorf("cannot parse reservation state: [%v]", err) + } + + anchorUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: abiReservation.AnchorTxHash, + OutputIndex: abiReservation.AnchorTxOutputIndex, + }, + Value: int64(abiReservation.AnchorAmount), + } + + return &tbtc.Reservation{ + Owner: chain.Address(abiReservation.Owner.String()), + MintedAmount: abiReservation.MintedAmount, + AcceptedAt: abiReservation.AcceptedAt, + WalletPublicKeyHash: abiReservation.WalletPubKeyHash, + AnchorUtxo: anchorUtxo, + ExpiresAt: abiReservation.ExpiresAt, + State: state, + RequestNonce: abiReservation.RequestNonce, + RetryCredit: abiReservation.RetryCredit, + DissolutionEligibleAt: abiReservation.DissolutionEligibleAt, + }, nil +} + +// convertReservationActionFromAbiType converts the ReservationRouter- +// specific Reservation.ReservationAction ABI struct to the TBTC +// application `tbtc.ReservationAction` representation. +// +// Field omissions (intentional): +// +// - `SourceAnchorUtxoHash`, `UsedRetryCredit`, +// `Watchtower{Default,LevelOne,LevelTwo}Delay`, +// `RetryCreditSourceNonce`: written for governance / late-settlement +// reconciliation but not read by the operator client in m1. +// +// The on-chain `actionDataHash` field is polymorphic across action types: +// it carries the keccak256 of the redeemer output script for redemptions, +// the wallet main UTXO hash for dissolutions, and is zero otherwise. The +// Go-side struct splits that polymorphism into two named fields +// (`RedeemerOutputScriptHash` for redemptions, `ExpectedMainUtxoHash` +// for dissolutions); we route `actionDataHash` to the field that matches +// the action's type and zero the other. +func convertReservationActionFromAbiType( + abiAction tbtcabi.ReservationReservationAction, +) (*tbtc.ReservationAction, error) { + actionType, err := parseReservationActionType(abiAction.ActionType) + if err != nil { + return nil, fmt.Errorf( + "cannot parse reservation action type: [%v]", + err, + ) + } + + state, err := parseReservationActionState(abiAction.State) + if err != nil { + return nil, fmt.Errorf( + "cannot parse reservation action state: [%v]", + err, + ) + } + + var ( + redeemerOutputScriptHash [32]byte + expectedMainUtxoHash [32]byte + ) + switch actionType { + case tbtc.ReservationActionTypeRedemption: + redeemerOutputScriptHash = abiAction.ActionDataHash + case tbtc.ReservationActionTypeDissolution: + expectedMainUtxoHash = abiAction.ActionDataHash + } + + return &tbtc.ReservationAction{ + TargetWalletPublicKeyHash: abiAction.TargetWalletPubKeyHash, + RequestedAt: abiAction.RequestedAt, + TimeoutAt: abiAction.TimeoutAt, + TxMaxFee: abiAction.TxMaxFee, + ActionType: actionType, + State: state, + FeePaid: abiAction.FeePaid, + Redeemer: chain.Address(abiAction.Redeemer.String()), + Amount: abiAction.Amount, + RedeemerOutputScriptHash: redeemerOutputScriptHash, + ExpectedMainUtxoHash: expectedMainUtxoHash, + IsPartial: abiAction.IsPartial, + }, nil +} + +// convertReservationParametersFromAbiType converts the ReservationRouter +// 10-tuple to the `tbtc.ReservationParameters` representation. +func convertReservationParametersFromAbiType( + abiParameters struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 + }, +) *tbtc.ReservationParameters { + return &tbtc.ReservationParameters{ + ReservationVault: chain.Address(abiParameters.ReservationVault.String()), + ReservationMinAmount: abiParameters.ReservationMinAmount, + ReservationTxMaxFee: abiParameters.ReservationTxMaxFee, + ReservationTermSeconds: abiParameters.ReservationTermSeconds, + ReservationDissolutionDelay: abiParameters.ReservationDissolutionDelay, + ReservationMaxTotalAmount: abiParameters.ReservationMaxTotalAmount, + ReservationTotalAmount: abiParameters.ReservationTotalAmount, + MaxReservationsPerWallet: abiParameters.MaxReservationsPerWallet, + ReservationActionTimeout: abiParameters.ReservationActionTimeout, + ReservationRenewalWindowSeconds: abiParameters.ReservationRenewalWindowSeconds, + } +} + +// parseReservationState converts the on-chain ReservationState enum +// (uint8) to the tbtc.ReservationState value. Values match the Solidity +// declaration one-for-one (Unknown=0, Active=1, ActionPending=2, +// Closed=3, Stranded=4). +func parseReservationState(value uint8) (tbtc.ReservationState, error) { + switch value { + case 0: + return tbtc.ReservationStateUnknown, nil + case 1: + return tbtc.ReservationStateActive, nil + case 2: + return tbtc.ReservationStateActionPending, nil + case 3: + return tbtc.ReservationStateClosed, nil + case 4: + return tbtc.ReservationStateStranded, nil + default: + return 0, fmt.Errorf("unexpected reservation state value: [%d]", value) + } +} + +// parseReservationActionType converts the on-chain ActionType enum +// (uint8) to the tbtc.ReservationActionType value. Values match the +// Solidity declaration one-for-one (None=0, Acceptance=1, Redemption=2, +// Reanchor=3, Dissolution=4). +func parseReservationActionType(value uint8) (tbtc.ReservationActionType, error) { + switch value { + case 0: + return tbtc.ReservationActionTypeNone, nil + case 1: + return tbtc.ReservationActionTypeAcceptance, nil + case 2: + return tbtc.ReservationActionTypeRedemption, nil + case 3: + return tbtc.ReservationActionTypeReanchor, nil + case 4: + return tbtc.ReservationActionTypeDissolution, nil + default: + return 0, fmt.Errorf("unexpected reservation action type value: [%d]", value) + } +} + +// parseReservationActionState converts the on-chain ActionState enum +// (uint8) to the tbtc.ReservationActionState value. Values match the +// Solidity declaration one-for-one (Unknown=0, Pending=1, Settled=2, +// TimedOut=3, Vetoed=4, Superseded=5). +func parseReservationActionState(value uint8) (tbtc.ReservationActionState, error) { + switch value { + case 0: + return tbtc.ReservationActionStateUnknown, nil + case 1: + return tbtc.ReservationActionStatePending, nil + case 2: + return tbtc.ReservationActionStateSettled, nil + case 3: + return tbtc.ReservationActionStateTimedOut, nil + case 4: + return tbtc.ReservationActionStateVetoed, nil + case 5: + return tbtc.ReservationActionStateSuperseded, nil + default: + return 0, fmt.Errorf("unexpected reservation action state value: [%d]", value) + } +} + +// RequestReservationAcceptance calls the Bridge (via reservationRouter binding, +// see reservationRouterBinding) to start a new reservation acceptance action +// generation for the given reservation. +func (tc *TbtcChain) RequestReservationAcceptance( + reservationKey *big.Int, + walletPublicKeyHash [20]byte, +) error { + gasEstimate, err := tc.reservationRouter.RequestReservationAcceptanceGasEstimate( + reservationKey, + walletPublicKeyHash, + ) + if err != nil { + return err + } + + gasEstimateWithMargin := float64(gasEstimate) * float64(1.2) + + _, err = tc.reservationRouter.RequestReservationAcceptance( + reservationKey, + walletPublicKeyHash, + ethutil.TransactionOptions{ + GasLimit: uint64(gasEstimateWithMargin), + }, + ) + + return err +} + +// RequestReservationReanchor asks the Bridge (via its ReservationRouter +// delegatecall target) to start a new reservation re-anchor action generation +// for the given reservation, targeting the given wallet. +func (tc *TbtcChain) RequestReservationReanchor( + reservationKey *big.Int, + targetWalletPublicKeyHash [20]byte, +) error { + gasEstimate, err := tc.reservationRouter.RequestReservationReanchorGasEstimate( + reservationKey, + targetWalletPublicKeyHash, + ) + if err != nil { + return err + } + + gasEstimateWithMargin := float64(gasEstimate) * float64(1.2) + + _, err = tc.reservationRouter.RequestReservationReanchor( + reservationKey, + targetWalletPublicKeyHash, + ethutil.TransactionOptions{ + GasLimit: uint64(gasEstimateWithMargin), + }, + ) + + return err +} + +// SubmitReservationProof submits an SPV proof for the given reservation +// action generation to the Bridge. The proof path is onlySpvMaintainer on +// the router; the call goes through Bridge.fallback's delegatecall so the +// router code reads the Bridge's isSpvMaintainer mapping at the Bridge's +// address. +func (tc *TbtcChain) SubmitReservationProof( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + proof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, +) error { + abiTxInfo := tbtcabi.BitcoinTxInfo4{ + Version: txInfo.Version, + InputVector: txInfo.InputVector, + OutputVector: txInfo.OutputVector, + Locktime: txInfo.Locktime, + } + abiProof := tbtcabi.BitcoinTxProof3{ + MerkleProof: proof.MerkleProof, + TxIndexInBlock: proof.TxIndexInBlock, + BitcoinHeaders: proof.BitcoinHeaders, + CoinbasePreimage: proof.CoinbasePreimage, + CoinbaseProof: proof.CoinbaseProof, + } + abiUtxo := tbtcabi.BitcoinTxUTXO4{ + TxHash: mainUtxo.TxHash, + TxOutputIndex: mainUtxo.TxOutputIndex, + TxOutputValue: mainUtxo.TxOutputValue, + } + + gasEstimate, err := tc.reservationRouter.SubmitReservationProofGasEstimate( + proofType, + abiTxInfo, + abiProof, + abiUtxo, + reservationKey, + requestNonce, + ) + if err != nil { + return err + } + + // The original estimate for this contract call is too low; the + // reservation proof path dispatches into ReservationProofs.submit*Proof, + // which performs a non-trivial amount of storage I/O. Apply a 20% + // margin mirroring the existing SubmitRedemptionProofWithReimbursement + // pattern in this file. + gasEstimateWithMargin := float64(gasEstimate) * float64(1.2) + + _, err = tc.reservationRouter.SubmitReservationProof( + proofType, + abiTxInfo, + abiProof, + abiUtxo, + reservationKey, + requestNonce, + ethutil.TransactionOptions{ + GasLimit: uint64(gasEstimateWithMargin), + }, + ) + + return err +} + +// NotifyReservationActionTimeout notifies the Bridge that the timeout for +// the given reservation action generation has elapsed. +func (tc *TbtcChain) NotifyReservationActionTimeout( + reservationKey *big.Int, + walletMembersIDs []uint32, +) error { + gasEstimate, err := tc.reservationRouter.NotifyReservationActionTimeoutGasEstimate( + reservationKey, + walletMembersIDs, + ) + if err != nil { + return err + } + + gasEstimateWithMargin := float64(gasEstimate) * float64(1.2) + + _, err = tc.reservationRouter.NotifyReservationActionTimeout( + reservationKey, + walletMembersIDs, + ethutil.TransactionOptions{ + GasLimit: uint64(gasEstimateWithMargin), + }, + ) + + return err +} + +// NotifyStaleReservedDeposit notifies the Bridge that the given reserved +// deposit's wallet did not anchor it within the reservation-action timeout. +func (tc *TbtcChain) NotifyStaleReservedDeposit( + depositKey *big.Int, +) error { + gasEstimate, err := tc.reservationRouter.NotifyStaleReservedDepositGasEstimate( + depositKey, + ) + if err != nil { + return err + } + + gasEstimateWithMargin := float64(gasEstimate) * float64(1.2) + + _, err = tc.reservationRouter.NotifyStaleReservedDeposit( + depositKey, + ethutil.TransactionOptions{ + GasLimit: uint64(gasEstimateWithMargin), + }, + ) + + return err +} + +// NotifyReservationStranded notifies the Bridge that the wallet custodying +// the given reservation has been closed or terminated. +func (tc *TbtcChain) NotifyReservationStranded( + reservationKey *big.Int, +) error { + gasEstimate, err := tc.reservationRouter.NotifyReservationStrandedGasEstimate( + reservationKey, + ) + if err != nil { + return err + } + + gasEstimateWithMargin := float64(gasEstimate) * float64(1.2) + + _, err = tc.reservationRouter.NotifyReservationStranded( + reservationKey, + ethutil.TransactionOptions{ + GasLimit: uint64(gasEstimateWithMargin), + }, + ) + + return err +} + +// ReservationCaps returns the cap parameters that gate reservation +// acceptance via the reservationRouter binding (see reservationRouterBinding). +func (tc *TbtcChain) ReservationCaps() ( + uint64, + uint64, + error, +) { + caps, err := tc.reservationRouter.ReservationCaps() + if err != nil { + return 0, 0, fmt.Errorf( + "cannot get reservation caps: [%v]", + err, + ) + } + + return caps.MaxReservationsAmountPerWallet, caps.ReservationMaxSingleAmount, nil +} + +// WalletReservationsAmount returns the aggregate satoshi amount currently +// anchored by the given wallet across all of its reservations. +func (tc *TbtcChain) WalletReservationsAmount( + walletPublicKeyHash [20]byte, +) (uint64, error) { + amount, err := tc.reservationRouter.WalletReservationsAmount(walletPublicKeyHash) + if err != nil { + return 0, fmt.Errorf( + "cannot get wallet reservations amount for [0x%x]: [%v]", + walletPublicKeyHash, + err, + ) + } + + return amount, nil +} + +// WalletReservationsCount returns the number of reservations currently +// custodied by the given wallet. +func (tc *TbtcChain) WalletReservationsCount( + walletPublicKeyHash [20]byte, +) (uint32, error) { + count, err := tc.reservationRouter.WalletReservationsCount(walletPublicKeyHash) + if err != nil { + return 0, fmt.Errorf( + "cannot get wallet reservations count for [0x%x]: [%v]", + walletPublicKeyHash, + err, + ) + } + + return count, nil +} + +// WalletReservations returns the reservation keys for all reservations +// currently custodied by the given wallet. +func (tc *TbtcChain) WalletReservations( + walletPublicKeyHash [20]byte, +) ([]*big.Int, error) { + keys, err := tc.reservationRouter.WalletReservations(walletPublicKeyHash) + if err != nil { + return nil, fmt.Errorf( + "cannot get wallet reservations for [0x%x]: [%v]", + walletPublicKeyHash, + err, + ) + } + + return keys, nil +} + +// ReservedDepositWallet returns the wallet public key hash to which the +// given reserved deposit was revealed. Returns the zero hash if the +// deposit is not a reserved deposit. +func (tc *TbtcChain) ReservedDepositWallet( + depositKey *big.Int, +) ([20]byte, error) { + walletPublicKeyHash, err := tc.reservationRouter.ReservedDepositWallet(depositKey) + if err != nil { + return [20]byte{}, fmt.Errorf( + "cannot get reserved deposit wallet for [0x%x]: [%v]", + depositKey, + err, + ) + } + + return walletPublicKeyHash, nil +} + +// ActiveReservationsCount returns the current count of active reservations +// across all wallets and the cap on that count. +func (tc *TbtcChain) ActiveReservationsCount() (uint32, uint32, error) { + activeReservationsCount, err := tc.reservationRouter.ActiveReservationsCount() + if err != nil { + return 0, 0, fmt.Errorf( + "cannot get active reservations count: [%v]", + err, + ) + } + + return activeReservationsCount.Count, activeReservationsCount.MaxActive, nil +} + +// IsReservedDeposit returns true if the given deposit was revealed with +// the reservation vault address and is therefore a reservation rather than +// a default deposit. +func (tc *TbtcChain) IsReservedDeposit( + depositKey *big.Int, +) (bool, error) { + isReserved, err := tc.bridge.IsReservedDeposit(depositKey) + if err != nil { + return false, fmt.Errorf( + "cannot check if deposit [0x%x] is reserved: [%v]", + depositKey, + err, + ) + } + + return isReserved, nil +} + +// OnReservationAcceptanceRequested registers a callback that is invoked +// when an on-chain ReservationAcceptanceRequested event is seen. The +// subscription filters against the Bridge's address (the binding is bound +// to the Bridge address; delegatecall preserves the caller's address +// context so events emitted by router code carry the Bridge's address). +func (tc *TbtcChain) OnReservationAcceptanceRequested( + handler func(event *tbtc.ReservationAcceptanceRequestedEvent), +) subscription.EventSubscription { + onEvent := func( + reservationKey *big.Int, + requestNonce uint64, + walletPublicKeyHash [20]byte, + depositAmount uint64, + txMaxFee uint64, + timeoutAt uint32, + blockNumber uint64, + ) { + handler(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: reservationKey, + RequestNonce: requestNonce, + WalletPublicKeyHash: walletPublicKeyHash, + DepositAmount: depositAmount, + TxMaxFee: txMaxFee, + TimeoutAt: timeoutAt, + BlockNumber: blockNumber, + }) + } + + return tc.reservationRouter.ReservationAcceptanceRequestedEvent( + nil, + nil, + nil, + ).OnEvent(onEvent) +} + +// PastReservationAcceptanceRequestedEvents fetches past +// ReservationAcceptanceRequested events according to the provided filter +// or unfiltered if the filter is nil. +func (tc *TbtcChain) PastReservationAcceptanceRequestedEvents( + filter *tbtc.ReservationAcceptanceRequestedEventFilter, +) ([]*tbtc.ReservationAcceptanceRequestedEvent, error) { + var startBlock uint64 + var endBlock *uint64 + var reservationKey []*big.Int + var walletPublicKeyHash [][20]byte + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + reservationKey = filter.ReservationKey + walletPublicKeyHash = filter.WalletPublicKeyHash + } + + events, err := tc.reservationRouter.PastReservationAcceptanceRequestedEvents( + startBlock, + endBlock, + reservationKey, + walletPublicKeyHash, + ) + if err != nil { + return nil, err + } + + convertedEvents := make([]*tbtc.ReservationAcceptanceRequestedEvent, 0) + for _, event := range events { + convertedEvents = append(convertedEvents, &tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: event.ReservationKey, + RequestNonce: event.RequestNonce, + WalletPublicKeyHash: event.WalletPubKeyHash, + DepositAmount: event.DepositAmount, + TxMaxFee: event.TxMaxFee, + TimeoutAt: event.TimeoutAt, + BlockNumber: event.Raw.BlockNumber, + }) + } + + sort.SliceStable(convertedEvents, func(i, j int) bool { + return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber + }) + + return convertedEvents, nil +} + +// OnReservationReanchorRequested registers a callback that is invoked +// when an on-chain ReservationReanchorRequested event is seen. +func (tc *TbtcChain) OnReservationReanchorRequested( + handler func(event *tbtc.ReservationReanchorRequestedEvent), +) subscription.EventSubscription { + onEvent := func( + reservationKey *big.Int, + requestNonce uint64, + sourceWalletPublicKeyHash [20]byte, + targetWalletPublicKeyHash [20]byte, + txMaxFee uint64, + blockNumber uint64, + ) { + handler(&tbtc.ReservationReanchorRequestedEvent{ + ReservationKey: reservationKey, + RequestNonce: requestNonce, + SourceWalletPublicKeyHash: sourceWalletPublicKeyHash, + TargetWalletPublicKeyHash: targetWalletPublicKeyHash, + TxMaxFee: txMaxFee, + BlockNumber: blockNumber, + }) + } + + return tc.reservationRouter.ReservationReanchorRequestedEvent( + nil, + nil, + nil, + nil, + ).OnEvent(onEvent) +} + +// PastReservationReanchorRequestedEvents fetches past +// ReservationReanchorRequested events according to the provided filter or +// unfiltered if the filter is nil. +func (tc *TbtcChain) PastReservationReanchorRequestedEvents( + filter *tbtc.ReservationReanchorRequestedEventFilter, +) ([]*tbtc.ReservationReanchorRequestedEvent, error) { + var startBlock uint64 + var endBlock *uint64 + var reservationKey []*big.Int + var sourceWalletPublicKeyHash [][20]byte + var targetWalletPublicKeyHash [][20]byte + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + reservationKey = filter.ReservationKey + sourceWalletPublicKeyHash = filter.SourceWalletPublicKeyHash + targetWalletPublicKeyHash = filter.TargetWalletPublicKeyHash + } + + events, err := tc.reservationRouter.PastReservationReanchorRequestedEvents( + startBlock, + endBlock, + reservationKey, + sourceWalletPublicKeyHash, + targetWalletPublicKeyHash, + ) + if err != nil { + return nil, err + } + + convertedEvents := make([]*tbtc.ReservationReanchorRequestedEvent, 0) + for _, event := range events { + convertedEvents = append(convertedEvents, &tbtc.ReservationReanchorRequestedEvent{ + ReservationKey: event.ReservationKey, + RequestNonce: event.RequestNonce, + SourceWalletPublicKeyHash: event.SourceWalletPubKeyHash, + TargetWalletPublicKeyHash: event.TargetWalletPubKeyHash, + TxMaxFee: event.TxMaxFee, + BlockNumber: event.Raw.BlockNumber, + }) + } + + sort.SliceStable(convertedEvents, func(i, j int) bool { + return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber + }) + + return convertedEvents, nil +} diff --git a/pkg/chain/ethereum/tbtc/gen/Makefile b/pkg/chain/ethereum/tbtc/gen/Makefile index 2229760f03..1bdbdea328 100644 --- a/pkg/chain/ethereum/tbtc/gen/Makefile +++ b/pkg/chain/ethereum/tbtc/gen/Makefile @@ -1,7 +1,7 @@ npm_package_name=@keep-network/tbtc-v2 # Contracts for which the bindings should be generated. -required_contracts := Bridge MaintainerProxy LightRelay LightRelayMaintainerProxy WalletProposalValidator RedemptionWatchtower +required_contracts := Bridge MaintainerProxy LightRelay LightRelayMaintainerProxy WalletProposalValidator RedemptionWatchtower ReservationRouter # There is a bug in the currently used abigen version (v1.10.19) that makes it # re-declaring structs used by multiple contracts @@ -22,6 +22,7 @@ define after_abi_hook $(eval type := $(1)) $(if $(filter $(type),WalletProposalValidator),$(call fix_wallet_proposal_validator_collision)) $(if $(filter $(type),MaintainerProxy),$(call fix_maintainer_proxy_collision)) + $(if $(filter $(type),ReservationRouter),$(call fix_reservation_router_collision)) endef define fix_wallet_proposal_validator_collision @perl -pi -e s,BitcoinTxInfo,BitcoinTxInfo2,g ./abi/WalletProposalValidator.go @@ -32,12 +33,26 @@ define fix_maintainer_proxy_collision @perl -pi -e s,BitcoinTxProof,BitcoinTxProof2,g ./abi/MaintainerProxy.go @perl -pi -e s,BitcoinTxInfo,BitcoinTxInfo3,g ./abi/MaintainerProxy.go endef +# ReservationRouter introduces its own copies of the BitcoinTx.* structs +# (BitcoinTxInfo, BitcoinTxProof, BitcoinTxUTXO) used by submitReservationProof. +# These names already collide with Bridge's (BitcoinTxInfo/BitcoinTxProof/BitcoinTxUTXO), +# WalletProposalValidator's (BitcoinTxInfo2/BitcoinTxUTXO3), and MaintainerProxy's +# (BitcoinTxInfo3/BitcoinTxProof2/BitcoinTxUTXO2). Renumber ReservationRouter's +# copies to the next free suffix in each family. The router code only ever runs +# via Bridge.fallback delegatecall, so its storage pointer is unused; this is +# a purely textual rename to keep the abi package compiling. +define fix_reservation_router_collision + @perl -pi -e s,BitcoinTxInfo,BitcoinTxInfo4,g ./abi/ReservationRouter.go + @perl -pi -e s,BitcoinTxProof,BitcoinTxProof3,g ./abi/ReservationRouter.go + @perl -pi -e s,BitcoinTxUTXO,BitcoinTxUTXO4,g ./abi/ReservationRouter.go +endef # See explanation in https://github.com/keep-network/keep-common/issues/117. define after_contract_hook $(eval type := $(1)) $(if $(filter $(type),WalletProposalValidator),$(call fix_wallet_proposal_validator_contract_collision)) $(if $(filter $(type),MaintainerProxy),$(call fix_maintainer_proxy_contract_collision)) + $(if $(filter $(type),ReservationRouter),$(call fix_reservation_router_contract_collision)) endef define fix_wallet_proposal_validator_contract_collision @perl -pi -e s,BitcoinTxUTXO,BitcoinTxUTXO3,g ./contract/WalletProposalValidator.go @@ -51,5 +66,16 @@ define fix_maintainer_proxy_contract_collision @perl -pi -e s,BitcoinTxProof,BitcoinTxProof2,g ./cmd/MaintainerProxy.go @perl -pi -e s,BitcoinTxInfo,BitcoinTxInfo3,g ./cmd/MaintainerProxy.go endef +# keep-common's generator emits BitcoinTx* references inside the generated +# contract/cmd binding code paths too (e.g. method wrappers). Apply the same +# renames there so the contract and cmd packages compile. +define fix_reservation_router_contract_collision + @perl -pi -e s,BitcoinTxInfo,BitcoinTxInfo4,g ./contract/ReservationRouter.go + @perl -pi -e s,BitcoinTxProof,BitcoinTxProof3,g ./contract/ReservationRouter.go + @perl -pi -e s,BitcoinTxUTXO,BitcoinTxUTXO4,g ./contract/ReservationRouter.go + @perl -pi -e s,BitcoinTxInfo,BitcoinTxInfo4,g ./cmd/ReservationRouter.go + @perl -pi -e s,BitcoinTxProof,BitcoinTxProof3,g ./cmd/ReservationRouter.go + @perl -pi -e s,BitcoinTxUTXO,BitcoinTxUTXO4,g ./cmd/ReservationRouter.go +endef include ../../common/gen/Makefile diff --git a/pkg/chain/ethereum/tbtc/gen/_address/ReservationRouter b/pkg/chain/ethereum/tbtc/gen/_address/ReservationRouter new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go b/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go index e76e6f779f..80df5a4d72 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go @@ -121,7 +121,7 @@ type WalletsWallet struct { // BridgeMetaData contains all meta data concerning the Bridge contract. var BridgeMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"DepositParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"DepositRevealed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"DepositsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeatTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"FraudParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldGovernance\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"GovernanceTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"}],\"name\":\"MovedFundsSweepTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovedFundsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsBelowDustReported\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"MovingFundsCommitmentSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovingFundsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"MovingFundsParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimeoutReset\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"NewWalletRegistered\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"NewWalletRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"RedemptionParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"}],\"name\":\"RedemptionRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"RedemptionTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"RedemptionWatchtowerSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"redemptionTxHash\",\"type\":\"bytes32\"}],\"name\":\"RedemptionsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"SpvMaintainerStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"TreasuryUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"VaultStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosing\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletMovingFunds\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"WalletParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletTerminated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletCreatedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletHeartbeatFailedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activeWalletPubKeyHash\",\"outputs\":[{\"internalType\":\"bytes20\",\"name\":\"\",\"type\":\"bytes20\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"contractReferences\",\"outputs\":[{\"internalType\":\"contractBank\",\"name\":\"bank\",\"type\":\"address\"},{\"internalType\":\"contractIRelay\",\"name\":\"relay\",\"type\":\"address\"},{\"internalType\":\"contractIWalletRegistry\",\"name\":\"ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"contractReimbursementPool\",\"name\":\"reimbursementPool\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimage\",\"type\":\"bytes\"},{\"internalType\":\"bool\",\"name\":\"witness\",\"type\":\"bool\"}],\"name\":\"defeatFraudChallenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"heartbeatMessage\",\"type\":\"bytes\"}],\"name\":\"defeatFraudChallengeWithHeartbeat\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"depositParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"deposits\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"revealedAt\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"sweptAt\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"internalType\":\"structDeposit.DepositRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"challengeKey\",\"type\":\"uint256\"}],\"name\":\"fraudChallenges\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"depositAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"reportedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"resolved\",\"type\":\"bool\"}],\"internalType\":\"structFraud.FraudChallenge\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fraudParameters\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getRedemptionWatchtower\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governance\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_bank\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_relay\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_treasury\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"addresspayable\",\"name\":\"_reimbursementPool\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"_txProofDifficultyFactor\",\"type\":\"uint96\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"isVaultTrusted\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"liveWalletsCount\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"requestKey\",\"type\":\"uint256\"}],\"name\":\"movedFundsSweepRequests\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"enumMovingFunds.MovedFundsSweepRequestState\",\"name\":\"state\",\"type\":\"uint8\"}],\"internalType\":\"structMovingFunds.MovedFundsSweepRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"movingFundsParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"}],\"name\":\"notifyFraudChallengeDefeatTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovedFundsSweepTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyMovingFundsBelowDust\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionVeto\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyWalletCloseable\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"notifyWalletClosingPeriodElapsed\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"pendingRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"balanceOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"redemptionData\",\"type\":\"bytes\"}],\"name\":\"receiveBalanceApproval\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"redemptionParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"activeWalletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"requestNewWallet\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"}],\"name\":\"requestRedemption\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"resetMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"}],\"name\":\"revealDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"name\":\"revealDepositWithExtraData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"setRedemptionWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setSpvMaintainerStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setVaultStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"utxoKey\",\"type\":\"uint256\"}],\"name\":\"spentMainUTXOs\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"submitDepositSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"}],\"internalType\":\"structBitcoinTx.RSVSignature\",\"name\":\"signature\",\"type\":\"tuple\"}],\"name\":\"submitFraudChallenge\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"submitMovedFundsSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"uint256\",\"name\":\"walletMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"}],\"name\":\"submitMovingFundsCommitment\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"movingFundsTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"movingFundsProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitMovingFundsProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"redemptionTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"redemptionProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitRedemptionProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"timedOutRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"transferGovernance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasury\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"txProofDifficultyFactor\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"updateDepositParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateFraudParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateMovingFundsParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateRedemptionParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"updateTreasury\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"updateWalletParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"walletParameters\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"wallets\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"mainUtxoHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint64\",\"name\":\"pendingRedemptionsValue\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsRequestedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"closingStartedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"pendingMovedFundsSweepRequestsCount\",\"type\":\"uint32\"},{\"internalType\":\"enumWallets.WalletState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTargetWalletsCommitmentHash\",\"type\":\"bytes32\"}],\"internalType\":\"structWallets.Wallet\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"DepositParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"DepositRevealed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newVault\",\"type\":\"address\"}],\"name\":\"DepositVaultFixed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"DepositsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeatTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"FraudParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldGovernance\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"GovernanceTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"}],\"name\":\"MovedFundsSweepTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovedFundsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsBelowDustReported\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"MovingFundsCommitmentSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovingFundsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"MovingFundsParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimeoutReset\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"NewWalletRegistered\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"NewWalletRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldRebateStaking\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newRebateStaking\",\"type\":\"address\"}],\"name\":\"RebateStakingRepaired\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"rebateStaking\",\"type\":\"address\"}],\"name\":\"RebateStakingSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"RedemptionParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"}],\"name\":\"RedemptionRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"RedemptionTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"RedemptionWatchtowerSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"redemptionTxHash\",\"type\":\"bytes32\"}],\"name\":\"RedemptionsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"SpvMaintainerStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"TreasuryUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"VaultStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosing\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletMovingFunds\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"WalletParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletTerminated\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletCreatedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletHeartbeatFailedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activeWalletPubKeyHash\",\"outputs\":[{\"internalType\":\"bytes20\",\"name\":\"\",\"type\":\"bytes20\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"contractReferences\",\"outputs\":[{\"internalType\":\"contractBank\",\"name\":\"bank\",\"type\":\"address\"},{\"internalType\":\"contractIRelay\",\"name\":\"relay\",\"type\":\"address\"},{\"internalType\":\"contractIWalletRegistry\",\"name\":\"ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"contractReimbursementPool\",\"name\":\"reimbursementPool\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimage\",\"type\":\"bytes\"},{\"internalType\":\"bool\",\"name\":\"witness\",\"type\":\"bool\"}],\"name\":\"defeatFraudChallenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"heartbeatMessage\",\"type\":\"bytes\"}],\"name\":\"defeatFraudChallengeWithHeartbeat\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"depositParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"deposits\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"revealedAt\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"sweptAt\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"internalType\":\"structDeposit.DepositRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"challengeKey\",\"type\":\"uint256\"}],\"name\":\"fraudChallenges\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"depositAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"reportedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"resolved\",\"type\":\"bool\"}],\"internalType\":\"structFraud.FraudChallenge\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fraudParameters\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getRebateStaking\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getRedemptionWatchtower\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getReservationRouter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governance\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_bank\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_relay\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_treasury\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"addresspayable\",\"name\":\"_reimbursementPool\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"_txProofDifficultyFactor\",\"type\":\"uint96\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"initializeV2_FixVaultZeroDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newRebateStaking\",\"type\":\"address\"}],\"name\":\"initializeV5_RepairRebateStaking\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"isReservedDeposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"isVaultTrusted\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"liveWalletsCount\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"requestKey\",\"type\":\"uint256\"}],\"name\":\"movedFundsSweepRequests\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"enumMovingFunds.MovedFundsSweepRequestState\",\"name\":\"state\",\"type\":\"uint8\"}],\"internalType\":\"structMovingFunds.MovedFundsSweepRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"movingFundsParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"}],\"name\":\"notifyFraudChallengeDefeatTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovedFundsSweepTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyMovingFundsBelowDust\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionVeto\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyWalletCloseable\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"notifyWalletClosingPeriodElapsed\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"pendingRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"balanceOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"redemptionData\",\"type\":\"bytes\"}],\"name\":\"receiveBalanceApproval\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"redemptionParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"activeWalletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"requestNewWallet\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"}],\"name\":\"requestRedemption\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"resetMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"}],\"name\":\"revealDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"name\":\"revealDepositWithExtraData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"rebateStaking\",\"type\":\"address\"}],\"name\":\"setRebateStaking\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"setRedemptionWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_reservationRouter\",\"type\":\"address\"}],\"name\":\"setReservationRouter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setSpvMaintainerStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setVaultStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"utxoKey\",\"type\":\"uint256\"}],\"name\":\"spentMainUTXOs\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"submitDepositSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"}],\"internalType\":\"structBitcoinTx.RSVSignature\",\"name\":\"signature\",\"type\":\"tuple\"}],\"name\":\"submitFraudChallenge\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"submitMovedFundsSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"uint256\",\"name\":\"walletMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"}],\"name\":\"submitMovingFundsCommitment\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"movingFundsTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"movingFundsProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitMovingFundsProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"redemptionTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"redemptionProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitRedemptionProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"timedOutRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"transferGovernance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasury\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"txProofDifficultyFactor\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"updateDepositParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateFraudParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateMovingFundsParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateRedemptionParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"updateTreasury\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"updateWalletParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"walletParameters\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"wallets\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"mainUtxoHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint64\",\"name\":\"pendingRedemptionsValue\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsRequestedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"closingStartedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"pendingMovedFundsSweepRequestsCount\",\"type\":\"uint32\"},{\"internalType\":\"enumWallets.WalletState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTargetWalletsCommitmentHash\",\"type\":\"bytes32\"}],\"internalType\":\"structWallets.Wallet\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", } // BridgeABI is the input ABI used to generate the binding from. @@ -528,6 +528,37 @@ func (_Bridge *BridgeCallerSession) FraudParameters() (struct { return _Bridge.Contract.FraudParameters(&_Bridge.CallOpts) } +// GetRebateStaking is a free data retrieval call binding the contract method 0x3edf8238. +// +// Solidity: function getRebateStaking() view returns(address) +func (_Bridge *BridgeCaller) GetRebateStaking(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "getRebateStaking") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetRebateStaking is a free data retrieval call binding the contract method 0x3edf8238. +// +// Solidity: function getRebateStaking() view returns(address) +func (_Bridge *BridgeSession) GetRebateStaking() (common.Address, error) { + return _Bridge.Contract.GetRebateStaking(&_Bridge.CallOpts) +} + +// GetRebateStaking is a free data retrieval call binding the contract method 0x3edf8238. +// +// Solidity: function getRebateStaking() view returns(address) +func (_Bridge *BridgeCallerSession) GetRebateStaking() (common.Address, error) { + return _Bridge.Contract.GetRebateStaking(&_Bridge.CallOpts) +} + // GetRedemptionWatchtower is a free data retrieval call binding the contract method 0x5f3281ca. // // Solidity: function getRedemptionWatchtower() view returns(address) @@ -559,6 +590,37 @@ func (_Bridge *BridgeCallerSession) GetRedemptionWatchtower() (common.Address, e return _Bridge.Contract.GetRedemptionWatchtower(&_Bridge.CallOpts) } +// GetReservationRouter is a free data retrieval call binding the contract method 0x5157ec0a. +// +// Solidity: function getReservationRouter() view returns(address) +func (_Bridge *BridgeCaller) GetReservationRouter(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "getReservationRouter") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetReservationRouter is a free data retrieval call binding the contract method 0x5157ec0a. +// +// Solidity: function getReservationRouter() view returns(address) +func (_Bridge *BridgeSession) GetReservationRouter() (common.Address, error) { + return _Bridge.Contract.GetReservationRouter(&_Bridge.CallOpts) +} + +// GetReservationRouter is a free data retrieval call binding the contract method 0x5157ec0a. +// +// Solidity: function getReservationRouter() view returns(address) +func (_Bridge *BridgeCallerSession) GetReservationRouter() (common.Address, error) { + return _Bridge.Contract.GetReservationRouter(&_Bridge.CallOpts) +} + // Governance is a free data retrieval call binding the contract method 0x5aa6e675. // // Solidity: function governance() view returns(address) @@ -590,6 +652,37 @@ func (_Bridge *BridgeCallerSession) Governance() (common.Address, error) { return _Bridge.Contract.Governance(&_Bridge.CallOpts) } +// IsReservedDeposit is a free data retrieval call binding the contract method 0x93df529b. +// +// Solidity: function isReservedDeposit(uint256 depositKey) view returns(bool) +func (_Bridge *BridgeCaller) IsReservedDeposit(opts *bind.CallOpts, depositKey *big.Int) (bool, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "isReservedDeposit", depositKey) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsReservedDeposit is a free data retrieval call binding the contract method 0x93df529b. +// +// Solidity: function isReservedDeposit(uint256 depositKey) view returns(bool) +func (_Bridge *BridgeSession) IsReservedDeposit(depositKey *big.Int) (bool, error) { + return _Bridge.Contract.IsReservedDeposit(&_Bridge.CallOpts, depositKey) +} + +// IsReservedDeposit is a free data retrieval call binding the contract method 0x93df529b. +// +// Solidity: function isReservedDeposit(uint256 depositKey) view returns(bool) +func (_Bridge *BridgeCallerSession) IsReservedDeposit(depositKey *big.Int) (bool, error) { + return _Bridge.Contract.IsReservedDeposit(&_Bridge.CallOpts, depositKey) +} + // IsVaultTrusted is a free data retrieval call binding the contract method 0xe53c0b55. // // Solidity: function isVaultTrusted(address vault) view returns(bool) @@ -1204,6 +1297,48 @@ func (_Bridge *BridgeTransactorSession) Initialize(_bank common.Address, _relay return _Bridge.Contract.Initialize(&_Bridge.TransactOpts, _bank, _relay, _treasury, _ecdsaWalletRegistry, _reimbursementPool, _txProofDifficultyFactor) } +// InitializeV2FixVaultZeroDeposit is a paid mutator transaction binding the contract method 0x456ffee0. +// +// Solidity: function initializeV2_FixVaultZeroDeposit() returns() +func (_Bridge *BridgeTransactor) InitializeV2FixVaultZeroDeposit(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "initializeV2_FixVaultZeroDeposit") +} + +// InitializeV2FixVaultZeroDeposit is a paid mutator transaction binding the contract method 0x456ffee0. +// +// Solidity: function initializeV2_FixVaultZeroDeposit() returns() +func (_Bridge *BridgeSession) InitializeV2FixVaultZeroDeposit() (*types.Transaction, error) { + return _Bridge.Contract.InitializeV2FixVaultZeroDeposit(&_Bridge.TransactOpts) +} + +// InitializeV2FixVaultZeroDeposit is a paid mutator transaction binding the contract method 0x456ffee0. +// +// Solidity: function initializeV2_FixVaultZeroDeposit() returns() +func (_Bridge *BridgeTransactorSession) InitializeV2FixVaultZeroDeposit() (*types.Transaction, error) { + return _Bridge.Contract.InitializeV2FixVaultZeroDeposit(&_Bridge.TransactOpts) +} + +// InitializeV5RepairRebateStaking is a paid mutator transaction binding the contract method 0x1ebf670d. +// +// Solidity: function initializeV5_RepairRebateStaking(address newRebateStaking) returns() +func (_Bridge *BridgeTransactor) InitializeV5RepairRebateStaking(opts *bind.TransactOpts, newRebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "initializeV5_RepairRebateStaking", newRebateStaking) +} + +// InitializeV5RepairRebateStaking is a paid mutator transaction binding the contract method 0x1ebf670d. +// +// Solidity: function initializeV5_RepairRebateStaking(address newRebateStaking) returns() +func (_Bridge *BridgeSession) InitializeV5RepairRebateStaking(newRebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.Contract.InitializeV5RepairRebateStaking(&_Bridge.TransactOpts, newRebateStaking) +} + +// InitializeV5RepairRebateStaking is a paid mutator transaction binding the contract method 0x1ebf670d. +// +// Solidity: function initializeV5_RepairRebateStaking(address newRebateStaking) returns() +func (_Bridge *BridgeTransactorSession) InitializeV5RepairRebateStaking(newRebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.Contract.InitializeV5RepairRebateStaking(&_Bridge.TransactOpts, newRebateStaking) +} + // NotifyFraudChallengeDefeatTimeout is a paid mutator transaction binding the contract method 0x79fc4eb3. // // Solidity: function notifyFraudChallengeDefeatTimeout(bytes walletPublicKey, uint32[] walletMembersIDs, bytes preimageSha256) returns() @@ -1498,6 +1633,27 @@ func (_Bridge *BridgeTransactorSession) RevealDepositWithExtraData(fundingTx Bit return _Bridge.Contract.RevealDepositWithExtraData(&_Bridge.TransactOpts, fundingTx, reveal, extraData) } +// SetRebateStaking is a paid mutator transaction binding the contract method 0xca73c462. +// +// Solidity: function setRebateStaking(address rebateStaking) returns() +func (_Bridge *BridgeTransactor) SetRebateStaking(opts *bind.TransactOpts, rebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setRebateStaking", rebateStaking) +} + +// SetRebateStaking is a paid mutator transaction binding the contract method 0xca73c462. +// +// Solidity: function setRebateStaking(address rebateStaking) returns() +func (_Bridge *BridgeSession) SetRebateStaking(rebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetRebateStaking(&_Bridge.TransactOpts, rebateStaking) +} + +// SetRebateStaking is a paid mutator transaction binding the contract method 0xca73c462. +// +// Solidity: function setRebateStaking(address rebateStaking) returns() +func (_Bridge *BridgeTransactorSession) SetRebateStaking(rebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetRebateStaking(&_Bridge.TransactOpts, rebateStaking) +} + // SetRedemptionWatchtower is a paid mutator transaction binding the contract method 0xbe26ebad. // // Solidity: function setRedemptionWatchtower(address redemptionWatchtower) returns() @@ -1519,6 +1675,27 @@ func (_Bridge *BridgeTransactorSession) SetRedemptionWatchtower(redemptionWatcht return _Bridge.Contract.SetRedemptionWatchtower(&_Bridge.TransactOpts, redemptionWatchtower) } +// SetReservationRouter is a paid mutator transaction binding the contract method 0xd394a4d3. +// +// Solidity: function setReservationRouter(address _reservationRouter) returns() +func (_Bridge *BridgeTransactor) SetReservationRouter(opts *bind.TransactOpts, _reservationRouter common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setReservationRouter", _reservationRouter) +} + +// SetReservationRouter is a paid mutator transaction binding the contract method 0xd394a4d3. +// +// Solidity: function setReservationRouter(address _reservationRouter) returns() +func (_Bridge *BridgeSession) SetReservationRouter(_reservationRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetReservationRouter(&_Bridge.TransactOpts, _reservationRouter) +} + +// SetReservationRouter is a paid mutator transaction binding the contract method 0xd394a4d3. +// +// Solidity: function setReservationRouter(address _reservationRouter) returns() +func (_Bridge *BridgeTransactorSession) SetReservationRouter(_reservationRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetReservationRouter(&_Bridge.TransactOpts, _reservationRouter) +} + // SetSpvMaintainerStatus is a paid mutator transaction binding the contract method 0x5f2b2d0d. // // Solidity: function setSpvMaintainerStatus(address spvMaintainer, bool isTrusted) returns() @@ -1834,6 +2011,27 @@ func (_Bridge *BridgeTransactorSession) UpdateWalletParameters(walletCreationPer return _Bridge.Contract.UpdateWalletParameters(&_Bridge.TransactOpts, walletCreationPeriod, walletCreationMinBtcBalance, walletCreationMaxBtcBalance, walletClosureMinBtcBalance, walletMaxAge, walletMaxBtcTransfer, walletClosingPeriod) } +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_Bridge *BridgeTransactor) Fallback(opts *bind.TransactOpts, calldata []byte) (*types.Transaction, error) { + return _Bridge.contract.RawTransact(opts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_Bridge *BridgeSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _Bridge.Contract.Fallback(&_Bridge.TransactOpts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_Bridge *BridgeTransactorSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _Bridge.Contract.Fallback(&_Bridge.TransactOpts, calldata) +} + // BridgeDepositParametersUpdatedIterator is returned from FilterDepositParametersUpdated and is used to iterate over the raw logs and unpacked data for DepositParametersUpdated events raised by the Bridge contract. type BridgeDepositParametersUpdatedIterator struct { Event *BridgeDepositParametersUpdated // Event containing the contract specifics and raw log @@ -2133,6 +2331,151 @@ func (_Bridge *BridgeFilterer) ParseDepositRevealed(log types.Log) (*BridgeDepos return event, nil } +// BridgeDepositVaultFixedIterator is returned from FilterDepositVaultFixed and is used to iterate over the raw logs and unpacked data for DepositVaultFixed events raised by the Bridge contract. +type BridgeDepositVaultFixedIterator struct { + Event *BridgeDepositVaultFixed // 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 *BridgeDepositVaultFixedIterator) 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(BridgeDepositVaultFixed) + 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(BridgeDepositVaultFixed) + 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 *BridgeDepositVaultFixedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeDepositVaultFixedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeDepositVaultFixed represents a DepositVaultFixed event raised by the Bridge contract. +type BridgeDepositVaultFixed struct { + DepositKey *big.Int + NewVault common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDepositVaultFixed is a free log retrieval operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. +// +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) FilterDepositVaultFixed(opts *bind.FilterOpts, depositKey []*big.Int) (*BridgeDepositVaultFixedIterator, error) { + + var depositKeyRule []interface{} + for _, depositKeyItem := range depositKey { + depositKeyRule = append(depositKeyRule, depositKeyItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "DepositVaultFixed", depositKeyRule) + if err != nil { + return nil, err + } + return &BridgeDepositVaultFixedIterator{contract: _Bridge.contract, event: "DepositVaultFixed", logs: logs, sub: sub}, nil +} + +// WatchDepositVaultFixed is a free log subscription operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. +// +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) WatchDepositVaultFixed(opts *bind.WatchOpts, sink chan<- *BridgeDepositVaultFixed, depositKey []*big.Int) (event.Subscription, error) { + + var depositKeyRule []interface{} + for _, depositKeyItem := range depositKey { + depositKeyRule = append(depositKeyRule, depositKeyItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "DepositVaultFixed", depositKeyRule) + 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(BridgeDepositVaultFixed) + if err := _Bridge.contract.UnpackLog(event, "DepositVaultFixed", 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 +} + +// ParseDepositVaultFixed is a log parse operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. +// +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) ParseDepositVaultFixed(log types.Log) (*BridgeDepositVaultFixed, error) { + event := new(BridgeDepositVaultFixed) + if err := _Bridge.contract.UnpackLog(event, "DepositVaultFixed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeDepositsSweptIterator is returned from FilterDepositsSwept and is used to iterate over the raw logs and unpacked data for DepositsSwept events raised by the Bridge contract. type BridgeDepositsSweptIterator struct { Event *BridgeDepositsSwept // Event containing the contract specifics and raw log @@ -4556,6 +4899,275 @@ func (_Bridge *BridgeFilterer) ParseNewWalletRequested(log types.Log) (*BridgeNe return event, nil } +// BridgeRebateStakingRepairedIterator is returned from FilterRebateStakingRepaired and is used to iterate over the raw logs and unpacked data for RebateStakingRepaired events raised by the Bridge contract. +type BridgeRebateStakingRepairedIterator struct { + Event *BridgeRebateStakingRepaired // 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 *BridgeRebateStakingRepairedIterator) 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(BridgeRebateStakingRepaired) + 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(BridgeRebateStakingRepaired) + 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 *BridgeRebateStakingRepairedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeRebateStakingRepairedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeRebateStakingRepaired represents a RebateStakingRepaired event raised by the Bridge contract. +type BridgeRebateStakingRepaired struct { + OldRebateStaking common.Address + NewRebateStaking common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRebateStakingRepaired is a free log retrieval operation binding the contract event 0x3e9cf92a8b1e7b429a80cef0378c64004ed9e723a896f58a2bb02005bb34d8c5. +// +// Solidity: event RebateStakingRepaired(address oldRebateStaking, address newRebateStaking) +func (_Bridge *BridgeFilterer) FilterRebateStakingRepaired(opts *bind.FilterOpts) (*BridgeRebateStakingRepairedIterator, error) { + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "RebateStakingRepaired") + if err != nil { + return nil, err + } + return &BridgeRebateStakingRepairedIterator{contract: _Bridge.contract, event: "RebateStakingRepaired", logs: logs, sub: sub}, nil +} + +// WatchRebateStakingRepaired is a free log subscription operation binding the contract event 0x3e9cf92a8b1e7b429a80cef0378c64004ed9e723a896f58a2bb02005bb34d8c5. +// +// Solidity: event RebateStakingRepaired(address oldRebateStaking, address newRebateStaking) +func (_Bridge *BridgeFilterer) WatchRebateStakingRepaired(opts *bind.WatchOpts, sink chan<- *BridgeRebateStakingRepaired) (event.Subscription, error) { + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "RebateStakingRepaired") + 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(BridgeRebateStakingRepaired) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingRepaired", 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 +} + +// ParseRebateStakingRepaired is a log parse operation binding the contract event 0x3e9cf92a8b1e7b429a80cef0378c64004ed9e723a896f58a2bb02005bb34d8c5. +// +// Solidity: event RebateStakingRepaired(address oldRebateStaking, address newRebateStaking) +func (_Bridge *BridgeFilterer) ParseRebateStakingRepaired(log types.Log) (*BridgeRebateStakingRepaired, error) { + event := new(BridgeRebateStakingRepaired) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingRepaired", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BridgeRebateStakingSetIterator is returned from FilterRebateStakingSet and is used to iterate over the raw logs and unpacked data for RebateStakingSet events raised by the Bridge contract. +type BridgeRebateStakingSetIterator struct { + Event *BridgeRebateStakingSet // 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 *BridgeRebateStakingSetIterator) 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(BridgeRebateStakingSet) + 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(BridgeRebateStakingSet) + 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 *BridgeRebateStakingSetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeRebateStakingSetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeRebateStakingSet represents a RebateStakingSet event raised by the Bridge contract. +type BridgeRebateStakingSet struct { + RebateStaking common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRebateStakingSet is a free log retrieval operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. +// +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) FilterRebateStakingSet(opts *bind.FilterOpts) (*BridgeRebateStakingSetIterator, error) { + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "RebateStakingSet") + if err != nil { + return nil, err + } + return &BridgeRebateStakingSetIterator{contract: _Bridge.contract, event: "RebateStakingSet", logs: logs, sub: sub}, nil +} + +// WatchRebateStakingSet is a free log subscription operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. +// +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) WatchRebateStakingSet(opts *bind.WatchOpts, sink chan<- *BridgeRebateStakingSet) (event.Subscription, error) { + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "RebateStakingSet") + 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(BridgeRebateStakingSet) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingSet", 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 +} + +// ParseRebateStakingSet is a log parse operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. +// +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) ParseRebateStakingSet(log types.Log) (*BridgeRebateStakingSet, error) { + event := new(BridgeRebateStakingSet) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingSet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeRedemptionParametersUpdatedIterator is returned from FilterRedemptionParametersUpdated and is used to iterate over the raw logs and unpacked data for RedemptionParametersUpdated events raised by the Bridge contract. type BridgeRedemptionParametersUpdatedIterator struct { Event *BridgeRedemptionParametersUpdated // Event containing the contract specifics and raw log diff --git a/pkg/chain/ethereum/tbtc/gen/abi/LightRelay.go b/pkg/chain/ethereum/tbtc/gen/abi/LightRelay.go index 971aae98b0..a0196e92c3 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/LightRelay.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/LightRelay.go @@ -26,6 +26,7 @@ var ( _ = common.Big1 _ = types.BloomLookup _ = event.NewSubscription + _ = abi.ConvertType ) // LightRelayMetaData contains all meta data concerning the LightRelay contract. @@ -134,11 +135,11 @@ func NewLightRelayFilterer(address common.Address, filterer bind.ContractFiltere // bindLightRelay binds a generic wrapper to an already deployed contract. func bindLightRelay(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(LightRelayABI)) + parsed, err := LightRelayMetaData.GetAbi() if err != nil { return nil, err } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil } // Call invokes the (constant) contract method with params as input values and diff --git a/pkg/chain/ethereum/tbtc/gen/abi/LightRelayMaintainerProxy.go b/pkg/chain/ethereum/tbtc/gen/abi/LightRelayMaintainerProxy.go index e90d8b1efa..910dedb510 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/LightRelayMaintainerProxy.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/LightRelayMaintainerProxy.go @@ -26,6 +26,7 @@ var ( _ = common.Big1 _ = types.BloomLookup _ = event.NewSubscription + _ = abi.ConvertType ) // LightRelayMaintainerProxyMetaData contains all meta data concerning the LightRelayMaintainerProxy contract. @@ -134,11 +135,11 @@ func NewLightRelayMaintainerProxyFilterer(address common.Address, filterer bind. // bindLightRelayMaintainerProxy binds a generic wrapper to an already deployed contract. func bindLightRelayMaintainerProxy(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(LightRelayMaintainerProxyABI)) + parsed, err := LightRelayMaintainerProxyMetaData.GetAbi() if err != nil { return nil, err } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil } // Call invokes the (constant) contract method with params as input values and diff --git a/pkg/chain/ethereum/tbtc/gen/abi/MaintainerProxy.go b/pkg/chain/ethereum/tbtc/gen/abi/MaintainerProxy.go index d76fa92d2c..ae37378e5d 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/MaintainerProxy.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/MaintainerProxy.go @@ -26,6 +26,7 @@ var ( _ = common.Big1 _ = types.BloomLookup _ = event.NewSubscription + _ = abi.ConvertType ) // BitcoinTxInfo3 is an auto generated low-level Go binding around an user-defined struct. @@ -158,11 +159,11 @@ func NewMaintainerProxyFilterer(address common.Address, filterer bind.ContractFi // bindMaintainerProxy binds a generic wrapper to an already deployed contract. func bindMaintainerProxy(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(MaintainerProxyABI)) + parsed, err := MaintainerProxyMetaData.GetAbi() if err != nil { return nil, err } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil } // Call invokes the (constant) contract method with params as input values and diff --git a/pkg/chain/ethereum/tbtc/gen/abi/RedemptionWatchtower.go b/pkg/chain/ethereum/tbtc/gen/abi/RedemptionWatchtower.go index 30c4b7ca59..8d48d59321 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/RedemptionWatchtower.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/RedemptionWatchtower.go @@ -31,7 +31,7 @@ var ( // RedemptionWatchtowerMetaData contains all meta data concerning the RedemptionWatchtower contract. var RedemptionWatchtowerMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"Banned\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"GuardianAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"GuardianRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"ObjectionRaised\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"Unbanned\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"VetoFinalized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"VetoPeriodCheckOmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"}],\"name\":\"VetoedFundsWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"disabledAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"executor\",\"type\":\"address\"}],\"name\":\"WatchtowerDisabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"enabledAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"manager\",\"type\":\"address\"}],\"name\":\"WatchtowerEnabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"watchtowerLifetime\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"vetoPenaltyFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"vetoFreezePeriod\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"defaultDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"levelOneDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"levelTwoDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"waivedAmountLimit\",\"type\":\"uint64\"}],\"name\":\"WatchtowerParametersUpdated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"addGuardian\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bank\",\"outputs\":[{\"internalType\":\"contractBank\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bridge\",\"outputs\":[{\"internalType\":\"contractBridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"defaultDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"disableWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_manager\",\"type\":\"address\"},{\"internalType\":\"address[]\",\"name\":\"_guardians\",\"type\":\"address[]\"}],\"name\":\"enableWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"getRedemptionDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractBridge\",\"name\":\"_bridge\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"isBanned\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"isGuardian\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"balanceOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"isSafeRedemption\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"levelOneDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"levelTwoDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"manager\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"objections\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"raiseObjection\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"removeGuardian\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"unban\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_watchtowerLifetime\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"_vetoPenaltyFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"_vetoFreezePeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_defaultDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_levelOneDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_levelTwoDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"_waivedAmountLimit\",\"type\":\"uint64\"}],\"name\":\"updateWatchtowerParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"vetoFreezePeriod\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"vetoPenaltyFeeDivisor\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"vetoProposals\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"withdrawableAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"finalizedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"objectionsCount\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"waivedAmountLimit\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"watchtowerDisabledAt\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"watchtowerEnabledAt\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"watchtowerLifetime\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"withdrawVetoedFunds\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"Banned\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"GuardianAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"GuardianRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"ObjectionRaised\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"Unbanned\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"VetoFinalized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"VetoPeriodCheckOmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"}],\"name\":\"VetoedFundsWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"disabledAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"executor\",\"type\":\"address\"}],\"name\":\"WatchtowerDisabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"enabledAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"manager\",\"type\":\"address\"}],\"name\":\"WatchtowerEnabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"watchtowerLifetime\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"vetoPenaltyFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"vetoFreezePeriod\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"defaultDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"levelOneDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"levelTwoDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"waivedAmountLimit\",\"type\":\"uint64\"}],\"name\":\"WatchtowerParametersUpdated\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"REQUIRED_OBJECTIONS_COUNT\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"addGuardian\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bank\",\"outputs\":[{\"internalType\":\"contractBank\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bridge\",\"outputs\":[{\"internalType\":\"contractBridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"defaultDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"disableWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_manager\",\"type\":\"address\"},{\"internalType\":\"address[]\",\"name\":\"_guardians\",\"type\":\"address[]\"}],\"name\":\"enableWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"getRedemptionDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractBridge\",\"name\":\"_bridge\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"isBanned\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"isGuardian\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"balanceOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"isSafeRedemption\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"levelOneDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"levelTwoDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"manager\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"objections\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"raiseObjection\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"guardian\",\"type\":\"address\"}],\"name\":\"removeGuardian\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"}],\"name\":\"unban\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_watchtowerLifetime\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"_vetoPenaltyFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"_vetoFreezePeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_defaultDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_levelOneDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_levelTwoDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"_waivedAmountLimit\",\"type\":\"uint64\"}],\"name\":\"updateWatchtowerParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"vetoFreezePeriod\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"vetoPenaltyFeeDivisor\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"vetoProposals\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"withdrawableAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"finalizedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"objectionsCount\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"waivedAmountLimit\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"watchtowerDisabledAt\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"watchtowerEnabledAt\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"watchtowerLifetime\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"withdrawVetoedFunds\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // RedemptionWatchtowerABI is the input ABI used to generate the binding from. @@ -180,6 +180,37 @@ func (_RedemptionWatchtower *RedemptionWatchtowerTransactorRaw) Transact(opts *b return _RedemptionWatchtower.Contract.contract.Transact(opts, method, params...) } +// REQUIREDOBJECTIONSCOUNT is a free data retrieval call binding the contract method 0x7a497647. +// +// Solidity: function REQUIRED_OBJECTIONS_COUNT() view returns(uint8) +func (_RedemptionWatchtower *RedemptionWatchtowerCaller) REQUIREDOBJECTIONSCOUNT(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _RedemptionWatchtower.contract.Call(opts, &out, "REQUIRED_OBJECTIONS_COUNT") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// REQUIREDOBJECTIONSCOUNT is a free data retrieval call binding the contract method 0x7a497647. +// +// Solidity: function REQUIRED_OBJECTIONS_COUNT() view returns(uint8) +func (_RedemptionWatchtower *RedemptionWatchtowerSession) REQUIREDOBJECTIONSCOUNT() (uint8, error) { + return _RedemptionWatchtower.Contract.REQUIREDOBJECTIONSCOUNT(&_RedemptionWatchtower.CallOpts) +} + +// REQUIREDOBJECTIONSCOUNT is a free data retrieval call binding the contract method 0x7a497647. +// +// Solidity: function REQUIRED_OBJECTIONS_COUNT() view returns(uint8) +func (_RedemptionWatchtower *RedemptionWatchtowerCallerSession) REQUIREDOBJECTIONSCOUNT() (uint8, error) { + return _RedemptionWatchtower.Contract.REQUIREDOBJECTIONSCOUNT(&_RedemptionWatchtower.CallOpts) +} + // Bank is a free data retrieval call binding the contract method 0x76cdb03b. // // Solidity: function bank() view returns(address) diff --git a/pkg/chain/ethereum/tbtc/gen/abi/ReservationRouter.go b/pkg/chain/ethereum/tbtc/gen/abi/ReservationRouter.go new file mode 100644 index 0000000000..8f4ac35f2e --- /dev/null +++ b/pkg/chain/ethereum/tbtc/gen/abi/ReservationRouter.go @@ -0,0 +1,3270 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package abi + +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 +) + +// BitcoinTxInfo4 is an auto generated low-level Go binding around an user-defined struct. +type BitcoinTxInfo4 struct { + Version [4]byte + InputVector []byte + OutputVector []byte + Locktime [4]byte +} + +// BitcoinTxProof3 is an auto generated low-level Go binding around an user-defined struct. +type BitcoinTxProof3 struct { + MerkleProof []byte + TxIndexInBlock *big.Int + BitcoinHeaders []byte + CoinbasePreimage [32]byte + CoinbaseProof []byte +} + +// BitcoinTxUTXO4 is an auto generated low-level Go binding around an user-defined struct. +type BitcoinTxUTXO4 struct { + TxHash [32]byte + TxOutputIndex uint32 + TxOutputValue uint64 +} + +// ReservationReservationAction is an auto generated low-level Go binding around an user-defined struct. +type ReservationReservationAction struct { + TargetWalletPubKeyHash [20]byte + RequestedAt uint32 + TimeoutAt uint32 + TxMaxFee uint64 + ActionType uint8 + State uint8 + FeePaid bool + Redeemer common.Address + Amount uint64 + ActionDataHash [32]byte + SourceAnchorUtxoHash [32]byte + UsedRetryCredit bool + WatchtowerDefaultDelay uint32 + WatchtowerLevelOneDelay uint32 + WatchtowerLevelTwoDelay uint32 + IsPartial bool + RetryCreditSourceNonce uint64 +} + +// ReservationReservationRequest is an auto generated low-level Go binding around an user-defined struct. +type ReservationReservationRequest struct { + Owner common.Address + MintedAmount uint64 + AcceptedAt uint32 + WalletPubKeyHash [20]byte + AnchorAmount uint64 + ExpiresAt uint32 + AnchorTxHash [32]byte + AnchorTxOutputIndex uint32 + State uint8 + RequestNonce uint64 + RetryCredit bool + DissolutionEligibleAt uint32 + CumulativeReanchorFee uint64 +} + +// ReservationRouterMetaData contains all meta data concerning the ReservationRouter contract. +var ReservationRouterMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldGovernance\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"GovernanceTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"timeoutAt\",\"type\":\"uint32\"}],\"name\":\"ReservationAcceptanceRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"anchorTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"anchorAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"expiresAt\",\"type\":\"uint32\"}],\"name\":\"ReservationAccepted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"}],\"name\":\"ReservationActionSuperseded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"enumReservation.ActionType\",\"name\":\"actionType\",\"type\":\"uint8\"}],\"name\":\"ReservationActionTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"maxReservationsAmountPerWallet\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"reservationMaxSingleAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"maxActiveReservations\",\"type\":\"uint32\"}],\"name\":\"ReservationCapsUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"enumReservation.ActionType\",\"name\":\"actionType\",\"type\":\"uint8\"}],\"name\":\"ReservationLateSettled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"reservationMinAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"reservationTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"reservationTermSeconds\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"reservationDissolutionDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"reservationMaxTotalAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"maxReservationsPerWallet\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"reservationActionTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"reservationRenewalWindowSeconds\",\"type\":\"uint32\"}],\"name\":\"ReservationParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"sourceWalletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"targetWalletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"}],\"name\":\"ReservationReanchorRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"newWalletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"newAnchorTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"newAnchorAmount\",\"type\":\"uint64\"}],\"name\":\"ReservationReanchored\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"}],\"name\":\"ReservationRetryCreditMinted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"reservationRouter\",\"type\":\"address\"}],\"name\":\"ReservationRouterSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"anchorAmount\",\"type\":\"uint64\"}],\"name\":\"ReservationStranded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"reservationVault\",\"type\":\"address\"}],\"name\":\"ReservationVaultUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"ReservedDepositMarkedStale\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"activeReservationsCount\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"count\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"maxActive\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governance\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyReservationActionTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"}],\"name\":\"notifyReservationStranded\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"notifyStaleReservedDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pendingReservedDeposits\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"requestReservationAcceptance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"internalType\":\"bytes20\",\"name\":\"targetWalletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"requestReservationReanchor\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"}],\"name\":\"reservationActions\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"targetWalletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"timeoutAt\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"enumReservation.ActionType\",\"name\":\"actionType\",\"type\":\"uint8\"},{\"internalType\":\"enumReservation.ActionState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bool\",\"name\":\"feePaid\",\"type\":\"bool\"},{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"internalType\":\"bytes32\",\"name\":\"actionDataHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"sourceAnchorUtxoHash\",\"type\":\"bytes32\"},{\"internalType\":\"bool\",\"name\":\"usedRetryCredit\",\"type\":\"bool\"},{\"internalType\":\"uint32\",\"name\":\"watchtowerDefaultDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"watchtowerLevelOneDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"watchtowerLevelTwoDelay\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"isPartial\",\"type\":\"bool\"},{\"internalType\":\"uint64\",\"name\":\"retryCreditSourceNonce\",\"type\":\"uint64\"}],\"internalType\":\"structReservation.ReservationAction\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"anchorTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"anchorTxOutputIndex\",\"type\":\"uint32\"}],\"name\":\"reservationByAnchorUtxo\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reservationCaps\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"maxReservationsAmountPerWallet\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"reservationMaxSingleAmount\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reservationParameters\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"reservationVault\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"reservationMinAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"reservationTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"reservationTermSeconds\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"reservationDissolutionDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"reservationMaxTotalAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"reservationTotalAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"maxReservationsPerWallet\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"reservationActionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"reservationRenewalWindowSeconds\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reservationRouter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"}],\"name\":\"reservations\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"mintedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"acceptedAt\",\"type\":\"uint32\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint64\",\"name\":\"anchorAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"expiresAt\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"anchorTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"anchorTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"enumReservation.ReservationState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"},{\"internalType\":\"bool\",\"name\":\"retryCredit\",\"type\":\"bool\"},{\"internalType\":\"uint32\",\"name\":\"dissolutionEligibleAt\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"cumulativeReanchorFee\",\"type\":\"uint64\"}],\"internalType\":\"structReservation.ReservationRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"reservedDepositWallet\",\"outputs\":[{\"internalType\":\"bytes20\",\"name\":\"\",\"type\":\"bytes20\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"proofType\",\"type\":\"uint8\"},{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"txInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"proof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"internalType\":\"uint64\",\"name\":\"requestNonce\",\"type\":\"uint64\"}],\"name\":\"submitReservationProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"transferGovernance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"maxReservationsAmountPerWallet\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"reservationMaxSingleAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"maxActiveReservations\",\"type\":\"uint32\"}],\"name\":\"updateReservationCaps\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"reservationVault\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"reservationMinAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"reservationTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"reservationTermSeconds\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"reservationDissolutionDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"reservationMaxTotalAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"maxReservationsPerWallet\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"reservationActionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"reservationRenewalWindowSeconds\",\"type\":\"uint32\"}],\"name\":\"updateReservationParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"walletReservations\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"walletReservationsAmount\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"walletReservationsCount\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// ReservationRouterABI is the input ABI used to generate the binding from. +// Deprecated: Use ReservationRouterMetaData.ABI instead. +var ReservationRouterABI = ReservationRouterMetaData.ABI + +// ReservationRouter is an auto generated Go binding around an Ethereum contract. +type ReservationRouter struct { + ReservationRouterCaller // Read-only binding to the contract + ReservationRouterTransactor // Write-only binding to the contract + ReservationRouterFilterer // Log filterer for contract events +} + +// ReservationRouterCaller is an auto generated read-only Go binding around an Ethereum contract. +type ReservationRouterCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReservationRouterTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ReservationRouterTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReservationRouterFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ReservationRouterFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReservationRouterSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ReservationRouterSession struct { + Contract *ReservationRouter // 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 +} + +// ReservationRouterCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ReservationRouterCallerSession struct { + Contract *ReservationRouterCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ReservationRouterTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ReservationRouterTransactorSession struct { + Contract *ReservationRouterTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ReservationRouterRaw is an auto generated low-level Go binding around an Ethereum contract. +type ReservationRouterRaw struct { + Contract *ReservationRouter // Generic contract binding to access the raw methods on +} + +// ReservationRouterCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ReservationRouterCallerRaw struct { + Contract *ReservationRouterCaller // Generic read-only contract binding to access the raw methods on +} + +// ReservationRouterTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ReservationRouterTransactorRaw struct { + Contract *ReservationRouterTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewReservationRouter creates a new instance of ReservationRouter, bound to a specific deployed contract. +func NewReservationRouter(address common.Address, backend bind.ContractBackend) (*ReservationRouter, error) { + contract, err := bindReservationRouter(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &ReservationRouter{ReservationRouterCaller: ReservationRouterCaller{contract: contract}, ReservationRouterTransactor: ReservationRouterTransactor{contract: contract}, ReservationRouterFilterer: ReservationRouterFilterer{contract: contract}}, nil +} + +// NewReservationRouterCaller creates a new read-only instance of ReservationRouter, bound to a specific deployed contract. +func NewReservationRouterCaller(address common.Address, caller bind.ContractCaller) (*ReservationRouterCaller, error) { + contract, err := bindReservationRouter(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ReservationRouterCaller{contract: contract}, nil +} + +// NewReservationRouterTransactor creates a new write-only instance of ReservationRouter, bound to a specific deployed contract. +func NewReservationRouterTransactor(address common.Address, transactor bind.ContractTransactor) (*ReservationRouterTransactor, error) { + contract, err := bindReservationRouter(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ReservationRouterTransactor{contract: contract}, nil +} + +// NewReservationRouterFilterer creates a new log filterer instance of ReservationRouter, bound to a specific deployed contract. +func NewReservationRouterFilterer(address common.Address, filterer bind.ContractFilterer) (*ReservationRouterFilterer, error) { + contract, err := bindReservationRouter(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ReservationRouterFilterer{contract: contract}, nil +} + +// bindReservationRouter binds a generic wrapper to an already deployed contract. +func bindReservationRouter(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ReservationRouterMetaData.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 (_ReservationRouter *ReservationRouterRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ReservationRouter.Contract.ReservationRouterCaller.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 (_ReservationRouter *ReservationRouterRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ReservationRouter.Contract.ReservationRouterTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ReservationRouter *ReservationRouterRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ReservationRouter.Contract.ReservationRouterTransactor.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 (_ReservationRouter *ReservationRouterCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ReservationRouter.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 (_ReservationRouter *ReservationRouterTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ReservationRouter.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ReservationRouter *ReservationRouterTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ReservationRouter.Contract.contract.Transact(opts, method, params...) +} + +// ActiveReservationsCount is a free data retrieval call binding the contract method 0x93fe5eab. +// +// Solidity: function activeReservationsCount() view returns(uint32 count, uint32 maxActive) +func (_ReservationRouter *ReservationRouterCaller) ActiveReservationsCount(opts *bind.CallOpts) (struct { + Count uint32 + MaxActive uint32 +}, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "activeReservationsCount") + + outstruct := new(struct { + Count uint32 + MaxActive uint32 + }) + if err != nil { + return *outstruct, err + } + + outstruct.Count = *abi.ConvertType(out[0], new(uint32)).(*uint32) + outstruct.MaxActive = *abi.ConvertType(out[1], new(uint32)).(*uint32) + + return *outstruct, err + +} + +// ActiveReservationsCount is a free data retrieval call binding the contract method 0x93fe5eab. +// +// Solidity: function activeReservationsCount() view returns(uint32 count, uint32 maxActive) +func (_ReservationRouter *ReservationRouterSession) ActiveReservationsCount() (struct { + Count uint32 + MaxActive uint32 +}, error) { + return _ReservationRouter.Contract.ActiveReservationsCount(&_ReservationRouter.CallOpts) +} + +// ActiveReservationsCount is a free data retrieval call binding the contract method 0x93fe5eab. +// +// Solidity: function activeReservationsCount() view returns(uint32 count, uint32 maxActive) +func (_ReservationRouter *ReservationRouterCallerSession) ActiveReservationsCount() (struct { + Count uint32 + MaxActive uint32 +}, error) { + return _ReservationRouter.Contract.ActiveReservationsCount(&_ReservationRouter.CallOpts) +} + +// Governance is a free data retrieval call binding the contract method 0x5aa6e675. +// +// Solidity: function governance() view returns(address) +func (_ReservationRouter *ReservationRouterCaller) Governance(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "governance") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Governance is a free data retrieval call binding the contract method 0x5aa6e675. +// +// Solidity: function governance() view returns(address) +func (_ReservationRouter *ReservationRouterSession) Governance() (common.Address, error) { + return _ReservationRouter.Contract.Governance(&_ReservationRouter.CallOpts) +} + +// Governance is a free data retrieval call binding the contract method 0x5aa6e675. +// +// Solidity: function governance() view returns(address) +func (_ReservationRouter *ReservationRouterCallerSession) Governance() (common.Address, error) { + return _ReservationRouter.Contract.Governance(&_ReservationRouter.CallOpts) +} + +// PendingReservedDeposits is a free data retrieval call binding the contract method 0x34830fc8. +// +// Solidity: function pendingReservedDeposits() view returns(uint64) +func (_ReservationRouter *ReservationRouterCaller) PendingReservedDeposits(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "pendingReservedDeposits") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// PendingReservedDeposits is a free data retrieval call binding the contract method 0x34830fc8. +// +// Solidity: function pendingReservedDeposits() view returns(uint64) +func (_ReservationRouter *ReservationRouterSession) PendingReservedDeposits() (uint64, error) { + return _ReservationRouter.Contract.PendingReservedDeposits(&_ReservationRouter.CallOpts) +} + +// PendingReservedDeposits is a free data retrieval call binding the contract method 0x34830fc8. +// +// Solidity: function pendingReservedDeposits() view returns(uint64) +func (_ReservationRouter *ReservationRouterCallerSession) PendingReservedDeposits() (uint64, error) { + return _ReservationRouter.Contract.PendingReservedDeposits(&_ReservationRouter.CallOpts) +} + +// ReservationActions is a free data retrieval call binding the contract method 0xcec8c6e9. +// +// Solidity: function reservationActions(uint256 reservationKey, uint64 requestNonce) view returns((bytes20,uint32,uint32,uint64,uint8,uint8,bool,address,uint64,bytes32,bytes32,bool,uint32,uint32,uint32,bool,uint64)) +func (_ReservationRouter *ReservationRouterCaller) ReservationActions(opts *bind.CallOpts, reservationKey *big.Int, requestNonce uint64) (ReservationReservationAction, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservationActions", reservationKey, requestNonce) + + if err != nil { + return *new(ReservationReservationAction), err + } + + out0 := *abi.ConvertType(out[0], new(ReservationReservationAction)).(*ReservationReservationAction) + + return out0, err + +} + +// ReservationActions is a free data retrieval call binding the contract method 0xcec8c6e9. +// +// Solidity: function reservationActions(uint256 reservationKey, uint64 requestNonce) view returns((bytes20,uint32,uint32,uint64,uint8,uint8,bool,address,uint64,bytes32,bytes32,bool,uint32,uint32,uint32,bool,uint64)) +func (_ReservationRouter *ReservationRouterSession) ReservationActions(reservationKey *big.Int, requestNonce uint64) (ReservationReservationAction, error) { + return _ReservationRouter.Contract.ReservationActions(&_ReservationRouter.CallOpts, reservationKey, requestNonce) +} + +// ReservationActions is a free data retrieval call binding the contract method 0xcec8c6e9. +// +// Solidity: function reservationActions(uint256 reservationKey, uint64 requestNonce) view returns((bytes20,uint32,uint32,uint64,uint8,uint8,bool,address,uint64,bytes32,bytes32,bool,uint32,uint32,uint32,bool,uint64)) +func (_ReservationRouter *ReservationRouterCallerSession) ReservationActions(reservationKey *big.Int, requestNonce uint64) (ReservationReservationAction, error) { + return _ReservationRouter.Contract.ReservationActions(&_ReservationRouter.CallOpts, reservationKey, requestNonce) +} + +// ReservationByAnchorUtxo is a free data retrieval call binding the contract method 0x79731f67. +// +// Solidity: function reservationByAnchorUtxo(bytes32 anchorTxHash, uint32 anchorTxOutputIndex) view returns(uint256) +func (_ReservationRouter *ReservationRouterCaller) ReservationByAnchorUtxo(opts *bind.CallOpts, anchorTxHash [32]byte, anchorTxOutputIndex uint32) (*big.Int, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservationByAnchorUtxo", anchorTxHash, anchorTxOutputIndex) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// ReservationByAnchorUtxo is a free data retrieval call binding the contract method 0x79731f67. +// +// Solidity: function reservationByAnchorUtxo(bytes32 anchorTxHash, uint32 anchorTxOutputIndex) view returns(uint256) +func (_ReservationRouter *ReservationRouterSession) ReservationByAnchorUtxo(anchorTxHash [32]byte, anchorTxOutputIndex uint32) (*big.Int, error) { + return _ReservationRouter.Contract.ReservationByAnchorUtxo(&_ReservationRouter.CallOpts, anchorTxHash, anchorTxOutputIndex) +} + +// ReservationByAnchorUtxo is a free data retrieval call binding the contract method 0x79731f67. +// +// Solidity: function reservationByAnchorUtxo(bytes32 anchorTxHash, uint32 anchorTxOutputIndex) view returns(uint256) +func (_ReservationRouter *ReservationRouterCallerSession) ReservationByAnchorUtxo(anchorTxHash [32]byte, anchorTxOutputIndex uint32) (*big.Int, error) { + return _ReservationRouter.Contract.ReservationByAnchorUtxo(&_ReservationRouter.CallOpts, anchorTxHash, anchorTxOutputIndex) +} + +// ReservationCaps is a free data retrieval call binding the contract method 0x63dfb29c. +// +// Solidity: function reservationCaps() view returns(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount) +func (_ReservationRouter *ReservationRouterCaller) ReservationCaps(opts *bind.CallOpts) (struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 +}, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservationCaps") + + outstruct := new(struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 + }) + if err != nil { + return *outstruct, err + } + + outstruct.MaxReservationsAmountPerWallet = *abi.ConvertType(out[0], new(uint64)).(*uint64) + outstruct.ReservationMaxSingleAmount = *abi.ConvertType(out[1], new(uint64)).(*uint64) + + return *outstruct, err + +} + +// ReservationCaps is a free data retrieval call binding the contract method 0x63dfb29c. +// +// Solidity: function reservationCaps() view returns(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount) +func (_ReservationRouter *ReservationRouterSession) ReservationCaps() (struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 +}, error) { + return _ReservationRouter.Contract.ReservationCaps(&_ReservationRouter.CallOpts) +} + +// ReservationCaps is a free data retrieval call binding the contract method 0x63dfb29c. +// +// Solidity: function reservationCaps() view returns(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount) +func (_ReservationRouter *ReservationRouterCallerSession) ReservationCaps() (struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 +}, error) { + return _ReservationRouter.Contract.ReservationCaps(&_ReservationRouter.CallOpts) +} + +// ReservationParameters is a free data retrieval call binding the contract method 0xf75b4b1c. +// +// Solidity: function reservationParameters() view returns(address reservationVault, uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint64 reservationTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) +func (_ReservationRouter *ReservationRouterCaller) ReservationParameters(opts *bind.CallOpts) (struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 +}, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservationParameters") + + outstruct := new(struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 + }) + if err != nil { + return *outstruct, err + } + + outstruct.ReservationVault = *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + outstruct.ReservationMinAmount = *abi.ConvertType(out[1], new(uint64)).(*uint64) + outstruct.ReservationTxMaxFee = *abi.ConvertType(out[2], new(uint64)).(*uint64) + outstruct.ReservationTermSeconds = *abi.ConvertType(out[3], new(uint32)).(*uint32) + outstruct.ReservationDissolutionDelay = *abi.ConvertType(out[4], new(uint32)).(*uint32) + outstruct.ReservationMaxTotalAmount = *abi.ConvertType(out[5], new(uint64)).(*uint64) + outstruct.ReservationTotalAmount = *abi.ConvertType(out[6], new(uint64)).(*uint64) + outstruct.MaxReservationsPerWallet = *abi.ConvertType(out[7], new(uint32)).(*uint32) + outstruct.ReservationActionTimeout = *abi.ConvertType(out[8], new(uint32)).(*uint32) + outstruct.ReservationRenewalWindowSeconds = *abi.ConvertType(out[9], new(uint32)).(*uint32) + + return *outstruct, err + +} + +// ReservationParameters is a free data retrieval call binding the contract method 0xf75b4b1c. +// +// Solidity: function reservationParameters() view returns(address reservationVault, uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint64 reservationTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) +func (_ReservationRouter *ReservationRouterSession) ReservationParameters() (struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 +}, error) { + return _ReservationRouter.Contract.ReservationParameters(&_ReservationRouter.CallOpts) +} + +// ReservationParameters is a free data retrieval call binding the contract method 0xf75b4b1c. +// +// Solidity: function reservationParameters() view returns(address reservationVault, uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint64 reservationTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) +func (_ReservationRouter *ReservationRouterCallerSession) ReservationParameters() (struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 +}, error) { + return _ReservationRouter.Contract.ReservationParameters(&_ReservationRouter.CallOpts) +} + +// ReservationRouter is a free data retrieval call binding the contract method 0x06ca90d2. +// +// Solidity: function reservationRouter() view returns(address) +func (_ReservationRouter *ReservationRouterCaller) ReservationRouter(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservationRouter") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ReservationRouter is a free data retrieval call binding the contract method 0x06ca90d2. +// +// Solidity: function reservationRouter() view returns(address) +func (_ReservationRouter *ReservationRouterSession) ReservationRouter() (common.Address, error) { + return _ReservationRouter.Contract.ReservationRouter(&_ReservationRouter.CallOpts) +} + +// ReservationRouter is a free data retrieval call binding the contract method 0x06ca90d2. +// +// Solidity: function reservationRouter() view returns(address) +func (_ReservationRouter *ReservationRouterCallerSession) ReservationRouter() (common.Address, error) { + return _ReservationRouter.Contract.ReservationRouter(&_ReservationRouter.CallOpts) +} + +// Reservations is a free data retrieval call binding the contract method 0x067cf832. +// +// Solidity: function reservations(uint256 reservationKey) view returns((address,uint64,uint32,bytes20,uint64,uint32,bytes32,uint32,uint8,uint64,bool,uint32,uint64)) +func (_ReservationRouter *ReservationRouterCaller) Reservations(opts *bind.CallOpts, reservationKey *big.Int) (ReservationReservationRequest, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservations", reservationKey) + + if err != nil { + return *new(ReservationReservationRequest), err + } + + out0 := *abi.ConvertType(out[0], new(ReservationReservationRequest)).(*ReservationReservationRequest) + + return out0, err + +} + +// Reservations is a free data retrieval call binding the contract method 0x067cf832. +// +// Solidity: function reservations(uint256 reservationKey) view returns((address,uint64,uint32,bytes20,uint64,uint32,bytes32,uint32,uint8,uint64,bool,uint32,uint64)) +func (_ReservationRouter *ReservationRouterSession) Reservations(reservationKey *big.Int) (ReservationReservationRequest, error) { + return _ReservationRouter.Contract.Reservations(&_ReservationRouter.CallOpts, reservationKey) +} + +// Reservations is a free data retrieval call binding the contract method 0x067cf832. +// +// Solidity: function reservations(uint256 reservationKey) view returns((address,uint64,uint32,bytes20,uint64,uint32,bytes32,uint32,uint8,uint64,bool,uint32,uint64)) +func (_ReservationRouter *ReservationRouterCallerSession) Reservations(reservationKey *big.Int) (ReservationReservationRequest, error) { + return _ReservationRouter.Contract.Reservations(&_ReservationRouter.CallOpts, reservationKey) +} + +// ReservedDepositWallet is a free data retrieval call binding the contract method 0x56803b55. +// +// Solidity: function reservedDepositWallet(uint256 depositKey) view returns(bytes20) +func (_ReservationRouter *ReservationRouterCaller) ReservedDepositWallet(opts *bind.CallOpts, depositKey *big.Int) ([20]byte, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "reservedDepositWallet", depositKey) + + if err != nil { + return *new([20]byte), err + } + + out0 := *abi.ConvertType(out[0], new([20]byte)).(*[20]byte) + + return out0, err + +} + +// ReservedDepositWallet is a free data retrieval call binding the contract method 0x56803b55. +// +// Solidity: function reservedDepositWallet(uint256 depositKey) view returns(bytes20) +func (_ReservationRouter *ReservationRouterSession) ReservedDepositWallet(depositKey *big.Int) ([20]byte, error) { + return _ReservationRouter.Contract.ReservedDepositWallet(&_ReservationRouter.CallOpts, depositKey) +} + +// ReservedDepositWallet is a free data retrieval call binding the contract method 0x56803b55. +// +// Solidity: function reservedDepositWallet(uint256 depositKey) view returns(bytes20) +func (_ReservationRouter *ReservationRouterCallerSession) ReservedDepositWallet(depositKey *big.Int) ([20]byte, error) { + return _ReservationRouter.Contract.ReservedDepositWallet(&_ReservationRouter.CallOpts, depositKey) +} + +// WalletReservations is a free data retrieval call binding the contract method 0x78699d2f. +// +// Solidity: function walletReservations(bytes20 walletPubKeyHash) view returns(uint256[]) +func (_ReservationRouter *ReservationRouterCaller) WalletReservations(opts *bind.CallOpts, walletPubKeyHash [20]byte) ([]*big.Int, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "walletReservations", walletPubKeyHash) + + if err != nil { + return *new([]*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new([]*big.Int)).(*[]*big.Int) + + return out0, err + +} + +// WalletReservations is a free data retrieval call binding the contract method 0x78699d2f. +// +// Solidity: function walletReservations(bytes20 walletPubKeyHash) view returns(uint256[]) +func (_ReservationRouter *ReservationRouterSession) WalletReservations(walletPubKeyHash [20]byte) ([]*big.Int, error) { + return _ReservationRouter.Contract.WalletReservations(&_ReservationRouter.CallOpts, walletPubKeyHash) +} + +// WalletReservations is a free data retrieval call binding the contract method 0x78699d2f. +// +// Solidity: function walletReservations(bytes20 walletPubKeyHash) view returns(uint256[]) +func (_ReservationRouter *ReservationRouterCallerSession) WalletReservations(walletPubKeyHash [20]byte) ([]*big.Int, error) { + return _ReservationRouter.Contract.WalletReservations(&_ReservationRouter.CallOpts, walletPubKeyHash) +} + +// WalletReservationsAmount is a free data retrieval call binding the contract method 0x63481e98. +// +// Solidity: function walletReservationsAmount(bytes20 walletPubKeyHash) view returns(uint64) +func (_ReservationRouter *ReservationRouterCaller) WalletReservationsAmount(opts *bind.CallOpts, walletPubKeyHash [20]byte) (uint64, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "walletReservationsAmount", walletPubKeyHash) + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// WalletReservationsAmount is a free data retrieval call binding the contract method 0x63481e98. +// +// Solidity: function walletReservationsAmount(bytes20 walletPubKeyHash) view returns(uint64) +func (_ReservationRouter *ReservationRouterSession) WalletReservationsAmount(walletPubKeyHash [20]byte) (uint64, error) { + return _ReservationRouter.Contract.WalletReservationsAmount(&_ReservationRouter.CallOpts, walletPubKeyHash) +} + +// WalletReservationsAmount is a free data retrieval call binding the contract method 0x63481e98. +// +// Solidity: function walletReservationsAmount(bytes20 walletPubKeyHash) view returns(uint64) +func (_ReservationRouter *ReservationRouterCallerSession) WalletReservationsAmount(walletPubKeyHash [20]byte) (uint64, error) { + return _ReservationRouter.Contract.WalletReservationsAmount(&_ReservationRouter.CallOpts, walletPubKeyHash) +} + +// WalletReservationsCount is a free data retrieval call binding the contract method 0x1de0555a. +// +// Solidity: function walletReservationsCount(bytes20 walletPubKeyHash) view returns(uint32) +func (_ReservationRouter *ReservationRouterCaller) WalletReservationsCount(opts *bind.CallOpts, walletPubKeyHash [20]byte) (uint32, error) { + var out []interface{} + err := _ReservationRouter.contract.Call(opts, &out, "walletReservationsCount", walletPubKeyHash) + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// WalletReservationsCount is a free data retrieval call binding the contract method 0x1de0555a. +// +// Solidity: function walletReservationsCount(bytes20 walletPubKeyHash) view returns(uint32) +func (_ReservationRouter *ReservationRouterSession) WalletReservationsCount(walletPubKeyHash [20]byte) (uint32, error) { + return _ReservationRouter.Contract.WalletReservationsCount(&_ReservationRouter.CallOpts, walletPubKeyHash) +} + +// WalletReservationsCount is a free data retrieval call binding the contract method 0x1de0555a. +// +// Solidity: function walletReservationsCount(bytes20 walletPubKeyHash) view returns(uint32) +func (_ReservationRouter *ReservationRouterCallerSession) WalletReservationsCount(walletPubKeyHash [20]byte) (uint32, error) { + return _ReservationRouter.Contract.WalletReservationsCount(&_ReservationRouter.CallOpts, walletPubKeyHash) +} + +// NotifyReservationActionTimeout is a paid mutator transaction binding the contract method 0x88aa5729. +// +// Solidity: function notifyReservationActionTimeout(uint256 reservationKey, uint32[] walletMembersIDs) returns() +func (_ReservationRouter *ReservationRouterTransactor) NotifyReservationActionTimeout(opts *bind.TransactOpts, reservationKey *big.Int, walletMembersIDs []uint32) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "notifyReservationActionTimeout", reservationKey, walletMembersIDs) +} + +// NotifyReservationActionTimeout is a paid mutator transaction binding the contract method 0x88aa5729. +// +// Solidity: function notifyReservationActionTimeout(uint256 reservationKey, uint32[] walletMembersIDs) returns() +func (_ReservationRouter *ReservationRouterSession) NotifyReservationActionTimeout(reservationKey *big.Int, walletMembersIDs []uint32) (*types.Transaction, error) { + return _ReservationRouter.Contract.NotifyReservationActionTimeout(&_ReservationRouter.TransactOpts, reservationKey, walletMembersIDs) +} + +// NotifyReservationActionTimeout is a paid mutator transaction binding the contract method 0x88aa5729. +// +// Solidity: function notifyReservationActionTimeout(uint256 reservationKey, uint32[] walletMembersIDs) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) NotifyReservationActionTimeout(reservationKey *big.Int, walletMembersIDs []uint32) (*types.Transaction, error) { + return _ReservationRouter.Contract.NotifyReservationActionTimeout(&_ReservationRouter.TransactOpts, reservationKey, walletMembersIDs) +} + +// NotifyReservationStranded is a paid mutator transaction binding the contract method 0xf95ea36f. +// +// Solidity: function notifyReservationStranded(uint256 reservationKey) returns() +func (_ReservationRouter *ReservationRouterTransactor) NotifyReservationStranded(opts *bind.TransactOpts, reservationKey *big.Int) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "notifyReservationStranded", reservationKey) +} + +// NotifyReservationStranded is a paid mutator transaction binding the contract method 0xf95ea36f. +// +// Solidity: function notifyReservationStranded(uint256 reservationKey) returns() +func (_ReservationRouter *ReservationRouterSession) NotifyReservationStranded(reservationKey *big.Int) (*types.Transaction, error) { + return _ReservationRouter.Contract.NotifyReservationStranded(&_ReservationRouter.TransactOpts, reservationKey) +} + +// NotifyReservationStranded is a paid mutator transaction binding the contract method 0xf95ea36f. +// +// Solidity: function notifyReservationStranded(uint256 reservationKey) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) NotifyReservationStranded(reservationKey *big.Int) (*types.Transaction, error) { + return _ReservationRouter.Contract.NotifyReservationStranded(&_ReservationRouter.TransactOpts, reservationKey) +} + +// NotifyStaleReservedDeposit is a paid mutator transaction binding the contract method 0x6ceb1b54. +// +// Solidity: function notifyStaleReservedDeposit(uint256 depositKey) returns() +func (_ReservationRouter *ReservationRouterTransactor) NotifyStaleReservedDeposit(opts *bind.TransactOpts, depositKey *big.Int) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "notifyStaleReservedDeposit", depositKey) +} + +// NotifyStaleReservedDeposit is a paid mutator transaction binding the contract method 0x6ceb1b54. +// +// Solidity: function notifyStaleReservedDeposit(uint256 depositKey) returns() +func (_ReservationRouter *ReservationRouterSession) NotifyStaleReservedDeposit(depositKey *big.Int) (*types.Transaction, error) { + return _ReservationRouter.Contract.NotifyStaleReservedDeposit(&_ReservationRouter.TransactOpts, depositKey) +} + +// NotifyStaleReservedDeposit is a paid mutator transaction binding the contract method 0x6ceb1b54. +// +// Solidity: function notifyStaleReservedDeposit(uint256 depositKey) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) NotifyStaleReservedDeposit(depositKey *big.Int) (*types.Transaction, error) { + return _ReservationRouter.Contract.NotifyStaleReservedDeposit(&_ReservationRouter.TransactOpts, depositKey) +} + +// RequestReservationAcceptance is a paid mutator transaction binding the contract method 0xbc78a18e. +// +// Solidity: function requestReservationAcceptance(uint256 reservationKey, bytes20 walletPubKeyHash) returns() +func (_ReservationRouter *ReservationRouterTransactor) RequestReservationAcceptance(opts *bind.TransactOpts, reservationKey *big.Int, walletPubKeyHash [20]byte) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "requestReservationAcceptance", reservationKey, walletPubKeyHash) +} + +// RequestReservationAcceptance is a paid mutator transaction binding the contract method 0xbc78a18e. +// +// Solidity: function requestReservationAcceptance(uint256 reservationKey, bytes20 walletPubKeyHash) returns() +func (_ReservationRouter *ReservationRouterSession) RequestReservationAcceptance(reservationKey *big.Int, walletPubKeyHash [20]byte) (*types.Transaction, error) { + return _ReservationRouter.Contract.RequestReservationAcceptance(&_ReservationRouter.TransactOpts, reservationKey, walletPubKeyHash) +} + +// RequestReservationAcceptance is a paid mutator transaction binding the contract method 0xbc78a18e. +// +// Solidity: function requestReservationAcceptance(uint256 reservationKey, bytes20 walletPubKeyHash) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) RequestReservationAcceptance(reservationKey *big.Int, walletPubKeyHash [20]byte) (*types.Transaction, error) { + return _ReservationRouter.Contract.RequestReservationAcceptance(&_ReservationRouter.TransactOpts, reservationKey, walletPubKeyHash) +} + +// RequestReservationReanchor is a paid mutator transaction binding the contract method 0xf934beb5. +// +// Solidity: function requestReservationReanchor(uint256 reservationKey, bytes20 targetWalletPubKeyHash) returns() +func (_ReservationRouter *ReservationRouterTransactor) RequestReservationReanchor(opts *bind.TransactOpts, reservationKey *big.Int, targetWalletPubKeyHash [20]byte) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "requestReservationReanchor", reservationKey, targetWalletPubKeyHash) +} + +// RequestReservationReanchor is a paid mutator transaction binding the contract method 0xf934beb5. +// +// Solidity: function requestReservationReanchor(uint256 reservationKey, bytes20 targetWalletPubKeyHash) returns() +func (_ReservationRouter *ReservationRouterSession) RequestReservationReanchor(reservationKey *big.Int, targetWalletPubKeyHash [20]byte) (*types.Transaction, error) { + return _ReservationRouter.Contract.RequestReservationReanchor(&_ReservationRouter.TransactOpts, reservationKey, targetWalletPubKeyHash) +} + +// RequestReservationReanchor is a paid mutator transaction binding the contract method 0xf934beb5. +// +// Solidity: function requestReservationReanchor(uint256 reservationKey, bytes20 targetWalletPubKeyHash) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) RequestReservationReanchor(reservationKey *big.Int, targetWalletPubKeyHash [20]byte) (*types.Transaction, error) { + return _ReservationRouter.Contract.RequestReservationReanchor(&_ReservationRouter.TransactOpts, reservationKey, targetWalletPubKeyHash) +} + +// SubmitReservationProof is a paid mutator transaction binding the contract method 0x668a4980. +// +// Solidity: function submitReservationProof(uint8 proofType, (bytes4,bytes,bytes,bytes4) txInfo, (bytes,uint256,bytes,bytes32,bytes) proof, (bytes32,uint32,uint64) mainUtxo, uint256 reservationKey, uint64 requestNonce) returns() +func (_ReservationRouter *ReservationRouterTransactor) SubmitReservationProof(opts *bind.TransactOpts, proofType uint8, txInfo BitcoinTxInfo4, proof BitcoinTxProof3, mainUtxo BitcoinTxUTXO4, reservationKey *big.Int, requestNonce uint64) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "submitReservationProof", proofType, txInfo, proof, mainUtxo, reservationKey, requestNonce) +} + +// SubmitReservationProof is a paid mutator transaction binding the contract method 0x668a4980. +// +// Solidity: function submitReservationProof(uint8 proofType, (bytes4,bytes,bytes,bytes4) txInfo, (bytes,uint256,bytes,bytes32,bytes) proof, (bytes32,uint32,uint64) mainUtxo, uint256 reservationKey, uint64 requestNonce) returns() +func (_ReservationRouter *ReservationRouterSession) SubmitReservationProof(proofType uint8, txInfo BitcoinTxInfo4, proof BitcoinTxProof3, mainUtxo BitcoinTxUTXO4, reservationKey *big.Int, requestNonce uint64) (*types.Transaction, error) { + return _ReservationRouter.Contract.SubmitReservationProof(&_ReservationRouter.TransactOpts, proofType, txInfo, proof, mainUtxo, reservationKey, requestNonce) +} + +// SubmitReservationProof is a paid mutator transaction binding the contract method 0x668a4980. +// +// Solidity: function submitReservationProof(uint8 proofType, (bytes4,bytes,bytes,bytes4) txInfo, (bytes,uint256,bytes,bytes32,bytes) proof, (bytes32,uint32,uint64) mainUtxo, uint256 reservationKey, uint64 requestNonce) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) SubmitReservationProof(proofType uint8, txInfo BitcoinTxInfo4, proof BitcoinTxProof3, mainUtxo BitcoinTxUTXO4, reservationKey *big.Int, requestNonce uint64) (*types.Transaction, error) { + return _ReservationRouter.Contract.SubmitReservationProof(&_ReservationRouter.TransactOpts, proofType, txInfo, proof, mainUtxo, reservationKey, requestNonce) +} + +// TransferGovernance is a paid mutator transaction binding the contract method 0xd38bfff4. +// +// Solidity: function transferGovernance(address newGovernance) returns() +func (_ReservationRouter *ReservationRouterTransactor) TransferGovernance(opts *bind.TransactOpts, newGovernance common.Address) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "transferGovernance", newGovernance) +} + +// TransferGovernance is a paid mutator transaction binding the contract method 0xd38bfff4. +// +// Solidity: function transferGovernance(address newGovernance) returns() +func (_ReservationRouter *ReservationRouterSession) TransferGovernance(newGovernance common.Address) (*types.Transaction, error) { + return _ReservationRouter.Contract.TransferGovernance(&_ReservationRouter.TransactOpts, newGovernance) +} + +// TransferGovernance is a paid mutator transaction binding the contract method 0xd38bfff4. +// +// Solidity: function transferGovernance(address newGovernance) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) TransferGovernance(newGovernance common.Address) (*types.Transaction, error) { + return _ReservationRouter.Contract.TransferGovernance(&_ReservationRouter.TransactOpts, newGovernance) +} + +// UpdateReservationCaps is a paid mutator transaction binding the contract method 0x8308c2ca. +// +// Solidity: function updateReservationCaps(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount, uint32 maxActiveReservations) returns() +func (_ReservationRouter *ReservationRouterTransactor) UpdateReservationCaps(opts *bind.TransactOpts, maxReservationsAmountPerWallet uint64, reservationMaxSingleAmount uint64, maxActiveReservations uint32) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "updateReservationCaps", maxReservationsAmountPerWallet, reservationMaxSingleAmount, maxActiveReservations) +} + +// UpdateReservationCaps is a paid mutator transaction binding the contract method 0x8308c2ca. +// +// Solidity: function updateReservationCaps(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount, uint32 maxActiveReservations) returns() +func (_ReservationRouter *ReservationRouterSession) UpdateReservationCaps(maxReservationsAmountPerWallet uint64, reservationMaxSingleAmount uint64, maxActiveReservations uint32) (*types.Transaction, error) { + return _ReservationRouter.Contract.UpdateReservationCaps(&_ReservationRouter.TransactOpts, maxReservationsAmountPerWallet, reservationMaxSingleAmount, maxActiveReservations) +} + +// UpdateReservationCaps is a paid mutator transaction binding the contract method 0x8308c2ca. +// +// Solidity: function updateReservationCaps(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount, uint32 maxActiveReservations) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) UpdateReservationCaps(maxReservationsAmountPerWallet uint64, reservationMaxSingleAmount uint64, maxActiveReservations uint32) (*types.Transaction, error) { + return _ReservationRouter.Contract.UpdateReservationCaps(&_ReservationRouter.TransactOpts, maxReservationsAmountPerWallet, reservationMaxSingleAmount, maxActiveReservations) +} + +// UpdateReservationParameters is a paid mutator transaction binding the contract method 0x59f6408b. +// +// Solidity: function updateReservationParameters(address reservationVault, uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) returns() +func (_ReservationRouter *ReservationRouterTransactor) UpdateReservationParameters(opts *bind.TransactOpts, reservationVault common.Address, reservationMinAmount uint64, reservationTxMaxFee uint64, reservationTermSeconds uint32, reservationDissolutionDelay uint32, reservationMaxTotalAmount uint64, maxReservationsPerWallet uint32, reservationActionTimeout uint32, reservationRenewalWindowSeconds uint32) (*types.Transaction, error) { + return _ReservationRouter.contract.Transact(opts, "updateReservationParameters", reservationVault, reservationMinAmount, reservationTxMaxFee, reservationTermSeconds, reservationDissolutionDelay, reservationMaxTotalAmount, maxReservationsPerWallet, reservationActionTimeout, reservationRenewalWindowSeconds) +} + +// UpdateReservationParameters is a paid mutator transaction binding the contract method 0x59f6408b. +// +// Solidity: function updateReservationParameters(address reservationVault, uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) returns() +func (_ReservationRouter *ReservationRouterSession) UpdateReservationParameters(reservationVault common.Address, reservationMinAmount uint64, reservationTxMaxFee uint64, reservationTermSeconds uint32, reservationDissolutionDelay uint32, reservationMaxTotalAmount uint64, maxReservationsPerWallet uint32, reservationActionTimeout uint32, reservationRenewalWindowSeconds uint32) (*types.Transaction, error) { + return _ReservationRouter.Contract.UpdateReservationParameters(&_ReservationRouter.TransactOpts, reservationVault, reservationMinAmount, reservationTxMaxFee, reservationTermSeconds, reservationDissolutionDelay, reservationMaxTotalAmount, maxReservationsPerWallet, reservationActionTimeout, reservationRenewalWindowSeconds) +} + +// UpdateReservationParameters is a paid mutator transaction binding the contract method 0x59f6408b. +// +// Solidity: function updateReservationParameters(address reservationVault, uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) returns() +func (_ReservationRouter *ReservationRouterTransactorSession) UpdateReservationParameters(reservationVault common.Address, reservationMinAmount uint64, reservationTxMaxFee uint64, reservationTermSeconds uint32, reservationDissolutionDelay uint32, reservationMaxTotalAmount uint64, maxReservationsPerWallet uint32, reservationActionTimeout uint32, reservationRenewalWindowSeconds uint32) (*types.Transaction, error) { + return _ReservationRouter.Contract.UpdateReservationParameters(&_ReservationRouter.TransactOpts, reservationVault, reservationMinAmount, reservationTxMaxFee, reservationTermSeconds, reservationDissolutionDelay, reservationMaxTotalAmount, maxReservationsPerWallet, reservationActionTimeout, reservationRenewalWindowSeconds) +} + +// ReservationRouterGovernanceTransferredIterator is returned from FilterGovernanceTransferred and is used to iterate over the raw logs and unpacked data for GovernanceTransferred events raised by the ReservationRouter contract. +type ReservationRouterGovernanceTransferredIterator struct { + Event *ReservationRouterGovernanceTransferred // 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 *ReservationRouterGovernanceTransferredIterator) 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(ReservationRouterGovernanceTransferred) + 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(ReservationRouterGovernanceTransferred) + 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 *ReservationRouterGovernanceTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterGovernanceTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterGovernanceTransferred represents a GovernanceTransferred event raised by the ReservationRouter contract. +type ReservationRouterGovernanceTransferred struct { + OldGovernance common.Address + NewGovernance common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterGovernanceTransferred is a free log retrieval operation binding the contract event 0x5f56bee8cffbe9a78652a74a60705edede02af10b0bbb888ca44b79a0d42ce80. +// +// Solidity: event GovernanceTransferred(address oldGovernance, address newGovernance) +func (_ReservationRouter *ReservationRouterFilterer) FilterGovernanceTransferred(opts *bind.FilterOpts) (*ReservationRouterGovernanceTransferredIterator, error) { + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "GovernanceTransferred") + if err != nil { + return nil, err + } + return &ReservationRouterGovernanceTransferredIterator{contract: _ReservationRouter.contract, event: "GovernanceTransferred", logs: logs, sub: sub}, nil +} + +// WatchGovernanceTransferred is a free log subscription operation binding the contract event 0x5f56bee8cffbe9a78652a74a60705edede02af10b0bbb888ca44b79a0d42ce80. +// +// Solidity: event GovernanceTransferred(address oldGovernance, address newGovernance) +func (_ReservationRouter *ReservationRouterFilterer) WatchGovernanceTransferred(opts *bind.WatchOpts, sink chan<- *ReservationRouterGovernanceTransferred) (event.Subscription, error) { + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "GovernanceTransferred") + 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(ReservationRouterGovernanceTransferred) + if err := _ReservationRouter.contract.UnpackLog(event, "GovernanceTransferred", 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 +} + +// ParseGovernanceTransferred is a log parse operation binding the contract event 0x5f56bee8cffbe9a78652a74a60705edede02af10b0bbb888ca44b79a0d42ce80. +// +// Solidity: event GovernanceTransferred(address oldGovernance, address newGovernance) +func (_ReservationRouter *ReservationRouterFilterer) ParseGovernanceTransferred(log types.Log) (*ReservationRouterGovernanceTransferred, error) { + event := new(ReservationRouterGovernanceTransferred) + if err := _ReservationRouter.contract.UnpackLog(event, "GovernanceTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the ReservationRouter contract. +type ReservationRouterInitializedIterator struct { + Event *ReservationRouterInitialized // 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 *ReservationRouterInitializedIterator) 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(ReservationRouterInitialized) + 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(ReservationRouterInitialized) + 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 *ReservationRouterInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterInitialized represents a Initialized event raised by the ReservationRouter contract. +type ReservationRouterInitialized struct { + Version uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_ReservationRouter *ReservationRouterFilterer) FilterInitialized(opts *bind.FilterOpts) (*ReservationRouterInitializedIterator, error) { + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &ReservationRouterInitializedIterator{contract: _ReservationRouter.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_ReservationRouter *ReservationRouterFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *ReservationRouterInitialized) (event.Subscription, error) { + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "Initialized") + 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(ReservationRouterInitialized) + if err := _ReservationRouter.contract.UnpackLog(event, "Initialized", 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 +} + +// ParseInitialized is a log parse operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_ReservationRouter *ReservationRouterFilterer) ParseInitialized(log types.Log) (*ReservationRouterInitialized, error) { + event := new(ReservationRouterInitialized) + if err := _ReservationRouter.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationAcceptanceRequestedIterator is returned from FilterReservationAcceptanceRequested and is used to iterate over the raw logs and unpacked data for ReservationAcceptanceRequested events raised by the ReservationRouter contract. +type ReservationRouterReservationAcceptanceRequestedIterator struct { + Event *ReservationRouterReservationAcceptanceRequested // 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 *ReservationRouterReservationAcceptanceRequestedIterator) 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(ReservationRouterReservationAcceptanceRequested) + 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(ReservationRouterReservationAcceptanceRequested) + 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 *ReservationRouterReservationAcceptanceRequestedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationAcceptanceRequestedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationAcceptanceRequested represents a ReservationAcceptanceRequested event raised by the ReservationRouter contract. +type ReservationRouterReservationAcceptanceRequested struct { + ReservationKey *big.Int + RequestNonce uint64 + WalletPubKeyHash [20]byte + DepositAmount uint64 + TxMaxFee uint64 + TimeoutAt uint32 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationAcceptanceRequested is a free log retrieval operation binding the contract event 0x1444a0a2e553520e4766f36c68368e10105489e52dc701d7fc9859c651475059. +// +// Solidity: event ReservationAcceptanceRequested(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed walletPubKeyHash, uint64 depositAmount, uint64 txMaxFee, uint32 timeoutAt) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationAcceptanceRequested(opts *bind.FilterOpts, reservationKey []*big.Int, walletPubKeyHash [][20]byte) (*ReservationRouterReservationAcceptanceRequestedIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationAcceptanceRequested", reservationKeyRule, walletPubKeyHashRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationAcceptanceRequestedIterator{contract: _ReservationRouter.contract, event: "ReservationAcceptanceRequested", logs: logs, sub: sub}, nil +} + +// WatchReservationAcceptanceRequested is a free log subscription operation binding the contract event 0x1444a0a2e553520e4766f36c68368e10105489e52dc701d7fc9859c651475059. +// +// Solidity: event ReservationAcceptanceRequested(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed walletPubKeyHash, uint64 depositAmount, uint64 txMaxFee, uint32 timeoutAt) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationAcceptanceRequested(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationAcceptanceRequested, reservationKey []*big.Int, walletPubKeyHash [][20]byte) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationAcceptanceRequested", reservationKeyRule, walletPubKeyHashRule) + 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(ReservationRouterReservationAcceptanceRequested) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationAcceptanceRequested", 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 +} + +// ParseReservationAcceptanceRequested is a log parse operation binding the contract event 0x1444a0a2e553520e4766f36c68368e10105489e52dc701d7fc9859c651475059. +// +// Solidity: event ReservationAcceptanceRequested(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed walletPubKeyHash, uint64 depositAmount, uint64 txMaxFee, uint32 timeoutAt) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationAcceptanceRequested(log types.Log) (*ReservationRouterReservationAcceptanceRequested, error) { + event := new(ReservationRouterReservationAcceptanceRequested) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationAcceptanceRequested", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationAcceptedIterator is returned from FilterReservationAccepted and is used to iterate over the raw logs and unpacked data for ReservationAccepted events raised by the ReservationRouter contract. +type ReservationRouterReservationAcceptedIterator struct { + Event *ReservationRouterReservationAccepted // 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 *ReservationRouterReservationAcceptedIterator) 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(ReservationRouterReservationAccepted) + 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(ReservationRouterReservationAccepted) + 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 *ReservationRouterReservationAcceptedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationAcceptedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationAccepted represents a ReservationAccepted event raised by the ReservationRouter contract. +type ReservationRouterReservationAccepted struct { + ReservationKey *big.Int + RequestNonce uint64 + WalletPubKeyHash [20]byte + Owner common.Address + AnchorTxHash [32]byte + AnchorAmount uint64 + ExpiresAt uint32 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationAccepted is a free log retrieval operation binding the contract event 0xcdba3d32072456500fc4b138dd3c63bb0a72d568e71af8a51744bab51238b770. +// +// Solidity: event ReservationAccepted(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed walletPubKeyHash, address indexed owner, bytes32 anchorTxHash, uint64 anchorAmount, uint32 expiresAt) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationAccepted(opts *bind.FilterOpts, reservationKey []*big.Int, walletPubKeyHash [][20]byte, owner []common.Address) (*ReservationRouterReservationAcceptedIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationAccepted", reservationKeyRule, walletPubKeyHashRule, ownerRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationAcceptedIterator{contract: _ReservationRouter.contract, event: "ReservationAccepted", logs: logs, sub: sub}, nil +} + +// WatchReservationAccepted is a free log subscription operation binding the contract event 0xcdba3d32072456500fc4b138dd3c63bb0a72d568e71af8a51744bab51238b770. +// +// Solidity: event ReservationAccepted(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed walletPubKeyHash, address indexed owner, bytes32 anchorTxHash, uint64 anchorAmount, uint32 expiresAt) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationAccepted(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationAccepted, reservationKey []*big.Int, walletPubKeyHash [][20]byte, owner []common.Address) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationAccepted", reservationKeyRule, walletPubKeyHashRule, ownerRule) + 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(ReservationRouterReservationAccepted) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationAccepted", 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 +} + +// ParseReservationAccepted is a log parse operation binding the contract event 0xcdba3d32072456500fc4b138dd3c63bb0a72d568e71af8a51744bab51238b770. +// +// Solidity: event ReservationAccepted(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed walletPubKeyHash, address indexed owner, bytes32 anchorTxHash, uint64 anchorAmount, uint32 expiresAt) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationAccepted(log types.Log) (*ReservationRouterReservationAccepted, error) { + event := new(ReservationRouterReservationAccepted) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationAccepted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationActionSupersededIterator is returned from FilterReservationActionSuperseded and is used to iterate over the raw logs and unpacked data for ReservationActionSuperseded events raised by the ReservationRouter contract. +type ReservationRouterReservationActionSupersededIterator struct { + Event *ReservationRouterReservationActionSuperseded // 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 *ReservationRouterReservationActionSupersededIterator) 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(ReservationRouterReservationActionSuperseded) + 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(ReservationRouterReservationActionSuperseded) + 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 *ReservationRouterReservationActionSupersededIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationActionSupersededIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationActionSuperseded represents a ReservationActionSuperseded event raised by the ReservationRouter contract. +type ReservationRouterReservationActionSuperseded struct { + ReservationKey *big.Int + RequestNonce uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationActionSuperseded is a free log retrieval operation binding the contract event 0x64979c37b08d25f639dac3b74caf99840af5995eba8a493b29dc8599312ea252. +// +// Solidity: event ReservationActionSuperseded(uint256 indexed reservationKey, uint64 requestNonce) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationActionSuperseded(opts *bind.FilterOpts, reservationKey []*big.Int) (*ReservationRouterReservationActionSupersededIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationActionSuperseded", reservationKeyRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationActionSupersededIterator{contract: _ReservationRouter.contract, event: "ReservationActionSuperseded", logs: logs, sub: sub}, nil +} + +// WatchReservationActionSuperseded is a free log subscription operation binding the contract event 0x64979c37b08d25f639dac3b74caf99840af5995eba8a493b29dc8599312ea252. +// +// Solidity: event ReservationActionSuperseded(uint256 indexed reservationKey, uint64 requestNonce) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationActionSuperseded(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationActionSuperseded, reservationKey []*big.Int) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationActionSuperseded", reservationKeyRule) + 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(ReservationRouterReservationActionSuperseded) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationActionSuperseded", 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 +} + +// ParseReservationActionSuperseded is a log parse operation binding the contract event 0x64979c37b08d25f639dac3b74caf99840af5995eba8a493b29dc8599312ea252. +// +// Solidity: event ReservationActionSuperseded(uint256 indexed reservationKey, uint64 requestNonce) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationActionSuperseded(log types.Log) (*ReservationRouterReservationActionSuperseded, error) { + event := new(ReservationRouterReservationActionSuperseded) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationActionSuperseded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationActionTimedOutIterator is returned from FilterReservationActionTimedOut and is used to iterate over the raw logs and unpacked data for ReservationActionTimedOut events raised by the ReservationRouter contract. +type ReservationRouterReservationActionTimedOutIterator struct { + Event *ReservationRouterReservationActionTimedOut // 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 *ReservationRouterReservationActionTimedOutIterator) 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(ReservationRouterReservationActionTimedOut) + 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(ReservationRouterReservationActionTimedOut) + 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 *ReservationRouterReservationActionTimedOutIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationActionTimedOutIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationActionTimedOut represents a ReservationActionTimedOut event raised by the ReservationRouter contract. +type ReservationRouterReservationActionTimedOut struct { + ReservationKey *big.Int + RequestNonce uint64 + ActionType uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationActionTimedOut is a free log retrieval operation binding the contract event 0xd3bb43b8c8b259f4da0efa2c7a34ce683c05d6e31864299fa4a867bb3ff218ba. +// +// Solidity: event ReservationActionTimedOut(uint256 indexed reservationKey, uint64 requestNonce, uint8 actionType) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationActionTimedOut(opts *bind.FilterOpts, reservationKey []*big.Int) (*ReservationRouterReservationActionTimedOutIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationActionTimedOut", reservationKeyRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationActionTimedOutIterator{contract: _ReservationRouter.contract, event: "ReservationActionTimedOut", logs: logs, sub: sub}, nil +} + +// WatchReservationActionTimedOut is a free log subscription operation binding the contract event 0xd3bb43b8c8b259f4da0efa2c7a34ce683c05d6e31864299fa4a867bb3ff218ba. +// +// Solidity: event ReservationActionTimedOut(uint256 indexed reservationKey, uint64 requestNonce, uint8 actionType) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationActionTimedOut(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationActionTimedOut, reservationKey []*big.Int) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationActionTimedOut", reservationKeyRule) + 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(ReservationRouterReservationActionTimedOut) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationActionTimedOut", 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 +} + +// ParseReservationActionTimedOut is a log parse operation binding the contract event 0xd3bb43b8c8b259f4da0efa2c7a34ce683c05d6e31864299fa4a867bb3ff218ba. +// +// Solidity: event ReservationActionTimedOut(uint256 indexed reservationKey, uint64 requestNonce, uint8 actionType) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationActionTimedOut(log types.Log) (*ReservationRouterReservationActionTimedOut, error) { + event := new(ReservationRouterReservationActionTimedOut) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationActionTimedOut", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationCapsUpdatedIterator is returned from FilterReservationCapsUpdated and is used to iterate over the raw logs and unpacked data for ReservationCapsUpdated events raised by the ReservationRouter contract. +type ReservationRouterReservationCapsUpdatedIterator struct { + Event *ReservationRouterReservationCapsUpdated // 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 *ReservationRouterReservationCapsUpdatedIterator) 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(ReservationRouterReservationCapsUpdated) + 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(ReservationRouterReservationCapsUpdated) + 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 *ReservationRouterReservationCapsUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationCapsUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationCapsUpdated represents a ReservationCapsUpdated event raised by the ReservationRouter contract. +type ReservationRouterReservationCapsUpdated struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 + MaxActiveReservations uint32 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationCapsUpdated is a free log retrieval operation binding the contract event 0x846df5edb182147898ee0a522f03f78718544ce16b0f06e64fe2f593b1fb160d. +// +// Solidity: event ReservationCapsUpdated(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount, uint32 maxActiveReservations) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationCapsUpdated(opts *bind.FilterOpts) (*ReservationRouterReservationCapsUpdatedIterator, error) { + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationCapsUpdated") + if err != nil { + return nil, err + } + return &ReservationRouterReservationCapsUpdatedIterator{contract: _ReservationRouter.contract, event: "ReservationCapsUpdated", logs: logs, sub: sub}, nil +} + +// WatchReservationCapsUpdated is a free log subscription operation binding the contract event 0x846df5edb182147898ee0a522f03f78718544ce16b0f06e64fe2f593b1fb160d. +// +// Solidity: event ReservationCapsUpdated(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount, uint32 maxActiveReservations) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationCapsUpdated(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationCapsUpdated) (event.Subscription, error) { + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationCapsUpdated") + 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(ReservationRouterReservationCapsUpdated) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationCapsUpdated", 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 +} + +// ParseReservationCapsUpdated is a log parse operation binding the contract event 0x846df5edb182147898ee0a522f03f78718544ce16b0f06e64fe2f593b1fb160d. +// +// Solidity: event ReservationCapsUpdated(uint64 maxReservationsAmountPerWallet, uint64 reservationMaxSingleAmount, uint32 maxActiveReservations) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationCapsUpdated(log types.Log) (*ReservationRouterReservationCapsUpdated, error) { + event := new(ReservationRouterReservationCapsUpdated) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationCapsUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationLateSettledIterator is returned from FilterReservationLateSettled and is used to iterate over the raw logs and unpacked data for ReservationLateSettled events raised by the ReservationRouter contract. +type ReservationRouterReservationLateSettledIterator struct { + Event *ReservationRouterReservationLateSettled // 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 *ReservationRouterReservationLateSettledIterator) 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(ReservationRouterReservationLateSettled) + 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(ReservationRouterReservationLateSettled) + 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 *ReservationRouterReservationLateSettledIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationLateSettledIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationLateSettled represents a ReservationLateSettled event raised by the ReservationRouter contract. +type ReservationRouterReservationLateSettled struct { + ReservationKey *big.Int + RequestNonce uint64 + ActionType uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationLateSettled is a free log retrieval operation binding the contract event 0x152c5b7b78a634931e032d1ab3d0c033e2e5d00e0e75f20252767746a1fa4f6d. +// +// Solidity: event ReservationLateSettled(uint256 indexed reservationKey, uint64 requestNonce, uint8 actionType) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationLateSettled(opts *bind.FilterOpts, reservationKey []*big.Int) (*ReservationRouterReservationLateSettledIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationLateSettled", reservationKeyRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationLateSettledIterator{contract: _ReservationRouter.contract, event: "ReservationLateSettled", logs: logs, sub: sub}, nil +} + +// WatchReservationLateSettled is a free log subscription operation binding the contract event 0x152c5b7b78a634931e032d1ab3d0c033e2e5d00e0e75f20252767746a1fa4f6d. +// +// Solidity: event ReservationLateSettled(uint256 indexed reservationKey, uint64 requestNonce, uint8 actionType) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationLateSettled(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationLateSettled, reservationKey []*big.Int) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationLateSettled", reservationKeyRule) + 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(ReservationRouterReservationLateSettled) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationLateSettled", 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 +} + +// ParseReservationLateSettled is a log parse operation binding the contract event 0x152c5b7b78a634931e032d1ab3d0c033e2e5d00e0e75f20252767746a1fa4f6d. +// +// Solidity: event ReservationLateSettled(uint256 indexed reservationKey, uint64 requestNonce, uint8 actionType) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationLateSettled(log types.Log) (*ReservationRouterReservationLateSettled, error) { + event := new(ReservationRouterReservationLateSettled) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationLateSettled", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationParametersUpdatedIterator is returned from FilterReservationParametersUpdated and is used to iterate over the raw logs and unpacked data for ReservationParametersUpdated events raised by the ReservationRouter contract. +type ReservationRouterReservationParametersUpdatedIterator struct { + Event *ReservationRouterReservationParametersUpdated // 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 *ReservationRouterReservationParametersUpdatedIterator) 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(ReservationRouterReservationParametersUpdated) + 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(ReservationRouterReservationParametersUpdated) + 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 *ReservationRouterReservationParametersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationParametersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationParametersUpdated represents a ReservationParametersUpdated event raised by the ReservationRouter contract. +type ReservationRouterReservationParametersUpdated struct { + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationParametersUpdated is a free log retrieval operation binding the contract event 0x7e6c56281c83edd8db45ddcb09afd1cc2cc009bcc94213c657592cb15a5b2901. +// +// Solidity: event ReservationParametersUpdated(uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationParametersUpdated(opts *bind.FilterOpts) (*ReservationRouterReservationParametersUpdatedIterator, error) { + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationParametersUpdated") + if err != nil { + return nil, err + } + return &ReservationRouterReservationParametersUpdatedIterator{contract: _ReservationRouter.contract, event: "ReservationParametersUpdated", logs: logs, sub: sub}, nil +} + +// WatchReservationParametersUpdated is a free log subscription operation binding the contract event 0x7e6c56281c83edd8db45ddcb09afd1cc2cc009bcc94213c657592cb15a5b2901. +// +// Solidity: event ReservationParametersUpdated(uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationParametersUpdated(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationParametersUpdated) (event.Subscription, error) { + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationParametersUpdated") + 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(ReservationRouterReservationParametersUpdated) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationParametersUpdated", 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 +} + +// ParseReservationParametersUpdated is a log parse operation binding the contract event 0x7e6c56281c83edd8db45ddcb09afd1cc2cc009bcc94213c657592cb15a5b2901. +// +// Solidity: event ReservationParametersUpdated(uint64 reservationMinAmount, uint64 reservationTxMaxFee, uint32 reservationTermSeconds, uint32 reservationDissolutionDelay, uint64 reservationMaxTotalAmount, uint32 maxReservationsPerWallet, uint32 reservationActionTimeout, uint32 reservationRenewalWindowSeconds) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationParametersUpdated(log types.Log) (*ReservationRouterReservationParametersUpdated, error) { + event := new(ReservationRouterReservationParametersUpdated) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationParametersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationReanchorRequestedIterator is returned from FilterReservationReanchorRequested and is used to iterate over the raw logs and unpacked data for ReservationReanchorRequested events raised by the ReservationRouter contract. +type ReservationRouterReservationReanchorRequestedIterator struct { + Event *ReservationRouterReservationReanchorRequested // 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 *ReservationRouterReservationReanchorRequestedIterator) 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(ReservationRouterReservationReanchorRequested) + 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(ReservationRouterReservationReanchorRequested) + 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 *ReservationRouterReservationReanchorRequestedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationReanchorRequestedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationReanchorRequested represents a ReservationReanchorRequested event raised by the ReservationRouter contract. +type ReservationRouterReservationReanchorRequested struct { + ReservationKey *big.Int + RequestNonce uint64 + SourceWalletPubKeyHash [20]byte + TargetWalletPubKeyHash [20]byte + TxMaxFee uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationReanchorRequested is a free log retrieval operation binding the contract event 0x90323e7ede1e7009754d91387f23522528e6356b2667952ff305a500ffaa9c6d. +// +// Solidity: event ReservationReanchorRequested(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed sourceWalletPubKeyHash, bytes20 indexed targetWalletPubKeyHash, uint64 txMaxFee) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationReanchorRequested(opts *bind.FilterOpts, reservationKey []*big.Int, sourceWalletPubKeyHash [][20]byte, targetWalletPubKeyHash [][20]byte) (*ReservationRouterReservationReanchorRequestedIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var sourceWalletPubKeyHashRule []interface{} + for _, sourceWalletPubKeyHashItem := range sourceWalletPubKeyHash { + sourceWalletPubKeyHashRule = append(sourceWalletPubKeyHashRule, sourceWalletPubKeyHashItem) + } + var targetWalletPubKeyHashRule []interface{} + for _, targetWalletPubKeyHashItem := range targetWalletPubKeyHash { + targetWalletPubKeyHashRule = append(targetWalletPubKeyHashRule, targetWalletPubKeyHashItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationReanchorRequested", reservationKeyRule, sourceWalletPubKeyHashRule, targetWalletPubKeyHashRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationReanchorRequestedIterator{contract: _ReservationRouter.contract, event: "ReservationReanchorRequested", logs: logs, sub: sub}, nil +} + +// WatchReservationReanchorRequested is a free log subscription operation binding the contract event 0x90323e7ede1e7009754d91387f23522528e6356b2667952ff305a500ffaa9c6d. +// +// Solidity: event ReservationReanchorRequested(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed sourceWalletPubKeyHash, bytes20 indexed targetWalletPubKeyHash, uint64 txMaxFee) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationReanchorRequested(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationReanchorRequested, reservationKey []*big.Int, sourceWalletPubKeyHash [][20]byte, targetWalletPubKeyHash [][20]byte) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var sourceWalletPubKeyHashRule []interface{} + for _, sourceWalletPubKeyHashItem := range sourceWalletPubKeyHash { + sourceWalletPubKeyHashRule = append(sourceWalletPubKeyHashRule, sourceWalletPubKeyHashItem) + } + var targetWalletPubKeyHashRule []interface{} + for _, targetWalletPubKeyHashItem := range targetWalletPubKeyHash { + targetWalletPubKeyHashRule = append(targetWalletPubKeyHashRule, targetWalletPubKeyHashItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationReanchorRequested", reservationKeyRule, sourceWalletPubKeyHashRule, targetWalletPubKeyHashRule) + 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(ReservationRouterReservationReanchorRequested) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationReanchorRequested", 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 +} + +// ParseReservationReanchorRequested is a log parse operation binding the contract event 0x90323e7ede1e7009754d91387f23522528e6356b2667952ff305a500ffaa9c6d. +// +// Solidity: event ReservationReanchorRequested(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed sourceWalletPubKeyHash, bytes20 indexed targetWalletPubKeyHash, uint64 txMaxFee) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationReanchorRequested(log types.Log) (*ReservationRouterReservationReanchorRequested, error) { + event := new(ReservationRouterReservationReanchorRequested) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationReanchorRequested", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationReanchoredIterator is returned from FilterReservationReanchored and is used to iterate over the raw logs and unpacked data for ReservationReanchored events raised by the ReservationRouter contract. +type ReservationRouterReservationReanchoredIterator struct { + Event *ReservationRouterReservationReanchored // 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 *ReservationRouterReservationReanchoredIterator) 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(ReservationRouterReservationReanchored) + 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(ReservationRouterReservationReanchored) + 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 *ReservationRouterReservationReanchoredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationReanchoredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationReanchored represents a ReservationReanchored event raised by the ReservationRouter contract. +type ReservationRouterReservationReanchored struct { + ReservationKey *big.Int + RequestNonce uint64 + NewWalletPubKeyHash [20]byte + NewAnchorTxHash [32]byte + NewAnchorAmount uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationReanchored is a free log retrieval operation binding the contract event 0xe42922c665e9600f84def7070f9dfeeb5dd650fb3522fb6bb2326e676bd23319. +// +// Solidity: event ReservationReanchored(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed newWalletPubKeyHash, bytes32 newAnchorTxHash, uint64 newAnchorAmount) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationReanchored(opts *bind.FilterOpts, reservationKey []*big.Int, newWalletPubKeyHash [][20]byte) (*ReservationRouterReservationReanchoredIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var newWalletPubKeyHashRule []interface{} + for _, newWalletPubKeyHashItem := range newWalletPubKeyHash { + newWalletPubKeyHashRule = append(newWalletPubKeyHashRule, newWalletPubKeyHashItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationReanchored", reservationKeyRule, newWalletPubKeyHashRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationReanchoredIterator{contract: _ReservationRouter.contract, event: "ReservationReanchored", logs: logs, sub: sub}, nil +} + +// WatchReservationReanchored is a free log subscription operation binding the contract event 0xe42922c665e9600f84def7070f9dfeeb5dd650fb3522fb6bb2326e676bd23319. +// +// Solidity: event ReservationReanchored(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed newWalletPubKeyHash, bytes32 newAnchorTxHash, uint64 newAnchorAmount) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationReanchored(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationReanchored, reservationKey []*big.Int, newWalletPubKeyHash [][20]byte) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + var newWalletPubKeyHashRule []interface{} + for _, newWalletPubKeyHashItem := range newWalletPubKeyHash { + newWalletPubKeyHashRule = append(newWalletPubKeyHashRule, newWalletPubKeyHashItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationReanchored", reservationKeyRule, newWalletPubKeyHashRule) + 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(ReservationRouterReservationReanchored) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationReanchored", 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 +} + +// ParseReservationReanchored is a log parse operation binding the contract event 0xe42922c665e9600f84def7070f9dfeeb5dd650fb3522fb6bb2326e676bd23319. +// +// Solidity: event ReservationReanchored(uint256 indexed reservationKey, uint64 requestNonce, bytes20 indexed newWalletPubKeyHash, bytes32 newAnchorTxHash, uint64 newAnchorAmount) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationReanchored(log types.Log) (*ReservationRouterReservationReanchored, error) { + event := new(ReservationRouterReservationReanchored) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationReanchored", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationRetryCreditMintedIterator is returned from FilterReservationRetryCreditMinted and is used to iterate over the raw logs and unpacked data for ReservationRetryCreditMinted events raised by the ReservationRouter contract. +type ReservationRouterReservationRetryCreditMintedIterator struct { + Event *ReservationRouterReservationRetryCreditMinted // 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 *ReservationRouterReservationRetryCreditMintedIterator) 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(ReservationRouterReservationRetryCreditMinted) + 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(ReservationRouterReservationRetryCreditMinted) + 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 *ReservationRouterReservationRetryCreditMintedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationRetryCreditMintedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationRetryCreditMinted represents a ReservationRetryCreditMinted event raised by the ReservationRouter contract. +type ReservationRouterReservationRetryCreditMinted struct { + ReservationKey *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationRetryCreditMinted is a free log retrieval operation binding the contract event 0x919795353bc408e11a0d5a133a9c6969367014ad975910a8b31d08b12597c4c2. +// +// Solidity: event ReservationRetryCreditMinted(uint256 indexed reservationKey) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationRetryCreditMinted(opts *bind.FilterOpts, reservationKey []*big.Int) (*ReservationRouterReservationRetryCreditMintedIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationRetryCreditMinted", reservationKeyRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationRetryCreditMintedIterator{contract: _ReservationRouter.contract, event: "ReservationRetryCreditMinted", logs: logs, sub: sub}, nil +} + +// WatchReservationRetryCreditMinted is a free log subscription operation binding the contract event 0x919795353bc408e11a0d5a133a9c6969367014ad975910a8b31d08b12597c4c2. +// +// Solidity: event ReservationRetryCreditMinted(uint256 indexed reservationKey) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationRetryCreditMinted(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationRetryCreditMinted, reservationKey []*big.Int) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationRetryCreditMinted", reservationKeyRule) + 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(ReservationRouterReservationRetryCreditMinted) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationRetryCreditMinted", 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 +} + +// ParseReservationRetryCreditMinted is a log parse operation binding the contract event 0x919795353bc408e11a0d5a133a9c6969367014ad975910a8b31d08b12597c4c2. +// +// Solidity: event ReservationRetryCreditMinted(uint256 indexed reservationKey) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationRetryCreditMinted(log types.Log) (*ReservationRouterReservationRetryCreditMinted, error) { + event := new(ReservationRouterReservationRetryCreditMinted) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationRetryCreditMinted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationRouterSetIterator is returned from FilterReservationRouterSet and is used to iterate over the raw logs and unpacked data for ReservationRouterSet events raised by the ReservationRouter contract. +type ReservationRouterReservationRouterSetIterator struct { + Event *ReservationRouterReservationRouterSet // 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 *ReservationRouterReservationRouterSetIterator) 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(ReservationRouterReservationRouterSet) + 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(ReservationRouterReservationRouterSet) + 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 *ReservationRouterReservationRouterSetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationRouterSetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationRouterSet represents a ReservationRouterSet event raised by the ReservationRouter contract. +type ReservationRouterReservationRouterSet struct { + ReservationRouter common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationRouterSet is a free log retrieval operation binding the contract event 0xd9eacf62803dd1f1bb5342d8eb5951546c371915e06223f589c0c95486c7c769. +// +// Solidity: event ReservationRouterSet(address reservationRouter) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationRouterSet(opts *bind.FilterOpts) (*ReservationRouterReservationRouterSetIterator, error) { + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationRouterSet") + if err != nil { + return nil, err + } + return &ReservationRouterReservationRouterSetIterator{contract: _ReservationRouter.contract, event: "ReservationRouterSet", logs: logs, sub: sub}, nil +} + +// WatchReservationRouterSet is a free log subscription operation binding the contract event 0xd9eacf62803dd1f1bb5342d8eb5951546c371915e06223f589c0c95486c7c769. +// +// Solidity: event ReservationRouterSet(address reservationRouter) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationRouterSet(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationRouterSet) (event.Subscription, error) { + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationRouterSet") + 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(ReservationRouterReservationRouterSet) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationRouterSet", 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 +} + +// ParseReservationRouterSet is a log parse operation binding the contract event 0xd9eacf62803dd1f1bb5342d8eb5951546c371915e06223f589c0c95486c7c769. +// +// Solidity: event ReservationRouterSet(address reservationRouter) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationRouterSet(log types.Log) (*ReservationRouterReservationRouterSet, error) { + event := new(ReservationRouterReservationRouterSet) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationRouterSet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationStrandedIterator is returned from FilterReservationStranded and is used to iterate over the raw logs and unpacked data for ReservationStranded events raised by the ReservationRouter contract. +type ReservationRouterReservationStrandedIterator struct { + Event *ReservationRouterReservationStranded // 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 *ReservationRouterReservationStrandedIterator) 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(ReservationRouterReservationStranded) + 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(ReservationRouterReservationStranded) + 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 *ReservationRouterReservationStrandedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationStrandedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationStranded represents a ReservationStranded event raised by the ReservationRouter contract. +type ReservationRouterReservationStranded struct { + ReservationKey *big.Int + WalletPubKeyHash [20]byte + Owner common.Address + AnchorAmount uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationStranded is a free log retrieval operation binding the contract event 0x95a304fee209cd8169392534093ab2cb9ee3b8c7031cf873b2f0c40a03b44d4d. +// +// Solidity: event ReservationStranded(uint256 indexed reservationKey, bytes20 indexed walletPubKeyHash, address indexed owner, uint64 anchorAmount) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationStranded(opts *bind.FilterOpts, reservationKey []*big.Int, walletPubKeyHash [][20]byte, owner []common.Address) (*ReservationRouterReservationStrandedIterator, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationStranded", reservationKeyRule, walletPubKeyHashRule, ownerRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservationStrandedIterator{contract: _ReservationRouter.contract, event: "ReservationStranded", logs: logs, sub: sub}, nil +} + +// WatchReservationStranded is a free log subscription operation binding the contract event 0x95a304fee209cd8169392534093ab2cb9ee3b8c7031cf873b2f0c40a03b44d4d. +// +// Solidity: event ReservationStranded(uint256 indexed reservationKey, bytes20 indexed walletPubKeyHash, address indexed owner, uint64 anchorAmount) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationStranded(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationStranded, reservationKey []*big.Int, walletPubKeyHash [][20]byte, owner []common.Address) (event.Subscription, error) { + + var reservationKeyRule []interface{} + for _, reservationKeyItem := range reservationKey { + reservationKeyRule = append(reservationKeyRule, reservationKeyItem) + } + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationStranded", reservationKeyRule, walletPubKeyHashRule, ownerRule) + 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(ReservationRouterReservationStranded) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationStranded", 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 +} + +// ParseReservationStranded is a log parse operation binding the contract event 0x95a304fee209cd8169392534093ab2cb9ee3b8c7031cf873b2f0c40a03b44d4d. +// +// Solidity: event ReservationStranded(uint256 indexed reservationKey, bytes20 indexed walletPubKeyHash, address indexed owner, uint64 anchorAmount) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationStranded(log types.Log) (*ReservationRouterReservationStranded, error) { + event := new(ReservationRouterReservationStranded) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationStranded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservationVaultUpdatedIterator is returned from FilterReservationVaultUpdated and is used to iterate over the raw logs and unpacked data for ReservationVaultUpdated events raised by the ReservationRouter contract. +type ReservationRouterReservationVaultUpdatedIterator struct { + Event *ReservationRouterReservationVaultUpdated // 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 *ReservationRouterReservationVaultUpdatedIterator) 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(ReservationRouterReservationVaultUpdated) + 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(ReservationRouterReservationVaultUpdated) + 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 *ReservationRouterReservationVaultUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservationVaultUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservationVaultUpdated represents a ReservationVaultUpdated event raised by the ReservationRouter contract. +type ReservationRouterReservationVaultUpdated struct { + ReservationVault common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservationVaultUpdated is a free log retrieval operation binding the contract event 0x81b37784221191020714846b5fdcdfcbde796cfad2627d47dc81c2b7765b1910. +// +// Solidity: event ReservationVaultUpdated(address reservationVault) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservationVaultUpdated(opts *bind.FilterOpts) (*ReservationRouterReservationVaultUpdatedIterator, error) { + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservationVaultUpdated") + if err != nil { + return nil, err + } + return &ReservationRouterReservationVaultUpdatedIterator{contract: _ReservationRouter.contract, event: "ReservationVaultUpdated", logs: logs, sub: sub}, nil +} + +// WatchReservationVaultUpdated is a free log subscription operation binding the contract event 0x81b37784221191020714846b5fdcdfcbde796cfad2627d47dc81c2b7765b1910. +// +// Solidity: event ReservationVaultUpdated(address reservationVault) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservationVaultUpdated(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservationVaultUpdated) (event.Subscription, error) { + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservationVaultUpdated") + 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(ReservationRouterReservationVaultUpdated) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationVaultUpdated", 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 +} + +// ParseReservationVaultUpdated is a log parse operation binding the contract event 0x81b37784221191020714846b5fdcdfcbde796cfad2627d47dc81c2b7765b1910. +// +// Solidity: event ReservationVaultUpdated(address reservationVault) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservationVaultUpdated(log types.Log) (*ReservationRouterReservationVaultUpdated, error) { + event := new(ReservationRouterReservationVaultUpdated) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservationVaultUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// ReservationRouterReservedDepositMarkedStaleIterator is returned from FilterReservedDepositMarkedStale and is used to iterate over the raw logs and unpacked data for ReservedDepositMarkedStale events raised by the ReservationRouter contract. +type ReservationRouterReservedDepositMarkedStaleIterator struct { + Event *ReservationRouterReservedDepositMarkedStale // 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 *ReservationRouterReservedDepositMarkedStaleIterator) 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(ReservationRouterReservedDepositMarkedStale) + 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(ReservationRouterReservedDepositMarkedStale) + 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 *ReservationRouterReservedDepositMarkedStaleIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ReservationRouterReservedDepositMarkedStaleIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ReservationRouterReservedDepositMarkedStale represents a ReservedDepositMarkedStale event raised by the ReservationRouter contract. +type ReservationRouterReservedDepositMarkedStale struct { + DepositKey *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReservedDepositMarkedStale is a free log retrieval operation binding the contract event 0xf4a10f7395c3a5e8165714c665ddb7f2e320f8bc2a21d9f8722f48f8d1d71eab. +// +// Solidity: event ReservedDepositMarkedStale(uint256 indexed depositKey) +func (_ReservationRouter *ReservationRouterFilterer) FilterReservedDepositMarkedStale(opts *bind.FilterOpts, depositKey []*big.Int) (*ReservationRouterReservedDepositMarkedStaleIterator, error) { + + var depositKeyRule []interface{} + for _, depositKeyItem := range depositKey { + depositKeyRule = append(depositKeyRule, depositKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.FilterLogs(opts, "ReservedDepositMarkedStale", depositKeyRule) + if err != nil { + return nil, err + } + return &ReservationRouterReservedDepositMarkedStaleIterator{contract: _ReservationRouter.contract, event: "ReservedDepositMarkedStale", logs: logs, sub: sub}, nil +} + +// WatchReservedDepositMarkedStale is a free log subscription operation binding the contract event 0xf4a10f7395c3a5e8165714c665ddb7f2e320f8bc2a21d9f8722f48f8d1d71eab. +// +// Solidity: event ReservedDepositMarkedStale(uint256 indexed depositKey) +func (_ReservationRouter *ReservationRouterFilterer) WatchReservedDepositMarkedStale(opts *bind.WatchOpts, sink chan<- *ReservationRouterReservedDepositMarkedStale, depositKey []*big.Int) (event.Subscription, error) { + + var depositKeyRule []interface{} + for _, depositKeyItem := range depositKey { + depositKeyRule = append(depositKeyRule, depositKeyItem) + } + + logs, sub, err := _ReservationRouter.contract.WatchLogs(opts, "ReservedDepositMarkedStale", depositKeyRule) + 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(ReservationRouterReservedDepositMarkedStale) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservedDepositMarkedStale", 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 +} + +// ParseReservedDepositMarkedStale is a log parse operation binding the contract event 0xf4a10f7395c3a5e8165714c665ddb7f2e320f8bc2a21d9f8722f48f8d1d71eab. +// +// Solidity: event ReservedDepositMarkedStale(uint256 indexed depositKey) +func (_ReservationRouter *ReservationRouterFilterer) ParseReservedDepositMarkedStale(log types.Log) (*ReservationRouterReservedDepositMarkedStale, error) { + event := new(ReservationRouterReservedDepositMarkedStale) + if err := _ReservationRouter.contract.UnpackLog(event, "ReservedDepositMarkedStale", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go b/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go index ed86d98785..c3197e49c0 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go @@ -95,9 +95,24 @@ type WalletProposalValidatorRedemptionProposal struct { RedemptionTxFee *big.Int } +// WalletProposalValidatorReservationAnchorProposal is an auto generated low-level Go binding around an user-defined struct. +type WalletProposalValidatorReservationAnchorProposal struct { + WalletPubKeyHash [20]byte + DepositKey WalletProposalValidatorDepositKey + AnchorTxFee *big.Int +} + +// WalletProposalValidatorReservationReanchorProposal is an auto generated low-level Go binding around an user-defined struct. +type WalletProposalValidatorReservationReanchorProposal struct { + SourceWalletPubKeyHash [20]byte + ReservationKey *big.Int + TargetWalletPubKeyHash [20]byte + ReanchorTxFee *big.Int +} + // WalletProposalValidatorMetaData contains all meta data concerning the WalletProposalValidator contract. var WalletProposalValidatorMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"contractBridge\",\"name\":\"_bridge\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"DEPOSIT_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_REFUND_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_SWEEP_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bridge\",\"outputs\":[{\"internalType\":\"contractBridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"}],\"internalType\":\"structWalletProposalValidator.DepositKey[]\",\"name\":\"depositsKeys\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"sweepTxFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256[]\",\"name\":\"depositsRevealBlocks\",\"type\":\"uint256[]\"}],\"internalType\":\"structWalletProposalValidator.DepositSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"}],\"internalType\":\"structWalletProposalValidator.DepositExtraInfo[]\",\"name\":\"depositsExtraInfo\",\"type\":\"tuple[]\"}],\"name\":\"validateDepositSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"structWalletProposalValidator.HeartbeatProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateHeartbeatProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"movedFundsSweepTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovedFundsSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateMovedFundsSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"internalType\":\"uint256\",\"name\":\"movingFundsTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovingFundsProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"validateMovingFundsProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes[]\",\"name\":\"redeemersOutputScripts\",\"type\":\"bytes[]\"},{\"internalType\":\"uint256\",\"name\":\"redemptionTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.RedemptionProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateRedemptionProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[{\"internalType\":\"contractBridge\",\"name\":\"_bridge\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"DEPOSIT_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_REFUND_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_SWEEP_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bridge\",\"outputs\":[{\"internalType\":\"contractBridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"}],\"internalType\":\"structWalletProposalValidator.DepositKey[]\",\"name\":\"depositsKeys\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"sweepTxFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256[]\",\"name\":\"depositsRevealBlocks\",\"type\":\"uint256[]\"}],\"internalType\":\"structWalletProposalValidator.DepositSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"}],\"internalType\":\"structWalletProposalValidator.DepositExtraInfo[]\",\"name\":\"depositsExtraInfo\",\"type\":\"tuple[]\"}],\"name\":\"validateDepositSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"structWalletProposalValidator.HeartbeatProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateHeartbeatProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"movedFundsSweepTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovedFundsSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateMovedFundsSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"internalType\":\"uint256\",\"name\":\"movingFundsTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovingFundsProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"validateMovingFundsProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes[]\",\"name\":\"redeemersOutputScripts\",\"type\":\"bytes[]\"},{\"internalType\":\"uint256\",\"name\":\"redemptionTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.RedemptionProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateRedemptionProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"}],\"internalType\":\"structWalletProposalValidator.DepositKey\",\"name\":\"depositKey\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"anchorTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.ReservationAnchorProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"}],\"internalType\":\"structWalletProposalValidator.DepositExtraInfo\",\"name\":\"depositExtraInfo\",\"type\":\"tuple\"}],\"name\":\"validateReservationAnchorProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"sourceWalletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint256\",\"name\":\"reservationKey\",\"type\":\"uint256\"},{\"internalType\":\"bytes20\",\"name\":\"targetWalletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint256\",\"name\":\"reanchorTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.ReservationReanchorProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateReservationReanchorProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", } // WalletProposalValidatorABI is the input ABI used to generate the binding from. @@ -617,3 +632,65 @@ func (_WalletProposalValidator *WalletProposalValidatorSession) ValidateRedempti func (_WalletProposalValidator *WalletProposalValidatorCallerSession) ValidateRedemptionProposal(proposal WalletProposalValidatorRedemptionProposal) (bool, error) { return _WalletProposalValidator.Contract.ValidateRedemptionProposal(&_WalletProposalValidator.CallOpts, proposal) } + +// ValidateReservationAnchorProposal is a free data retrieval call binding the contract method 0xddded0a4. +// +// Solidity: function validateReservationAnchorProposal((bytes20,(bytes32,uint32),uint256) proposal, ((bytes4,bytes,bytes,bytes4),bytes8,bytes20,bytes20,bytes4) depositExtraInfo) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorCaller) ValidateReservationAnchorProposal(opts *bind.CallOpts, proposal WalletProposalValidatorReservationAnchorProposal, depositExtraInfo WalletProposalValidatorDepositExtraInfo) (bool, error) { + var out []interface{} + err := _WalletProposalValidator.contract.Call(opts, &out, "validateReservationAnchorProposal", proposal, depositExtraInfo) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ValidateReservationAnchorProposal is a free data retrieval call binding the contract method 0xddded0a4. +// +// Solidity: function validateReservationAnchorProposal((bytes20,(bytes32,uint32),uint256) proposal, ((bytes4,bytes,bytes,bytes4),bytes8,bytes20,bytes20,bytes4) depositExtraInfo) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorSession) ValidateReservationAnchorProposal(proposal WalletProposalValidatorReservationAnchorProposal, depositExtraInfo WalletProposalValidatorDepositExtraInfo) (bool, error) { + return _WalletProposalValidator.Contract.ValidateReservationAnchorProposal(&_WalletProposalValidator.CallOpts, proposal, depositExtraInfo) +} + +// ValidateReservationAnchorProposal is a free data retrieval call binding the contract method 0xddded0a4. +// +// Solidity: function validateReservationAnchorProposal((bytes20,(bytes32,uint32),uint256) proposal, ((bytes4,bytes,bytes,bytes4),bytes8,bytes20,bytes20,bytes4) depositExtraInfo) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorCallerSession) ValidateReservationAnchorProposal(proposal WalletProposalValidatorReservationAnchorProposal, depositExtraInfo WalletProposalValidatorDepositExtraInfo) (bool, error) { + return _WalletProposalValidator.Contract.ValidateReservationAnchorProposal(&_WalletProposalValidator.CallOpts, proposal, depositExtraInfo) +} + +// ValidateReservationReanchorProposal is a free data retrieval call binding the contract method 0x97dd538f. +// +// Solidity: function validateReservationReanchorProposal((bytes20,uint256,bytes20,uint256) proposal) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorCaller) ValidateReservationReanchorProposal(opts *bind.CallOpts, proposal WalletProposalValidatorReservationReanchorProposal) (bool, error) { + var out []interface{} + err := _WalletProposalValidator.contract.Call(opts, &out, "validateReservationReanchorProposal", proposal) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ValidateReservationReanchorProposal is a free data retrieval call binding the contract method 0x97dd538f. +// +// Solidity: function validateReservationReanchorProposal((bytes20,uint256,bytes20,uint256) proposal) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorSession) ValidateReservationReanchorProposal(proposal WalletProposalValidatorReservationReanchorProposal) (bool, error) { + return _WalletProposalValidator.Contract.ValidateReservationReanchorProposal(&_WalletProposalValidator.CallOpts, proposal) +} + +// ValidateReservationReanchorProposal is a free data retrieval call binding the contract method 0x97dd538f. +// +// Solidity: function validateReservationReanchorProposal((bytes20,uint256,bytes20,uint256) proposal) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorCallerSession) ValidateReservationReanchorProposal(proposal WalletProposalValidatorReservationReanchorProposal) (bool, error) { + return _WalletProposalValidator.Contract.ValidateReservationReanchorProposal(&_WalletProposalValidator.CallOpts, proposal) +} diff --git a/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go b/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go index f7a5944669..fe214dfed0 100644 --- a/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go @@ -58,8 +58,11 @@ func init() { bDepositsCommand(), bFraudChallengesCommand(), bFraudParametersCommand(), + bGetRebateStakingCommand(), bGetRedemptionWatchtowerCommand(), + bGetReservationRouterCommand(), bGovernanceCommand(), + bIsReservedDepositCommand(), bIsVaultTrustedCommand(), bLiveWalletsCountCommand(), bMovedFundsSweepRequestsCommand(), @@ -77,6 +80,8 @@ func init() { bEcdsaWalletCreatedCallbackCommand(), bEcdsaWalletHeartbeatFailedCallbackCommand(), bInitializeCommand(), + bInitializeV2FixVaultZeroDepositCommand(), + bInitializeV5RepairRebateStakingCommand(), bNotifyMovingFundsBelowDustCommand(), bNotifyRedemptionVetoCommand(), bNotifyWalletCloseableCommand(), @@ -87,7 +92,9 @@ func init() { bResetMovingFundsTimeoutCommand(), bRevealDepositCommand(), bRevealDepositWithExtraDataCommand(), + bSetRebateStakingCommand(), bSetRedemptionWatchtowerCommand(), + bSetReservationRouterCommand(), bSetSpvMaintainerStatusCommand(), bSetVaultStatusCommand(), bSubmitDepositSweepProofCommand(), @@ -331,6 +338,40 @@ func bFraudParameters(c *cobra.Command, args []string) error { return nil } +func bGetRebateStakingCommand() *cobra.Command { + c := &cobra.Command{ + Use: "get-rebate-staking", + Short: "Calls the view method getRebateStaking on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bGetRebateStaking, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bGetRebateStaking(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + result, err := contract.GetRebateStakingAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bGetRedemptionWatchtowerCommand() *cobra.Command { c := &cobra.Command{ Use: "get-redemption-watchtower", @@ -365,6 +406,40 @@ func bGetRedemptionWatchtower(c *cobra.Command, args []string) error { return nil } +func bGetReservationRouterCommand() *cobra.Command { + c := &cobra.Command{ + Use: "get-reservation-router", + Short: "Calls the view method getReservationRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bGetReservationRouter, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bGetReservationRouter(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + result, err := contract.GetReservationRouterAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bGovernanceCommand() *cobra.Command { c := &cobra.Command{ Use: "governance", @@ -399,6 +474,49 @@ func bGovernance(c *cobra.Command, args []string) error { return nil } +func bIsReservedDepositCommand() *cobra.Command { + c := &cobra.Command{ + Use: "is-reserved-deposit [arg_depositKey]", + Short: "Calls the view method isReservedDeposit on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bIsReservedDeposit, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bIsReservedDeposit(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_depositKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_depositKey, a uint256, from passed value %v", + args[0], + ) + } + + result, err := contract.IsReservedDepositAtBlock( + arg_depositKey, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bIsVaultTrustedCommand() *cobra.Command { c := &cobra.Command{ Use: "is-vault-trusted [arg_vault]", @@ -1296,6 +1414,125 @@ func bInitialize(c *cobra.Command, args []string) error { return nil } +func bInitializeV2FixVaultZeroDepositCommand() *cobra.Command { + c := &cobra.Command{ + Use: "initialize-v2-fix-vault-zero-deposit", + Short: "Calls the nonpayable method initializeV2FixVaultZeroDeposit on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bInitializeV2FixVaultZeroDeposit, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bInitializeV2FixVaultZeroDeposit(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.InitializeV2FixVaultZeroDeposit() + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallInitializeV2FixVaultZeroDeposit( + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func bInitializeV5RepairRebateStakingCommand() *cobra.Command { + c := &cobra.Command{ + Use: "initialize-v5-repair-rebate-staking [arg_newRebateStaking]", + Short: "Calls the nonpayable method initializeV5RepairRebateStaking on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bInitializeV5RepairRebateStaking, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bInitializeV5RepairRebateStaking(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_newRebateStaking, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_newRebateStaking, a address, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.InitializeV5RepairRebateStaking( + arg_newRebateStaking, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallInitializeV5RepairRebateStaking( + arg_newRebateStaking, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + func bNotifyMovingFundsBelowDustCommand() *cobra.Command { c := &cobra.Command{ Use: "notify-moving-funds-below-dust [arg_walletPubKeyHash] [arg_mainUtxo_json]", @@ -2026,6 +2263,71 @@ func bRevealDepositWithExtraData(c *cobra.Command, args []string) error { return nil } +func bSetRebateStakingCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-rebate-staking [arg_rebateStaking]", + Short: "Calls the nonpayable method setRebateStaking on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetRebateStaking, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetRebateStaking(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_rebateStaking, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_rebateStaking, a address, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetRebateStaking( + arg_rebateStaking, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetRebateStaking( + arg_rebateStaking, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + func bSetRedemptionWatchtowerCommand() *cobra.Command { c := &cobra.Command{ Use: "set-redemption-watchtower [arg_redemptionWatchtower]", @@ -2091,6 +2393,71 @@ func bSetRedemptionWatchtower(c *cobra.Command, args []string) error { return nil } +func bSetReservationRouterCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-reservation-router [arg__reservationRouter]", + Short: "Calls the nonpayable method setReservationRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetReservationRouter, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetReservationRouter(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg__reservationRouter, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg__reservationRouter, a address, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetReservationRouter( + arg__reservationRouter, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetReservationRouter( + arg__reservationRouter, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + func bSetSpvMaintainerStatusCommand() *cobra.Command { c := &cobra.Command{ Use: "set-spv-maintainer-status [arg_spvMaintainer] [arg_isTrusted]", diff --git a/pkg/chain/ethereum/tbtc/gen/cmd/RedemptionWatchtower.go b/pkg/chain/ethereum/tbtc/gen/cmd/RedemptionWatchtower.go index ccbddec189..cb5578a1b1 100644 --- a/pkg/chain/ethereum/tbtc/gen/cmd/RedemptionWatchtower.go +++ b/pkg/chain/ethereum/tbtc/gen/cmd/RedemptionWatchtower.go @@ -61,6 +61,7 @@ func init() { rwManagerCommand(), rwObjectionsCommand(), rwOwnerCommand(), + rwREQUIREDOBJECTIONSCOUNTCommand(), rwVetoFreezePeriodCommand(), rwVetoPenaltyFeeDivisorCommand(), rwVetoProposalsCommand(), @@ -562,6 +563,40 @@ func rwOwner(c *cobra.Command, args []string) error { return nil } +func rwREQUIREDOBJECTIONSCOUNTCommand() *cobra.Command { + c := &cobra.Command{ + Use: "r-e-q-u-i-r-e-d-o-b-j-e-c-t-i-o-n-s-c-o-u-n-t", + Short: "Calls the view method rEQUIREDOBJECTIONSCOUNT on the RedemptionWatchtower contract.", + Args: cmd.ArgCountChecker(0), + RunE: rwREQUIREDOBJECTIONSCOUNT, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rwREQUIREDOBJECTIONSCOUNT(c *cobra.Command, args []string) error { + contract, err := initializeRedemptionWatchtower(c) + if err != nil { + return err + } + + result, err := contract.REQUIREDOBJECTIONSCOUNTAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func rwVetoFreezePeriodCommand() *cobra.Command { c := &cobra.Command{ Use: "veto-freeze-period", diff --git a/pkg/chain/ethereum/tbtc/gen/cmd/ReservationRouter.go b/pkg/chain/ethereum/tbtc/gen/cmd/ReservationRouter.go new file mode 100644 index 0000000000..8c23881c8e --- /dev/null +++ b/pkg/chain/ethereum/tbtc/gen/cmd/ReservationRouter.go @@ -0,0 +1,1331 @@ +// Code generated - DO NOT EDIT. +// This file is a generated command and any manual changes will be lost. + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + + chainutil "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" + "github.com/keep-network/keep-common/pkg/cmd" + "github.com/keep-network/keep-common/pkg/utils/decode" + "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi" + "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/contract" + + "github.com/spf13/cobra" +) + +var ReservationRouterCommand *cobra.Command + +var reservationRouterDescription = `The reservation-router command allows calling the ReservationRouter contract on an + Ethereum network. It has subcommands corresponding to each contract method, + which respectively each take parameters based on the contract method's + parameters. + + Subcommands will submit a non-mutating call to the network and output the + result. + + All subcommands can be called against a specific block by passing the + -b/--block flag. + + Subcommands for mutating methods may be submitted as a mutating transaction + by passing the -s/--submit flag. In this mode, this command will terminate + successfully once the transaction has been submitted, but will not wait for + the transaction to be included in a block. They return the transaction hash. + + Calls that require ether to be paid will get 0 ether by default, which can + be changed by passing the -v/--value flag.` + +func init() { + ReservationRouterCommand := &cobra.Command{ + Use: "reservation-router", + Short: `Provides access to the ReservationRouter contract.`, + Long: reservationRouterDescription, + } + + ReservationRouterCommand.AddCommand( + rrActiveReservationsCountCommand(), + rrGovernanceCommand(), + rrPendingReservedDepositsCommand(), + rrReservationActionsCommand(), + rrReservationByAnchorUtxoCommand(), + rrReservationCapsCommand(), + rrReservationParametersCommand(), + rrReservationRouterCommand(), + rrReservationsCommand(), + rrReservedDepositWalletCommand(), + rrWalletReservationsCommand(), + rrWalletReservationsAmountCommand(), + rrWalletReservationsCountCommand(), + rrNotifyReservationStrandedCommand(), + rrNotifyStaleReservedDepositCommand(), + rrRequestReservationAcceptanceCommand(), + rrRequestReservationReanchorCommand(), + rrSubmitReservationProofCommand(), + rrTransferGovernanceCommand(), + rrUpdateReservationCapsCommand(), + rrUpdateReservationParametersCommand(), + ) + + ModuleCommand.AddCommand(ReservationRouterCommand) +} + +/// ------------------- Const methods ------------------- + +func rrActiveReservationsCountCommand() *cobra.Command { + c := &cobra.Command{ + Use: "active-reservations-count", + Short: "Calls the view method activeReservationsCount on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(0), + RunE: rrActiveReservationsCount, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrActiveReservationsCount(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + result, err := contract.ActiveReservationsCountAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrGovernanceCommand() *cobra.Command { + c := &cobra.Command{ + Use: "governance", + Short: "Calls the view method governance on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(0), + RunE: rrGovernance, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrGovernance(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + result, err := contract.GovernanceAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrPendingReservedDepositsCommand() *cobra.Command { + c := &cobra.Command{ + Use: "pending-reserved-deposits", + Short: "Calls the view method pendingReservedDeposits on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(0), + RunE: rrPendingReservedDeposits, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrPendingReservedDeposits(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + result, err := contract.PendingReservedDepositsAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservationActionsCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reservation-actions [arg_reservationKey] [arg_requestNonce]", + Short: "Calls the view method reservationActions on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(2), + RunE: rrReservationActions, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservationActions(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_reservationKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationKey, a uint256, from passed value %v", + args[0], + ) + } + arg_requestNonce, err := decode.ParseUint[uint64](args[1], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_requestNonce, a uint64, from passed value %v", + args[1], + ) + } + + result, err := contract.ReservationActionsAtBlock( + arg_reservationKey, + arg_requestNonce, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservationByAnchorUtxoCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reservation-by-anchor-utxo [arg_anchorTxHash] [arg_anchorTxOutputIndex]", + Short: "Calls the view method reservationByAnchorUtxo on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(2), + RunE: rrReservationByAnchorUtxo, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservationByAnchorUtxo(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_anchorTxHash, err := decode.ParseBytes32(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_anchorTxHash, a bytes32, from passed value %v", + args[0], + ) + } + arg_anchorTxOutputIndex, err := decode.ParseUint[uint32](args[1], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_anchorTxOutputIndex, a uint32, from passed value %v", + args[1], + ) + } + + result, err := contract.ReservationByAnchorUtxoAtBlock( + arg_anchorTxHash, + arg_anchorTxOutputIndex, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservationCapsCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reservation-caps", + Short: "Calls the view method reservationCaps on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(0), + RunE: rrReservationCaps, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservationCaps(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + result, err := contract.ReservationCapsAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservationParametersCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reservation-parameters", + Short: "Calls the view method reservationParameters on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(0), + RunE: rrReservationParameters, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservationParameters(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + result, err := contract.ReservationParametersAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservationRouterCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reservation-router", + Short: "Calls the view method reservationRouter on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(0), + RunE: rrReservationRouter, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservationRouter(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + result, err := contract.ReservationRouterAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservationsCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reservations [arg_reservationKey]", + Short: "Calls the view method reservations on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrReservations, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservations(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_reservationKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationKey, a uint256, from passed value %v", + args[0], + ) + } + + result, err := contract.ReservationsAtBlock( + arg_reservationKey, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrReservedDepositWalletCommand() *cobra.Command { + c := &cobra.Command{ + Use: "reserved-deposit-wallet [arg_depositKey]", + Short: "Calls the view method reservedDepositWallet on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrReservedDepositWallet, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrReservedDepositWallet(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_depositKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_depositKey, a uint256, from passed value %v", + args[0], + ) + } + + result, err := contract.ReservedDepositWalletAtBlock( + arg_depositKey, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrWalletReservationsCommand() *cobra.Command { + c := &cobra.Command{ + Use: "wallet-reservations [arg_walletPubKeyHash]", + Short: "Calls the view method walletReservations on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrWalletReservations, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrWalletReservations(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_walletPubKeyHash, err := decode.ParseBytes20(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletPubKeyHash, a bytes20, from passed value %v", + args[0], + ) + } + + result, err := contract.WalletReservationsAtBlock( + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrWalletReservationsAmountCommand() *cobra.Command { + c := &cobra.Command{ + Use: "wallet-reservations-amount [arg_walletPubKeyHash]", + Short: "Calls the view method walletReservationsAmount on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrWalletReservationsAmount, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrWalletReservationsAmount(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_walletPubKeyHash, err := decode.ParseBytes20(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletPubKeyHash, a bytes20, from passed value %v", + args[0], + ) + } + + result, err := contract.WalletReservationsAmountAtBlock( + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func rrWalletReservationsCountCommand() *cobra.Command { + c := &cobra.Command{ + Use: "wallet-reservations-count [arg_walletPubKeyHash]", + Short: "Calls the view method walletReservationsCount on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrWalletReservationsCount, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func rrWalletReservationsCount(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_walletPubKeyHash, err := decode.ParseBytes20(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletPubKeyHash, a bytes20, from passed value %v", + args[0], + ) + } + + result, err := contract.WalletReservationsCountAtBlock( + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +/// ------------------- Non-const methods ------------------- + +func rrNotifyReservationStrandedCommand() *cobra.Command { + c := &cobra.Command{ + Use: "notify-reservation-stranded [arg_reservationKey]", + Short: "Calls the nonpayable method notifyReservationStranded on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrNotifyReservationStranded, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrNotifyReservationStranded(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_reservationKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationKey, a uint256, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.NotifyReservationStranded( + arg_reservationKey, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallNotifyReservationStranded( + arg_reservationKey, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrNotifyStaleReservedDepositCommand() *cobra.Command { + c := &cobra.Command{ + Use: "notify-stale-reserved-deposit [arg_depositKey]", + Short: "Calls the nonpayable method notifyStaleReservedDeposit on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrNotifyStaleReservedDeposit, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrNotifyStaleReservedDeposit(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_depositKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_depositKey, a uint256, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.NotifyStaleReservedDeposit( + arg_depositKey, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallNotifyStaleReservedDeposit( + arg_depositKey, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrRequestReservationAcceptanceCommand() *cobra.Command { + c := &cobra.Command{ + Use: "request-reservation-acceptance [arg_reservationKey] [arg_walletPubKeyHash]", + Short: "Calls the nonpayable method requestReservationAcceptance on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(2), + RunE: rrRequestReservationAcceptance, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrRequestReservationAcceptance(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_reservationKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationKey, a uint256, from passed value %v", + args[0], + ) + } + arg_walletPubKeyHash, err := decode.ParseBytes20(args[1]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletPubKeyHash, a bytes20, from passed value %v", + args[1], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.RequestReservationAcceptance( + arg_reservationKey, + arg_walletPubKeyHash, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallRequestReservationAcceptance( + arg_reservationKey, + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrRequestReservationReanchorCommand() *cobra.Command { + c := &cobra.Command{ + Use: "request-reservation-reanchor [arg_reservationKey] [arg_targetWalletPubKeyHash]", + Short: "Calls the nonpayable method requestReservationReanchor on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(2), + RunE: rrRequestReservationReanchor, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrRequestReservationReanchor(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_reservationKey, err := hexutil.DecodeBig(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationKey, a uint256, from passed value %v", + args[0], + ) + } + arg_targetWalletPubKeyHash, err := decode.ParseBytes20(args[1]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_targetWalletPubKeyHash, a bytes20, from passed value %v", + args[1], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.RequestReservationReanchor( + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallRequestReservationReanchor( + arg_reservationKey, + arg_targetWalletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrSubmitReservationProofCommand() *cobra.Command { + c := &cobra.Command{ + Use: "submit-reservation-proof [arg_proofType] [arg_txInfo_json] [arg_proof_json] [arg_mainUtxo_json] [arg_reservationKey] [arg_requestNonce]", + Short: "Calls the nonpayable method submitReservationProof on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(6), + RunE: rrSubmitReservationProof, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrSubmitReservationProof(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_proofType, err := decode.ParseUint[uint8](args[0], 8) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_proofType, a uint8, from passed value %v", + args[0], + ) + } + + arg_txInfo_json := abi.BitcoinTxInfo4{} + if err := json.Unmarshal([]byte(args[1]), &arg_txInfo_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_txInfo_json to abi.BitcoinTxInfo4: %w", err) + } + + arg_proof_json := abi.BitcoinTxProof3{} + if err := json.Unmarshal([]byte(args[2]), &arg_proof_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_proof_json to abi.BitcoinTxProof3: %w", err) + } + + arg_mainUtxo_json := abi.BitcoinTxUTXO4{} + if err := json.Unmarshal([]byte(args[3]), &arg_mainUtxo_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_mainUtxo_json to abi.BitcoinTxUTXO4: %w", err) + } + arg_reservationKey, err := hexutil.DecodeBig(args[4]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationKey, a uint256, from passed value %v", + args[4], + ) + } + arg_requestNonce, err := decode.ParseUint[uint64](args[5], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_requestNonce, a uint64, from passed value %v", + args[5], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SubmitReservationProof( + arg_proofType, + arg_txInfo_json, + arg_proof_json, + arg_mainUtxo_json, + arg_reservationKey, + arg_requestNonce, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSubmitReservationProof( + arg_proofType, + arg_txInfo_json, + arg_proof_json, + arg_mainUtxo_json, + arg_reservationKey, + arg_requestNonce, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrTransferGovernanceCommand() *cobra.Command { + c := &cobra.Command{ + Use: "transfer-governance [arg_newGovernance]", + Short: "Calls the nonpayable method transferGovernance on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(1), + RunE: rrTransferGovernance, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrTransferGovernance(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_newGovernance, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_newGovernance, a address, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.TransferGovernance( + arg_newGovernance, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallTransferGovernance( + arg_newGovernance, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrUpdateReservationCapsCommand() *cobra.Command { + c := &cobra.Command{ + Use: "update-reservation-caps [arg_maxReservationsAmountPerWallet] [arg_reservationMaxSingleAmount] [arg_maxActiveReservations]", + Short: "Calls the nonpayable method updateReservationCaps on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(3), + RunE: rrUpdateReservationCaps, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrUpdateReservationCaps(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_maxReservationsAmountPerWallet, err := decode.ParseUint[uint64](args[0], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_maxReservationsAmountPerWallet, a uint64, from passed value %v", + args[0], + ) + } + arg_reservationMaxSingleAmount, err := decode.ParseUint[uint64](args[1], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationMaxSingleAmount, a uint64, from passed value %v", + args[1], + ) + } + arg_maxActiveReservations, err := decode.ParseUint[uint32](args[2], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_maxActiveReservations, a uint32, from passed value %v", + args[2], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.UpdateReservationCaps( + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallUpdateReservationCaps( + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +func rrUpdateReservationParametersCommand() *cobra.Command { + c := &cobra.Command{ + Use: "update-reservation-parameters [arg_reservationVault] [arg_reservationMinAmount] [arg_reservationTxMaxFee] [arg_reservationTermSeconds] [arg_reservationDissolutionDelay] [arg_reservationMaxTotalAmount] [arg_maxReservationsPerWallet] [arg_reservationActionTimeout] [arg_reservationRenewalWindowSeconds]", + Short: "Calls the nonpayable method updateReservationParameters on the ReservationRouter contract.", + Args: cmd.ArgCountChecker(9), + RunE: rrUpdateReservationParameters, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func rrUpdateReservationParameters(c *cobra.Command, args []string) error { + contract, err := initializeReservationRouter(c) + if err != nil { + return err + } + + arg_reservationVault, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationVault, a address, from passed value %v", + args[0], + ) + } + arg_reservationMinAmount, err := decode.ParseUint[uint64](args[1], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationMinAmount, a uint64, from passed value %v", + args[1], + ) + } + arg_reservationTxMaxFee, err := decode.ParseUint[uint64](args[2], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationTxMaxFee, a uint64, from passed value %v", + args[2], + ) + } + arg_reservationTermSeconds, err := decode.ParseUint[uint32](args[3], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationTermSeconds, a uint32, from passed value %v", + args[3], + ) + } + arg_reservationDissolutionDelay, err := decode.ParseUint[uint32](args[4], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationDissolutionDelay, a uint32, from passed value %v", + args[4], + ) + } + arg_reservationMaxTotalAmount, err := decode.ParseUint[uint64](args[5], 64) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationMaxTotalAmount, a uint64, from passed value %v", + args[5], + ) + } + arg_maxReservationsPerWallet, err := decode.ParseUint[uint32](args[6], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_maxReservationsPerWallet, a uint32, from passed value %v", + args[6], + ) + } + arg_reservationActionTimeout, err := decode.ParseUint[uint32](args[7], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationActionTimeout, a uint32, from passed value %v", + args[7], + ) + } + arg_reservationRenewalWindowSeconds, err := decode.ParseUint[uint32](args[8], 32) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_reservationRenewalWindowSeconds, a uint32, from passed value %v", + args[8], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.UpdateReservationParameters( + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallUpdateReservationParameters( + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + +/// ------------------- Initialization ------------------- + +func initializeReservationRouter(c *cobra.Command) (*contract.ReservationRouter, error) { + cfg := *ModuleCommand.GetConfig() + + client, err := ethclient.Dial(cfg.URL) + if err != nil { + return nil, fmt.Errorf("error connecting to host chain node: [%v]", err) + } + + chainID, err := client.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf( + "failed to resolve host chain id: [%v]", + err, + ) + } + + key, err := chainutil.DecryptKeyFile( + cfg.Account.KeyFile, + cfg.Account.KeyFilePassword, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to read KeyFile: %s: [%v]", + cfg.Account.KeyFile, + err, + ) + } + + miningWaiter := chainutil.NewMiningWaiter(client, cfg) + + blockCounter, err := chainutil.NewBlockCounter(client) + if err != nil { + return nil, fmt.Errorf( + "failed to create block counter: [%v]", + err, + ) + } + + address, err := cfg.ContractAddress("ReservationRouter") + if err != nil { + return nil, fmt.Errorf( + "failed to get %s address: [%w]", + "ReservationRouter", + err, + ) + } + + return contract.NewReservationRouter( + address, + chainID, + key, + client, + chainutil.NewNonceManager(client, key.Address), + miningWaiter, + blockCounter, + &sync.Mutex{}, + ) +} diff --git a/pkg/chain/ethereum/tbtc/gen/cmd/WalletProposalValidator.go b/pkg/chain/ethereum/tbtc/gen/cmd/WalletProposalValidator.go index d86d97458f..92932d206d 100644 --- a/pkg/chain/ethereum/tbtc/gen/cmd/WalletProposalValidator.go +++ b/pkg/chain/ethereum/tbtc/gen/cmd/WalletProposalValidator.go @@ -59,6 +59,8 @@ func init() { wpvValidateMovedFundsSweepProposalCommand(), wpvValidateMovingFundsProposalCommand(), wpvValidateRedemptionProposalCommand(), + wpvValidateReservationAnchorProposalCommand(), + wpvValidateReservationReanchorProposalCommand(), ) ModuleCommand.AddCommand(WalletProposalValidatorCommand) @@ -470,6 +472,92 @@ func wpvValidateRedemptionProposal(c *cobra.Command, args []string) error { return nil } +func wpvValidateReservationAnchorProposalCommand() *cobra.Command { + c := &cobra.Command{ + Use: "validate-reservation-anchor-proposal [arg_proposal_json] [arg_depositExtraInfo_json]", + Short: "Calls the view method validateReservationAnchorProposal on the WalletProposalValidator contract.", + Args: cmd.ArgCountChecker(2), + RunE: wpvValidateReservationAnchorProposal, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func wpvValidateReservationAnchorProposal(c *cobra.Command, args []string) error { + contract, err := initializeWalletProposalValidator(c) + if err != nil { + return err + } + + arg_proposal_json := abi.WalletProposalValidatorReservationAnchorProposal{} + if err := json.Unmarshal([]byte(args[0]), &arg_proposal_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_proposal_json to abi.WalletProposalValidatorReservationAnchorProposal: %w", err) + } + + arg_depositExtraInfo_json := abi.WalletProposalValidatorDepositExtraInfo{} + if err := json.Unmarshal([]byte(args[1]), &arg_depositExtraInfo_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_depositExtraInfo_json to abi.WalletProposalValidatorDepositExtraInfo: %w", err) + } + + result, err := contract.ValidateReservationAnchorProposalAtBlock( + arg_proposal_json, + arg_depositExtraInfo_json, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func wpvValidateReservationReanchorProposalCommand() *cobra.Command { + c := &cobra.Command{ + Use: "validate-reservation-reanchor-proposal [arg_proposal_json]", + Short: "Calls the view method validateReservationReanchorProposal on the WalletProposalValidator contract.", + Args: cmd.ArgCountChecker(1), + RunE: wpvValidateReservationReanchorProposal, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func wpvValidateReservationReanchorProposal(c *cobra.Command, args []string) error { + contract, err := initializeWalletProposalValidator(c) + if err != nil { + return err + } + + arg_proposal_json := abi.WalletProposalValidatorReservationReanchorProposal{} + if err := json.Unmarshal([]byte(args[0]), &arg_proposal_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_proposal_json to abi.WalletProposalValidatorReservationReanchorProposal: %w", err) + } + + result, err := contract.ValidateReservationReanchorProposalAtBlock( + arg_proposal_json, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + /// ------------------- Non-const methods ------------------- /// ------------------- Initialization ------------------- diff --git a/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go b/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go index ae73c92607..30a9b3faa3 100644 --- a/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go @@ -914,6 +914,268 @@ func (b *Bridge) InitializeGasEstimate( return result, err } +// Transaction submission. +func (b *Bridge) InitializeV2FixVaultZeroDeposit( + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction initializeV2FixVaultZeroDeposit", + ) + + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.InitializeV2FixVaultZeroDeposit( + transactorOptions, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "initializeV2FixVaultZeroDeposit", + ) + } + + bLogger.Infof( + "submitted transaction initializeV2FixVaultZeroDeposit with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go b.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := b.contract.InitializeV2FixVaultZeroDeposit( + newTransactorOptions, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "initializeV2FixVaultZeroDeposit", + ) + } + + bLogger.Infof( + "submitted transaction initializeV2FixVaultZeroDeposit with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + b.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallInitializeV2FixVaultZeroDeposit( + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "initializeV2FixVaultZeroDeposit", + &result, + ) + + return err +} + +func (b *Bridge) InitializeV2FixVaultZeroDepositGasEstimate() (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "initializeV2FixVaultZeroDeposit", + b.contractABI, + b.transactor, + ) + + return result, err +} + +// Transaction submission. +func (b *Bridge) InitializeV5RepairRebateStaking( + arg_newRebateStaking common.Address, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction initializeV5RepairRebateStaking", + " params: ", + fmt.Sprint( + arg_newRebateStaking, + ), + ) + + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.InitializeV5RepairRebateStaking( + transactorOptions, + arg_newRebateStaking, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "initializeV5RepairRebateStaking", + arg_newRebateStaking, + ) + } + + bLogger.Infof( + "submitted transaction initializeV5RepairRebateStaking with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go b.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := b.contract.InitializeV5RepairRebateStaking( + newTransactorOptions, + arg_newRebateStaking, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "initializeV5RepairRebateStaking", + arg_newRebateStaking, + ) + } + + bLogger.Infof( + "submitted transaction initializeV5RepairRebateStaking with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + b.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallInitializeV5RepairRebateStaking( + arg_newRebateStaking common.Address, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "initializeV5RepairRebateStaking", + &result, + arg_newRebateStaking, + ) + + return err +} + +func (b *Bridge) InitializeV5RepairRebateStakingGasEstimate( + arg_newRebateStaking common.Address, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "initializeV5RepairRebateStaking", + b.contractABI, + b.transactor, + arg_newRebateStaking, + ) + + return result, err +} + // Transaction submission. func (b *Bridge) NotifyFraudChallengeDefeatTimeout( arg_walletPublicKey []byte, @@ -3027,16 +3289,16 @@ func (b *Bridge) RevealDepositWithExtraDataGasEstimate( } // Transaction submission. -func (b *Bridge) SetRedemptionWatchtower( - arg_redemptionWatchtower common.Address, +func (b *Bridge) SetRebateStaking( + arg_rebateStaking common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction setRedemptionWatchtower", + "submitting transaction setRebateStaking", " params: ", fmt.Sprint( - arg_redemptionWatchtower, + arg_rebateStaking, ), ) @@ -3062,22 +3324,22 @@ func (b *Bridge) SetRedemptionWatchtower( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SetRedemptionWatchtower( + transaction, err := b.contract.SetRebateStaking( transactorOptions, - arg_redemptionWatchtower, + arg_rebateStaking, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setRedemptionWatchtower", - arg_redemptionWatchtower, + "setRebateStaking", + arg_rebateStaking, ) } bLogger.Infof( - "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", + "submitted transaction setRebateStaking with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3096,22 +3358,22 @@ func (b *Bridge) SetRedemptionWatchtower( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SetRedemptionWatchtower( + transaction, err := b.contract.SetRebateStaking( newTransactorOptions, - arg_redemptionWatchtower, + arg_rebateStaking, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setRedemptionWatchtower", - arg_redemptionWatchtower, + "setRebateStaking", + arg_rebateStaking, ) } bLogger.Infof( - "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", + "submitted transaction setRebateStaking with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3126,8 +3388,8 @@ func (b *Bridge) SetRedemptionWatchtower( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSetRedemptionWatchtower( - arg_redemptionWatchtower common.Address, +func (b *Bridge) CallSetRebateStaking( + arg_rebateStaking common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3139,40 +3401,316 @@ func (b *Bridge) CallSetRedemptionWatchtower( b.caller, b.errorResolver, b.contractAddress, - "setRedemptionWatchtower", + "setRebateStaking", &result, - arg_redemptionWatchtower, + arg_rebateStaking, ) return err } -func (b *Bridge) SetRedemptionWatchtowerGasEstimate( - arg_redemptionWatchtower common.Address, +func (b *Bridge) SetRebateStakingGasEstimate( + arg_rebateStaking common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "setRedemptionWatchtower", + "setRebateStaking", b.contractABI, b.transactor, - arg_redemptionWatchtower, + arg_rebateStaking, ) return result, err } // Transaction submission. -func (b *Bridge) SetSpvMaintainerStatus( - arg_spvMaintainer common.Address, - arg_isTrusted bool, +func (b *Bridge) SetRedemptionWatchtower( + arg_redemptionWatchtower common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction setSpvMaintainerStatus", + "submitting transaction setRedemptionWatchtower", + " params: ", + fmt.Sprint( + arg_redemptionWatchtower, + ), + ) + + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.SetRedemptionWatchtower( + transactorOptions, + arg_redemptionWatchtower, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "setRedemptionWatchtower", + arg_redemptionWatchtower, + ) + } + + bLogger.Infof( + "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go b.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := b.contract.SetRedemptionWatchtower( + newTransactorOptions, + arg_redemptionWatchtower, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "setRedemptionWatchtower", + arg_redemptionWatchtower, + ) + } + + bLogger.Infof( + "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + b.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallSetRedemptionWatchtower( + arg_redemptionWatchtower common.Address, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "setRedemptionWatchtower", + &result, + arg_redemptionWatchtower, + ) + + return err +} + +func (b *Bridge) SetRedemptionWatchtowerGasEstimate( + arg_redemptionWatchtower common.Address, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "setRedemptionWatchtower", + b.contractABI, + b.transactor, + arg_redemptionWatchtower, + ) + + return result, err +} + +// Transaction submission. +func (b *Bridge) SetReservationRouter( + arg__reservationRouter common.Address, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction setReservationRouter", + " params: ", + fmt.Sprint( + arg__reservationRouter, + ), + ) + + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.SetReservationRouter( + transactorOptions, + arg__reservationRouter, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "setReservationRouter", + arg__reservationRouter, + ) + } + + bLogger.Infof( + "submitted transaction setReservationRouter with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go b.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := b.contract.SetReservationRouter( + newTransactorOptions, + arg__reservationRouter, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "setReservationRouter", + arg__reservationRouter, + ) + } + + bLogger.Infof( + "submitted transaction setReservationRouter with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + b.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallSetReservationRouter( + arg__reservationRouter common.Address, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "setReservationRouter", + &result, + arg__reservationRouter, + ) + + return err +} + +func (b *Bridge) SetReservationRouterGasEstimate( + arg__reservationRouter common.Address, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "setReservationRouter", + b.contractABI, + b.transactor, + arg__reservationRouter, + ) + + return result, err +} + +// Transaction submission. +func (b *Bridge) SetSpvMaintainerStatus( + arg_spvMaintainer common.Address, + arg_isTrusted bool, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction setSpvMaintainerStatus", " params: ", fmt.Sprint( arg_spvMaintainer, @@ -5961,6 +6499,43 @@ func (b *Bridge) FraudParametersAtBlock( return result, err } +func (b *Bridge) GetRebateStaking() (common.Address, error) { + result, err := b.contract.GetRebateStaking( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "getRebateStaking", + ) + } + + return result, err +} + +func (b *Bridge) GetRebateStakingAtBlock( + blockNumber *big.Int, +) (common.Address, error) { + var result common.Address + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "getRebateStaking", + &result, + ) + + return result, err +} + func (b *Bridge) GetRedemptionWatchtower() (common.Address, error) { result, err := b.contract.GetRedemptionWatchtower( b.callerOptions, @@ -5998,6 +6573,43 @@ func (b *Bridge) GetRedemptionWatchtowerAtBlock( return result, err } +func (b *Bridge) GetReservationRouter() (common.Address, error) { + result, err := b.contract.GetReservationRouter( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "getReservationRouter", + ) + } + + return result, err +} + +func (b *Bridge) GetReservationRouterAtBlock( + blockNumber *big.Int, +) (common.Address, error) { + var result common.Address + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "getReservationRouter", + &result, + ) + + return result, err +} + func (b *Bridge) Governance() (common.Address, error) { result, err := b.contract.Governance( b.callerOptions, @@ -6035,6 +6647,49 @@ func (b *Bridge) GovernanceAtBlock( return result, err } +func (b *Bridge) IsReservedDeposit( + arg_depositKey *big.Int, +) (bool, error) { + result, err := b.contract.IsReservedDeposit( + b.callerOptions, + arg_depositKey, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "isReservedDeposit", + arg_depositKey, + ) + } + + return result, err +} + +func (b *Bridge) IsReservedDepositAtBlock( + arg_depositKey *big.Int, + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "isReservedDeposit", + &result, + arg_depositKey, + ) + + return result, err +} + func (b *Bridge) IsVaultTrusted( arg_vault common.Address, ) (bool, error) { @@ -6949,6 +7604,196 @@ func (b *Bridge) PastDepositRevealedEvents( return events, nil } +func (b *Bridge) DepositVaultFixedEvent( + opts *ethereum.SubscribeOpts, + depositKeyFilter []*big.Int, +) *BDepositVaultFixedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BDepositVaultFixedSubscription{ + b, + opts, + depositKeyFilter, + } +} + +type BDepositVaultFixedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + depositKeyFilter []*big.Int +} + +type bridgeDepositVaultFixedFunc func( + DepositKey *big.Int, + NewVault common.Address, + blockNumber uint64, +) + +func (dvfs *BDepositVaultFixedSubscription) OnEvent( + handler bridgeDepositVaultFixedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeDepositVaultFixed) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.DepositKey, + event.NewVault, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := dvfs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (dvfs *BDepositVaultFixedSubscription) Pipe( + sink chan *abi.BridgeDepositVaultFixed, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(dvfs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := dvfs.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - dvfs.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past DepositVaultFixed events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := dvfs.contract.PastDepositVaultFixedEvents( + fromBlock, + nil, + dvfs.depositKeyFilter, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past DepositVaultFixed events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := dvfs.contract.watchDepositVaultFixed( + sink, + dvfs.depositKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchDepositVaultFixed( + sink chan *abi.BridgeDepositVaultFixed, + depositKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchDepositVaultFixed( + &bind.WatchOpts{Context: ctx}, + sink, + depositKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event DepositVaultFixed had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event DepositVaultFixed failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastDepositVaultFixedEvents( + startBlock uint64, + endBlock *uint64, + depositKeyFilter []*big.Int, +) ([]*abi.BridgeDepositVaultFixed, error) { + iterator, err := b.contract.FilterDepositVaultFixed( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + depositKeyFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past DepositVaultFixed events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeDepositVaultFixed, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + func (b *Bridge) DepositsSweptEvent( opts *ethereum.SubscribeOpts, ) *BDepositsSweptSubscription { @@ -10154,6 +10999,366 @@ func (b *Bridge) PastNewWalletRequestedEvents( return events, nil } +func (b *Bridge) RebateStakingRepairedEvent( + opts *ethereum.SubscribeOpts, +) *BRebateStakingRepairedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BRebateStakingRepairedSubscription{ + b, + opts, + } +} + +type BRebateStakingRepairedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeRebateStakingRepairedFunc func( + OldRebateStaking common.Address, + NewRebateStaking common.Address, + blockNumber uint64, +) + +func (rsrs *BRebateStakingRepairedSubscription) OnEvent( + handler bridgeRebateStakingRepairedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeRebateStakingRepaired) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.OldRebateStaking, + event.NewRebateStaking, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rsrs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rsrs *BRebateStakingRepairedSubscription) Pipe( + sink chan *abi.BridgeRebateStakingRepaired, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rsrs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rsrs.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rsrs.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past RebateStakingRepaired events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rsrs.contract.PastRebateStakingRepairedEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past RebateStakingRepaired events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rsrs.contract.watchRebateStakingRepaired( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchRebateStakingRepaired( + sink chan *abi.BridgeRebateStakingRepaired, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchRebateStakingRepaired( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event RebateStakingRepaired had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event RebateStakingRepaired failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastRebateStakingRepairedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeRebateStakingRepaired, error) { + iterator, err := b.contract.FilterRebateStakingRepaired( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past RebateStakingRepaired events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeRebateStakingRepaired, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) RebateStakingSetEvent( + opts *ethereum.SubscribeOpts, +) *BRebateStakingSetSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BRebateStakingSetSubscription{ + b, + opts, + } +} + +type BRebateStakingSetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeRebateStakingSetFunc func( + RebateStaking common.Address, + blockNumber uint64, +) + +func (rsss *BRebateStakingSetSubscription) OnEvent( + handler bridgeRebateStakingSetFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeRebateStakingSet) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.RebateStaking, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rsss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rsss *BRebateStakingSetSubscription) Pipe( + sink chan *abi.BridgeRebateStakingSet, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rsss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rsss.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rsss.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past RebateStakingSet events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rsss.contract.PastRebateStakingSetEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past RebateStakingSet events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rsss.contract.watchRebateStakingSet( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchRebateStakingSet( + sink chan *abi.BridgeRebateStakingSet, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchRebateStakingSet( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event RebateStakingSet had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event RebateStakingSet failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastRebateStakingSetEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeRebateStakingSet, error) { + iterator, err := b.contract.FilterRebateStakingSet( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past RebateStakingSet events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeRebateStakingSet, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + func (b *Bridge) RedemptionParametersUpdatedEvent( opts *ethereum.SubscribeOpts, ) *BRedemptionParametersUpdatedSubscription { diff --git a/pkg/chain/ethereum/tbtc/gen/contract/RedemptionWatchtower.go b/pkg/chain/ethereum/tbtc/gen/contract/RedemptionWatchtower.go index 9e49a30418..bd40cd601a 100644 --- a/pkg/chain/ethereum/tbtc/gen/contract/RedemptionWatchtower.go +++ b/pkg/chain/ethereum/tbtc/gen/contract/RedemptionWatchtower.go @@ -2165,6 +2165,43 @@ func (rw *RedemptionWatchtower) OwnerAtBlock( return result, err } +func (rw *RedemptionWatchtower) REQUIREDOBJECTIONSCOUNT() (uint8, error) { + result, err := rw.contract.REQUIREDOBJECTIONSCOUNT( + rw.callerOptions, + ) + + if err != nil { + return result, rw.errorResolver.ResolveError( + err, + rw.callerOptions.From, + nil, + "rEQUIREDOBJECTIONSCOUNT", + ) + } + + return result, err +} + +func (rw *RedemptionWatchtower) REQUIREDOBJECTIONSCOUNTAtBlock( + blockNumber *big.Int, +) (uint8, error) { + var result uint8 + + err := chainutil.CallAtBlock( + rw.callerOptions.From, + blockNumber, + nil, + rw.contractABI, + rw.caller, + rw.errorResolver, + rw.contractAddress, + "rEQUIREDOBJECTIONSCOUNT", + &result, + ) + + return result, err +} + func (rw *RedemptionWatchtower) VetoFreezePeriod() (uint32, error) { result, err := rw.contract.VetoFreezePeriod( rw.callerOptions, diff --git a/pkg/chain/ethereum/tbtc/gen/contract/ReservationRouter.go b/pkg/chain/ethereum/tbtc/gen/contract/ReservationRouter.go new file mode 100644 index 0000000000..97b6eba015 --- /dev/null +++ b/pkg/chain/ethereum/tbtc/gen/contract/ReservationRouter.go @@ -0,0 +1,5187 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package contract + +import ( + "context" + "fmt" + "math/big" + "strings" + "sync" + "time" + + hostchainabi "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + + "github.com/ipfs/go-log" + + "github.com/keep-network/keep-common/pkg/chain/ethereum" + chainutil "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" + "github.com/keep-network/keep-common/pkg/subscription" + "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi" +) + +// Create a package-level logger for this contract. The logger exists at +// package level so that the logger is registered at startup and can be +// included or excluded from logging at startup by name. +var rrLogger = log.Logger("keep-contract-ReservationRouter") + +type ReservationRouter struct { + contract *abi.ReservationRouter + contractAddress common.Address + contractABI *hostchainabi.ABI + caller bind.ContractCaller + transactor bind.ContractTransactor + callerOptions *bind.CallOpts + transactorOptions *bind.TransactOpts + errorResolver *chainutil.ErrorResolver + nonceManager *ethereum.NonceManager + miningWaiter *chainutil.MiningWaiter + blockCounter *ethereum.BlockCounter + + transactionMutex *sync.Mutex +} + +func NewReservationRouter( + contractAddress common.Address, + chainId *big.Int, + accountKey *keystore.Key, + backend bind.ContractBackend, + nonceManager *ethereum.NonceManager, + miningWaiter *chainutil.MiningWaiter, + blockCounter *ethereum.BlockCounter, + transactionMutex *sync.Mutex, +) (*ReservationRouter, error) { + callerOptions := &bind.CallOpts{ + From: accountKey.Address, + } + + transactorOptions, err := bind.NewKeyedTransactorWithChainID( + accountKey.PrivateKey, + chainId, + ) + if err != nil { + return nil, fmt.Errorf("failed to instantiate transactor: [%v]", err) + } + + contract, err := abi.NewReservationRouter( + contractAddress, + backend, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to instantiate contract at address: %s [%v]", + contractAddress.String(), + err, + ) + } + + contractABI, err := hostchainabi.JSON(strings.NewReader(abi.ReservationRouterABI)) + if err != nil { + return nil, fmt.Errorf("failed to instantiate ABI: [%v]", err) + } + + return &ReservationRouter{ + contract: contract, + contractAddress: contractAddress, + contractABI: &contractABI, + caller: backend, + transactor: backend, + callerOptions: callerOptions, + transactorOptions: transactorOptions, + errorResolver: chainutil.NewErrorResolver(backend, &contractABI, &contractAddress), + nonceManager: nonceManager, + miningWaiter: miningWaiter, + blockCounter: blockCounter, + transactionMutex: transactionMutex, + }, nil +} + +// ----- Non-const Methods ------ + +// Transaction submission. +func (rr *ReservationRouter) NotifyReservationActionTimeout( + arg_reservationKey *big.Int, + arg_walletMembersIDs []uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction notifyReservationActionTimeout", + " params: ", + fmt.Sprint( + arg_reservationKey, + arg_walletMembersIDs, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.NotifyReservationActionTimeout( + transactorOptions, + arg_reservationKey, + arg_walletMembersIDs, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "notifyReservationActionTimeout", + arg_reservationKey, + arg_walletMembersIDs, + ) + } + + rrLogger.Infof( + "submitted transaction notifyReservationActionTimeout with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.NotifyReservationActionTimeout( + newTransactorOptions, + arg_reservationKey, + arg_walletMembersIDs, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "notifyReservationActionTimeout", + arg_reservationKey, + arg_walletMembersIDs, + ) + } + + rrLogger.Infof( + "submitted transaction notifyReservationActionTimeout with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallNotifyReservationActionTimeout( + arg_reservationKey *big.Int, + arg_walletMembersIDs []uint32, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "notifyReservationActionTimeout", + &result, + arg_reservationKey, + arg_walletMembersIDs, + ) + + return err +} + +func (rr *ReservationRouter) NotifyReservationActionTimeoutGasEstimate( + arg_reservationKey *big.Int, + arg_walletMembersIDs []uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "notifyReservationActionTimeout", + rr.contractABI, + rr.transactor, + arg_reservationKey, + arg_walletMembersIDs, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) NotifyReservationStranded( + arg_reservationKey *big.Int, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction notifyReservationStranded", + " params: ", + fmt.Sprint( + arg_reservationKey, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.NotifyReservationStranded( + transactorOptions, + arg_reservationKey, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "notifyReservationStranded", + arg_reservationKey, + ) + } + + rrLogger.Infof( + "submitted transaction notifyReservationStranded with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.NotifyReservationStranded( + newTransactorOptions, + arg_reservationKey, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "notifyReservationStranded", + arg_reservationKey, + ) + } + + rrLogger.Infof( + "submitted transaction notifyReservationStranded with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallNotifyReservationStranded( + arg_reservationKey *big.Int, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "notifyReservationStranded", + &result, + arg_reservationKey, + ) + + return err +} + +func (rr *ReservationRouter) NotifyReservationStrandedGasEstimate( + arg_reservationKey *big.Int, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "notifyReservationStranded", + rr.contractABI, + rr.transactor, + arg_reservationKey, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) NotifyStaleReservedDeposit( + arg_depositKey *big.Int, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction notifyStaleReservedDeposit", + " params: ", + fmt.Sprint( + arg_depositKey, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.NotifyStaleReservedDeposit( + transactorOptions, + arg_depositKey, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "notifyStaleReservedDeposit", + arg_depositKey, + ) + } + + rrLogger.Infof( + "submitted transaction notifyStaleReservedDeposit with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.NotifyStaleReservedDeposit( + newTransactorOptions, + arg_depositKey, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "notifyStaleReservedDeposit", + arg_depositKey, + ) + } + + rrLogger.Infof( + "submitted transaction notifyStaleReservedDeposit with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallNotifyStaleReservedDeposit( + arg_depositKey *big.Int, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "notifyStaleReservedDeposit", + &result, + arg_depositKey, + ) + + return err +} + +func (rr *ReservationRouter) NotifyStaleReservedDepositGasEstimate( + arg_depositKey *big.Int, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "notifyStaleReservedDeposit", + rr.contractABI, + rr.transactor, + arg_depositKey, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) RequestReservationAcceptance( + arg_reservationKey *big.Int, + arg_walletPubKeyHash [20]byte, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction requestReservationAcceptance", + " params: ", + fmt.Sprint( + arg_reservationKey, + arg_walletPubKeyHash, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.RequestReservationAcceptance( + transactorOptions, + arg_reservationKey, + arg_walletPubKeyHash, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "requestReservationAcceptance", + arg_reservationKey, + arg_walletPubKeyHash, + ) + } + + rrLogger.Infof( + "submitted transaction requestReservationAcceptance with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.RequestReservationAcceptance( + newTransactorOptions, + arg_reservationKey, + arg_walletPubKeyHash, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "requestReservationAcceptance", + arg_reservationKey, + arg_walletPubKeyHash, + ) + } + + rrLogger.Infof( + "submitted transaction requestReservationAcceptance with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallRequestReservationAcceptance( + arg_reservationKey *big.Int, + arg_walletPubKeyHash [20]byte, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "requestReservationAcceptance", + &result, + arg_reservationKey, + arg_walletPubKeyHash, + ) + + return err +} + +func (rr *ReservationRouter) RequestReservationAcceptanceGasEstimate( + arg_reservationKey *big.Int, + arg_walletPubKeyHash [20]byte, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "requestReservationAcceptance", + rr.contractABI, + rr.transactor, + arg_reservationKey, + arg_walletPubKeyHash, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) RequestReservationReanchor( + arg_reservationKey *big.Int, + arg_targetWalletPubKeyHash [20]byte, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction requestReservationReanchor", + " params: ", + fmt.Sprint( + arg_reservationKey, + arg_targetWalletPubKeyHash, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.RequestReservationReanchor( + transactorOptions, + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "requestReservationReanchor", + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + } + + rrLogger.Infof( + "submitted transaction requestReservationReanchor with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.RequestReservationReanchor( + newTransactorOptions, + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "requestReservationReanchor", + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + } + + rrLogger.Infof( + "submitted transaction requestReservationReanchor with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallRequestReservationReanchor( + arg_reservationKey *big.Int, + arg_targetWalletPubKeyHash [20]byte, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "requestReservationReanchor", + &result, + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + + return err +} + +func (rr *ReservationRouter) RequestReservationReanchorGasEstimate( + arg_reservationKey *big.Int, + arg_targetWalletPubKeyHash [20]byte, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "requestReservationReanchor", + rr.contractABI, + rr.transactor, + arg_reservationKey, + arg_targetWalletPubKeyHash, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) SubmitReservationProof( + arg_proofType uint8, + arg_txInfo abi.BitcoinTxInfo4, + arg_proof abi.BitcoinTxProof3, + arg_mainUtxo abi.BitcoinTxUTXO4, + arg_reservationKey *big.Int, + arg_requestNonce uint64, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction submitReservationProof", + " params: ", + fmt.Sprint( + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.SubmitReservationProof( + transactorOptions, + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "submitReservationProof", + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ) + } + + rrLogger.Infof( + "submitted transaction submitReservationProof with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.SubmitReservationProof( + newTransactorOptions, + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "submitReservationProof", + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ) + } + + rrLogger.Infof( + "submitted transaction submitReservationProof with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallSubmitReservationProof( + arg_proofType uint8, + arg_txInfo abi.BitcoinTxInfo4, + arg_proof abi.BitcoinTxProof3, + arg_mainUtxo abi.BitcoinTxUTXO4, + arg_reservationKey *big.Int, + arg_requestNonce uint64, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "submitReservationProof", + &result, + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ) + + return err +} + +func (rr *ReservationRouter) SubmitReservationProofGasEstimate( + arg_proofType uint8, + arg_txInfo abi.BitcoinTxInfo4, + arg_proof abi.BitcoinTxProof3, + arg_mainUtxo abi.BitcoinTxUTXO4, + arg_reservationKey *big.Int, + arg_requestNonce uint64, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "submitReservationProof", + rr.contractABI, + rr.transactor, + arg_proofType, + arg_txInfo, + arg_proof, + arg_mainUtxo, + arg_reservationKey, + arg_requestNonce, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) TransferGovernance( + arg_newGovernance common.Address, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction transferGovernance", + " params: ", + fmt.Sprint( + arg_newGovernance, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.TransferGovernance( + transactorOptions, + arg_newGovernance, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "transferGovernance", + arg_newGovernance, + ) + } + + rrLogger.Infof( + "submitted transaction transferGovernance with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.TransferGovernance( + newTransactorOptions, + arg_newGovernance, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "transferGovernance", + arg_newGovernance, + ) + } + + rrLogger.Infof( + "submitted transaction transferGovernance with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallTransferGovernance( + arg_newGovernance common.Address, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "transferGovernance", + &result, + arg_newGovernance, + ) + + return err +} + +func (rr *ReservationRouter) TransferGovernanceGasEstimate( + arg_newGovernance common.Address, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "transferGovernance", + rr.contractABI, + rr.transactor, + arg_newGovernance, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) UpdateReservationCaps( + arg_maxReservationsAmountPerWallet uint64, + arg_reservationMaxSingleAmount uint64, + arg_maxActiveReservations uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction updateReservationCaps", + " params: ", + fmt.Sprint( + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.UpdateReservationCaps( + transactorOptions, + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "updateReservationCaps", + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + } + + rrLogger.Infof( + "submitted transaction updateReservationCaps with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.UpdateReservationCaps( + newTransactorOptions, + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "updateReservationCaps", + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + } + + rrLogger.Infof( + "submitted transaction updateReservationCaps with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallUpdateReservationCaps( + arg_maxReservationsAmountPerWallet uint64, + arg_reservationMaxSingleAmount uint64, + arg_maxActiveReservations uint32, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "updateReservationCaps", + &result, + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + + return err +} + +func (rr *ReservationRouter) UpdateReservationCapsGasEstimate( + arg_maxReservationsAmountPerWallet uint64, + arg_reservationMaxSingleAmount uint64, + arg_maxActiveReservations uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "updateReservationCaps", + rr.contractABI, + rr.transactor, + arg_maxReservationsAmountPerWallet, + arg_reservationMaxSingleAmount, + arg_maxActiveReservations, + ) + + return result, err +} + +// Transaction submission. +func (rr *ReservationRouter) UpdateReservationParameters( + arg_reservationVault common.Address, + arg_reservationMinAmount uint64, + arg_reservationTxMaxFee uint64, + arg_reservationTermSeconds uint32, + arg_reservationDissolutionDelay uint32, + arg_reservationMaxTotalAmount uint64, + arg_maxReservationsPerWallet uint32, + arg_reservationActionTimeout uint32, + arg_reservationRenewalWindowSeconds uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + rrLogger.Debug( + "submitting transaction updateReservationParameters", + " params: ", + fmt.Sprint( + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ), + ) + + rr.transactionMutex.Lock() + defer rr.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *rr.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := rr.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := rr.contract.UpdateReservationParameters( + transactorOptions, + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + if err != nil { + return transaction, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "updateReservationParameters", + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + } + + rrLogger.Infof( + "submitted transaction updateReservationParameters with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go rr.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := rr.contract.UpdateReservationParameters( + newTransactorOptions, + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + if err != nil { + return nil, rr.errorResolver.ResolveError( + err, + rr.transactorOptions.From, + nil, + "updateReservationParameters", + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + } + + rrLogger.Infof( + "submitted transaction updateReservationParameters with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + rr.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (rr *ReservationRouter) CallUpdateReservationParameters( + arg_reservationVault common.Address, + arg_reservationMinAmount uint64, + arg_reservationTxMaxFee uint64, + arg_reservationTermSeconds uint32, + arg_reservationDissolutionDelay uint32, + arg_reservationMaxTotalAmount uint64, + arg_maxReservationsPerWallet uint32, + arg_reservationActionTimeout uint32, + arg_reservationRenewalWindowSeconds uint32, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + rr.transactorOptions.From, + blockNumber, nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "updateReservationParameters", + &result, + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + + return err +} + +func (rr *ReservationRouter) UpdateReservationParametersGasEstimate( + arg_reservationVault common.Address, + arg_reservationMinAmount uint64, + arg_reservationTxMaxFee uint64, + arg_reservationTermSeconds uint32, + arg_reservationDissolutionDelay uint32, + arg_reservationMaxTotalAmount uint64, + arg_maxReservationsPerWallet uint32, + arg_reservationActionTimeout uint32, + arg_reservationRenewalWindowSeconds uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + rr.callerOptions.From, + rr.contractAddress, + "updateReservationParameters", + rr.contractABI, + rr.transactor, + arg_reservationVault, + arg_reservationMinAmount, + arg_reservationTxMaxFee, + arg_reservationTermSeconds, + arg_reservationDissolutionDelay, + arg_reservationMaxTotalAmount, + arg_maxReservationsPerWallet, + arg_reservationActionTimeout, + arg_reservationRenewalWindowSeconds, + ) + + return result, err +} + +// ----- Const Methods ------ + +type activeReservationsCount struct { + Count uint32 + MaxActive uint32 +} + +func (rr *ReservationRouter) ActiveReservationsCount() (activeReservationsCount, error) { + result, err := rr.contract.ActiveReservationsCount( + rr.callerOptions, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "activeReservationsCount", + ) + } + + return result, err +} + +func (rr *ReservationRouter) ActiveReservationsCountAtBlock( + blockNumber *big.Int, +) (activeReservationsCount, error) { + var result activeReservationsCount + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "activeReservationsCount", + &result, + ) + + return result, err +} + +func (rr *ReservationRouter) Governance() (common.Address, error) { + result, err := rr.contract.Governance( + rr.callerOptions, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "governance", + ) + } + + return result, err +} + +func (rr *ReservationRouter) GovernanceAtBlock( + blockNumber *big.Int, +) (common.Address, error) { + var result common.Address + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "governance", + &result, + ) + + return result, err +} + +func (rr *ReservationRouter) PendingReservedDeposits() (uint64, error) { + result, err := rr.contract.PendingReservedDeposits( + rr.callerOptions, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "pendingReservedDeposits", + ) + } + + return result, err +} + +func (rr *ReservationRouter) PendingReservedDepositsAtBlock( + blockNumber *big.Int, +) (uint64, error) { + var result uint64 + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "pendingReservedDeposits", + &result, + ) + + return result, err +} + +func (rr *ReservationRouter) ReservationActions( + arg_reservationKey *big.Int, + arg_requestNonce uint64, +) (abi.ReservationReservationAction, error) { + result, err := rr.contract.ReservationActions( + rr.callerOptions, + arg_reservationKey, + arg_requestNonce, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservationActions", + arg_reservationKey, + arg_requestNonce, + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservationActionsAtBlock( + arg_reservationKey *big.Int, + arg_requestNonce uint64, + blockNumber *big.Int, +) (abi.ReservationReservationAction, error) { + var result abi.ReservationReservationAction + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservationActions", + &result, + arg_reservationKey, + arg_requestNonce, + ) + + return result, err +} + +func (rr *ReservationRouter) ReservationByAnchorUtxo( + arg_anchorTxHash [32]byte, + arg_anchorTxOutputIndex uint32, +) (*big.Int, error) { + result, err := rr.contract.ReservationByAnchorUtxo( + rr.callerOptions, + arg_anchorTxHash, + arg_anchorTxOutputIndex, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservationByAnchorUtxo", + arg_anchorTxHash, + arg_anchorTxOutputIndex, + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservationByAnchorUtxoAtBlock( + arg_anchorTxHash [32]byte, + arg_anchorTxOutputIndex uint32, + blockNumber *big.Int, +) (*big.Int, error) { + var result *big.Int + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservationByAnchorUtxo", + &result, + arg_anchorTxHash, + arg_anchorTxOutputIndex, + ) + + return result, err +} + +type reservationCaps struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 +} + +func (rr *ReservationRouter) ReservationCaps() (reservationCaps, error) { + result, err := rr.contract.ReservationCaps( + rr.callerOptions, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservationCaps", + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservationCapsAtBlock( + blockNumber *big.Int, +) (reservationCaps, error) { + var result reservationCaps + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservationCaps", + &result, + ) + + return result, err +} + +type reservationParameters struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 +} + +func (rr *ReservationRouter) ReservationParameters() (reservationParameters, error) { + result, err := rr.contract.ReservationParameters( + rr.callerOptions, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservationParameters", + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservationParametersAtBlock( + blockNumber *big.Int, +) (reservationParameters, error) { + var result reservationParameters + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservationParameters", + &result, + ) + + return result, err +} + +func (rr *ReservationRouter) ReservationRouter() (common.Address, error) { + result, err := rr.contract.ReservationRouter( + rr.callerOptions, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservationRouter", + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservationRouterAtBlock( + blockNumber *big.Int, +) (common.Address, error) { + var result common.Address + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservationRouter", + &result, + ) + + return result, err +} + +func (rr *ReservationRouter) Reservations( + arg_reservationKey *big.Int, +) (abi.ReservationReservationRequest, error) { + result, err := rr.contract.Reservations( + rr.callerOptions, + arg_reservationKey, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservations", + arg_reservationKey, + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservationsAtBlock( + arg_reservationKey *big.Int, + blockNumber *big.Int, +) (abi.ReservationReservationRequest, error) { + var result abi.ReservationReservationRequest + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservations", + &result, + arg_reservationKey, + ) + + return result, err +} + +func (rr *ReservationRouter) ReservedDepositWallet( + arg_depositKey *big.Int, +) ([20]byte, error) { + result, err := rr.contract.ReservedDepositWallet( + rr.callerOptions, + arg_depositKey, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "reservedDepositWallet", + arg_depositKey, + ) + } + + return result, err +} + +func (rr *ReservationRouter) ReservedDepositWalletAtBlock( + arg_depositKey *big.Int, + blockNumber *big.Int, +) ([20]byte, error) { + var result [20]byte + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "reservedDepositWallet", + &result, + arg_depositKey, + ) + + return result, err +} + +func (rr *ReservationRouter) WalletReservations( + arg_walletPubKeyHash [20]byte, +) ([]*big.Int, error) { + result, err := rr.contract.WalletReservations( + rr.callerOptions, + arg_walletPubKeyHash, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "walletReservations", + arg_walletPubKeyHash, + ) + } + + return result, err +} + +func (rr *ReservationRouter) WalletReservationsAtBlock( + arg_walletPubKeyHash [20]byte, + blockNumber *big.Int, +) ([]*big.Int, error) { + var result []*big.Int + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "walletReservations", + &result, + arg_walletPubKeyHash, + ) + + return result, err +} + +func (rr *ReservationRouter) WalletReservationsAmount( + arg_walletPubKeyHash [20]byte, +) (uint64, error) { + result, err := rr.contract.WalletReservationsAmount( + rr.callerOptions, + arg_walletPubKeyHash, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "walletReservationsAmount", + arg_walletPubKeyHash, + ) + } + + return result, err +} + +func (rr *ReservationRouter) WalletReservationsAmountAtBlock( + arg_walletPubKeyHash [20]byte, + blockNumber *big.Int, +) (uint64, error) { + var result uint64 + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "walletReservationsAmount", + &result, + arg_walletPubKeyHash, + ) + + return result, err +} + +func (rr *ReservationRouter) WalletReservationsCount( + arg_walletPubKeyHash [20]byte, +) (uint32, error) { + result, err := rr.contract.WalletReservationsCount( + rr.callerOptions, + arg_walletPubKeyHash, + ) + + if err != nil { + return result, rr.errorResolver.ResolveError( + err, + rr.callerOptions.From, + nil, + "walletReservationsCount", + arg_walletPubKeyHash, + ) + } + + return result, err +} + +func (rr *ReservationRouter) WalletReservationsCountAtBlock( + arg_walletPubKeyHash [20]byte, + blockNumber *big.Int, +) (uint32, error) { + var result uint32 + + err := chainutil.CallAtBlock( + rr.callerOptions.From, + blockNumber, + nil, + rr.contractABI, + rr.caller, + rr.errorResolver, + rr.contractAddress, + "walletReservationsCount", + &result, + arg_walletPubKeyHash, + ) + + return result, err +} + +// ------ Events ------- + +func (rr *ReservationRouter) GovernanceTransferredEvent( + opts *ethereum.SubscribeOpts, +) *RrGovernanceTransferredSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrGovernanceTransferredSubscription{ + rr, + opts, + } +} + +type RrGovernanceTransferredSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts +} + +type reservationRouterGovernanceTransferredFunc func( + OldGovernance common.Address, + NewGovernance common.Address, + blockNumber uint64, +) + +func (gts *RrGovernanceTransferredSubscription) OnEvent( + handler reservationRouterGovernanceTransferredFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterGovernanceTransferred) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.OldGovernance, + event.NewGovernance, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := gts.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (gts *RrGovernanceTransferredSubscription) Pipe( + sink chan *abi.ReservationRouterGovernanceTransferred, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(gts.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := gts.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - gts.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past GovernanceTransferred events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := gts.contract.PastGovernanceTransferredEvents( + fromBlock, + nil, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past GovernanceTransferred events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := gts.contract.watchGovernanceTransferred( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchGovernanceTransferred( + sink chan *abi.ReservationRouterGovernanceTransferred, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchGovernanceTransferred( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event GovernanceTransferred had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event GovernanceTransferred failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastGovernanceTransferredEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.ReservationRouterGovernanceTransferred, error) { + iterator, err := rr.contract.FilterGovernanceTransferred( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past GovernanceTransferred events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterGovernanceTransferred, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) InitializedEvent( + opts *ethereum.SubscribeOpts, +) *RrInitializedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrInitializedSubscription{ + rr, + opts, + } +} + +type RrInitializedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts +} + +type reservationRouterInitializedFunc func( + Version uint8, + blockNumber uint64, +) + +func (is *RrInitializedSubscription) OnEvent( + handler reservationRouterInitializedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterInitialized) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.Version, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := is.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (is *RrInitializedSubscription) Pipe( + sink chan *abi.ReservationRouterInitialized, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(is.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := is.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - is.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past Initialized events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := is.contract.PastInitializedEvents( + fromBlock, + nil, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past Initialized events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := is.contract.watchInitialized( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchInitialized( + sink chan *abi.ReservationRouterInitialized, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchInitialized( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event Initialized had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event Initialized failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastInitializedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.ReservationRouterInitialized, error) { + iterator, err := rr.contract.FilterInitialized( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past Initialized events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterInitialized, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationAcceptanceRequestedEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, +) *RrReservationAcceptanceRequestedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationAcceptanceRequestedSubscription{ + rr, + opts, + reservationKeyFilter, + walletPubKeyHashFilter, + } +} + +type RrReservationAcceptanceRequestedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int + walletPubKeyHashFilter [][20]byte +} + +type reservationRouterReservationAcceptanceRequestedFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + WalletPubKeyHash [20]byte, + DepositAmount uint64, + TxMaxFee uint64, + TimeoutAt uint32, + blockNumber uint64, +) + +func (rars *RrReservationAcceptanceRequestedSubscription) OnEvent( + handler reservationRouterReservationAcceptanceRequestedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationAcceptanceRequested) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.WalletPubKeyHash, + event.DepositAmount, + event.TxMaxFee, + event.TimeoutAt, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rars.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rars *RrReservationAcceptanceRequestedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationAcceptanceRequested, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rars.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rars.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rars.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationAcceptanceRequested events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rars.contract.PastReservationAcceptanceRequestedEvents( + fromBlock, + nil, + rars.reservationKeyFilter, + rars.walletPubKeyHashFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationAcceptanceRequested events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rars.contract.watchReservationAcceptanceRequested( + sink, + rars.reservationKeyFilter, + rars.walletPubKeyHashFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationAcceptanceRequested( + sink chan *abi.ReservationRouterReservationAcceptanceRequested, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationAcceptanceRequested( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + walletPubKeyHashFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationAcceptanceRequested had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationAcceptanceRequested failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationAcceptanceRequestedEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, +) ([]*abi.ReservationRouterReservationAcceptanceRequested, error) { + iterator, err := rr.contract.FilterReservationAcceptanceRequested( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + walletPubKeyHashFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationAcceptanceRequested events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationAcceptanceRequested, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationAcceptedEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, + ownerFilter []common.Address, +) *RrReservationAcceptedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationAcceptedSubscription{ + rr, + opts, + reservationKeyFilter, + walletPubKeyHashFilter, + ownerFilter, + } +} + +type RrReservationAcceptedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int + walletPubKeyHashFilter [][20]byte + ownerFilter []common.Address +} + +type reservationRouterReservationAcceptedFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + WalletPubKeyHash [20]byte, + Owner common.Address, + AnchorTxHash [32]byte, + AnchorAmount uint64, + ExpiresAt uint32, + blockNumber uint64, +) + +func (ras *RrReservationAcceptedSubscription) OnEvent( + handler reservationRouterReservationAcceptedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationAccepted) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.WalletPubKeyHash, + event.Owner, + event.AnchorTxHash, + event.AnchorAmount, + event.ExpiresAt, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := ras.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (ras *RrReservationAcceptedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationAccepted, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(ras.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := ras.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - ras.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationAccepted events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := ras.contract.PastReservationAcceptedEvents( + fromBlock, + nil, + ras.reservationKeyFilter, + ras.walletPubKeyHashFilter, + ras.ownerFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationAccepted events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := ras.contract.watchReservationAccepted( + sink, + ras.reservationKeyFilter, + ras.walletPubKeyHashFilter, + ras.ownerFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationAccepted( + sink chan *abi.ReservationRouterReservationAccepted, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, + ownerFilter []common.Address, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationAccepted( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + walletPubKeyHashFilter, + ownerFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationAccepted had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationAccepted failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationAcceptedEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, + ownerFilter []common.Address, +) ([]*abi.ReservationRouterReservationAccepted, error) { + iterator, err := rr.contract.FilterReservationAccepted( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + walletPubKeyHashFilter, + ownerFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationAccepted events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationAccepted, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationActionSupersededEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, +) *RrReservationActionSupersededSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationActionSupersededSubscription{ + rr, + opts, + reservationKeyFilter, + } +} + +type RrReservationActionSupersededSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int +} + +type reservationRouterReservationActionSupersededFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + blockNumber uint64, +) + +func (rass *RrReservationActionSupersededSubscription) OnEvent( + handler reservationRouterReservationActionSupersededFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationActionSuperseded) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rass.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rass *RrReservationActionSupersededSubscription) Pipe( + sink chan *abi.ReservationRouterReservationActionSuperseded, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rass.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rass.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rass.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationActionSuperseded events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rass.contract.PastReservationActionSupersededEvents( + fromBlock, + nil, + rass.reservationKeyFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationActionSuperseded events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rass.contract.watchReservationActionSuperseded( + sink, + rass.reservationKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationActionSuperseded( + sink chan *abi.ReservationRouterReservationActionSuperseded, + reservationKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationActionSuperseded( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationActionSuperseded had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationActionSuperseded failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationActionSupersededEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, +) ([]*abi.ReservationRouterReservationActionSuperseded, error) { + iterator, err := rr.contract.FilterReservationActionSuperseded( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationActionSuperseded events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationActionSuperseded, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationActionTimedOutEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, +) *RrReservationActionTimedOutSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationActionTimedOutSubscription{ + rr, + opts, + reservationKeyFilter, + } +} + +type RrReservationActionTimedOutSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int +} + +type reservationRouterReservationActionTimedOutFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + ActionType uint8, + blockNumber uint64, +) + +func (ratos *RrReservationActionTimedOutSubscription) OnEvent( + handler reservationRouterReservationActionTimedOutFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationActionTimedOut) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.ActionType, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := ratos.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (ratos *RrReservationActionTimedOutSubscription) Pipe( + sink chan *abi.ReservationRouterReservationActionTimedOut, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(ratos.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := ratos.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - ratos.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationActionTimedOut events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := ratos.contract.PastReservationActionTimedOutEvents( + fromBlock, + nil, + ratos.reservationKeyFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationActionTimedOut events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := ratos.contract.watchReservationActionTimedOut( + sink, + ratos.reservationKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationActionTimedOut( + sink chan *abi.ReservationRouterReservationActionTimedOut, + reservationKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationActionTimedOut( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationActionTimedOut had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationActionTimedOut failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationActionTimedOutEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, +) ([]*abi.ReservationRouterReservationActionTimedOut, error) { + iterator, err := rr.contract.FilterReservationActionTimedOut( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationActionTimedOut events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationActionTimedOut, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationCapsUpdatedEvent( + opts *ethereum.SubscribeOpts, +) *RrReservationCapsUpdatedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationCapsUpdatedSubscription{ + rr, + opts, + } +} + +type RrReservationCapsUpdatedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts +} + +type reservationRouterReservationCapsUpdatedFunc func( + MaxReservationsAmountPerWallet uint64, + ReservationMaxSingleAmount uint64, + MaxActiveReservations uint32, + blockNumber uint64, +) + +func (rcus *RrReservationCapsUpdatedSubscription) OnEvent( + handler reservationRouterReservationCapsUpdatedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationCapsUpdated) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.MaxReservationsAmountPerWallet, + event.ReservationMaxSingleAmount, + event.MaxActiveReservations, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rcus.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rcus *RrReservationCapsUpdatedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationCapsUpdated, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rcus.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rcus.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rcus.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationCapsUpdated events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rcus.contract.PastReservationCapsUpdatedEvents( + fromBlock, + nil, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationCapsUpdated events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rcus.contract.watchReservationCapsUpdated( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationCapsUpdated( + sink chan *abi.ReservationRouterReservationCapsUpdated, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationCapsUpdated( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationCapsUpdated had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationCapsUpdated failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationCapsUpdatedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.ReservationRouterReservationCapsUpdated, error) { + iterator, err := rr.contract.FilterReservationCapsUpdated( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationCapsUpdated events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationCapsUpdated, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationLateSettledEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, +) *RrReservationLateSettledSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationLateSettledSubscription{ + rr, + opts, + reservationKeyFilter, + } +} + +type RrReservationLateSettledSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int +} + +type reservationRouterReservationLateSettledFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + ActionType uint8, + blockNumber uint64, +) + +func (rlss *RrReservationLateSettledSubscription) OnEvent( + handler reservationRouterReservationLateSettledFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationLateSettled) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.ActionType, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rlss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rlss *RrReservationLateSettledSubscription) Pipe( + sink chan *abi.ReservationRouterReservationLateSettled, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rlss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rlss.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rlss.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationLateSettled events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rlss.contract.PastReservationLateSettledEvents( + fromBlock, + nil, + rlss.reservationKeyFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationLateSettled events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rlss.contract.watchReservationLateSettled( + sink, + rlss.reservationKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationLateSettled( + sink chan *abi.ReservationRouterReservationLateSettled, + reservationKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationLateSettled( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationLateSettled had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationLateSettled failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationLateSettledEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, +) ([]*abi.ReservationRouterReservationLateSettled, error) { + iterator, err := rr.contract.FilterReservationLateSettled( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationLateSettled events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationLateSettled, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationParametersUpdatedEvent( + opts *ethereum.SubscribeOpts, +) *RrReservationParametersUpdatedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationParametersUpdatedSubscription{ + rr, + opts, + } +} + +type RrReservationParametersUpdatedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts +} + +type reservationRouterReservationParametersUpdatedFunc func( + ReservationMinAmount uint64, + ReservationTxMaxFee uint64, + ReservationTermSeconds uint32, + ReservationDissolutionDelay uint32, + ReservationMaxTotalAmount uint64, + MaxReservationsPerWallet uint32, + ReservationActionTimeout uint32, + ReservationRenewalWindowSeconds uint32, + blockNumber uint64, +) + +func (rpus *RrReservationParametersUpdatedSubscription) OnEvent( + handler reservationRouterReservationParametersUpdatedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationParametersUpdated) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationMinAmount, + event.ReservationTxMaxFee, + event.ReservationTermSeconds, + event.ReservationDissolutionDelay, + event.ReservationMaxTotalAmount, + event.MaxReservationsPerWallet, + event.ReservationActionTimeout, + event.ReservationRenewalWindowSeconds, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rpus.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rpus *RrReservationParametersUpdatedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationParametersUpdated, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rpus.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rpus.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rpus.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationParametersUpdated events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rpus.contract.PastReservationParametersUpdatedEvents( + fromBlock, + nil, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationParametersUpdated events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rpus.contract.watchReservationParametersUpdated( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationParametersUpdated( + sink chan *abi.ReservationRouterReservationParametersUpdated, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationParametersUpdated( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationParametersUpdated had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationParametersUpdated failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationParametersUpdatedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.ReservationRouterReservationParametersUpdated, error) { + iterator, err := rr.contract.FilterReservationParametersUpdated( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationParametersUpdated events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationParametersUpdated, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationReanchorRequestedEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, + sourceWalletPubKeyHashFilter [][20]byte, + targetWalletPubKeyHashFilter [][20]byte, +) *RrReservationReanchorRequestedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationReanchorRequestedSubscription{ + rr, + opts, + reservationKeyFilter, + sourceWalletPubKeyHashFilter, + targetWalletPubKeyHashFilter, + } +} + +type RrReservationReanchorRequestedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int + sourceWalletPubKeyHashFilter [][20]byte + targetWalletPubKeyHashFilter [][20]byte +} + +type reservationRouterReservationReanchorRequestedFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + SourceWalletPubKeyHash [20]byte, + TargetWalletPubKeyHash [20]byte, + TxMaxFee uint64, + blockNumber uint64, +) + +func (rrrs *RrReservationReanchorRequestedSubscription) OnEvent( + handler reservationRouterReservationReanchorRequestedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationReanchorRequested) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.SourceWalletPubKeyHash, + event.TargetWalletPubKeyHash, + event.TxMaxFee, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rrrs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rrrs *RrReservationReanchorRequestedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationReanchorRequested, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rrrs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rrrs.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rrrs.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationReanchorRequested events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rrrs.contract.PastReservationReanchorRequestedEvents( + fromBlock, + nil, + rrrs.reservationKeyFilter, + rrrs.sourceWalletPubKeyHashFilter, + rrrs.targetWalletPubKeyHashFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationReanchorRequested events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rrrs.contract.watchReservationReanchorRequested( + sink, + rrrs.reservationKeyFilter, + rrrs.sourceWalletPubKeyHashFilter, + rrrs.targetWalletPubKeyHashFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationReanchorRequested( + sink chan *abi.ReservationRouterReservationReanchorRequested, + reservationKeyFilter []*big.Int, + sourceWalletPubKeyHashFilter [][20]byte, + targetWalletPubKeyHashFilter [][20]byte, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationReanchorRequested( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + sourceWalletPubKeyHashFilter, + targetWalletPubKeyHashFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationReanchorRequested had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationReanchorRequested failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationReanchorRequestedEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, + sourceWalletPubKeyHashFilter [][20]byte, + targetWalletPubKeyHashFilter [][20]byte, +) ([]*abi.ReservationRouterReservationReanchorRequested, error) { + iterator, err := rr.contract.FilterReservationReanchorRequested( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + sourceWalletPubKeyHashFilter, + targetWalletPubKeyHashFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationReanchorRequested events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationReanchorRequested, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationReanchoredEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, + newWalletPubKeyHashFilter [][20]byte, +) *RrReservationReanchoredSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationReanchoredSubscription{ + rr, + opts, + reservationKeyFilter, + newWalletPubKeyHashFilter, + } +} + +type RrReservationReanchoredSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int + newWalletPubKeyHashFilter [][20]byte +} + +type reservationRouterReservationReanchoredFunc func( + ReservationKey *big.Int, + RequestNonce uint64, + NewWalletPubKeyHash [20]byte, + NewAnchorTxHash [32]byte, + NewAnchorAmount uint64, + blockNumber uint64, +) + +func (rrs *RrReservationReanchoredSubscription) OnEvent( + handler reservationRouterReservationReanchoredFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationReanchored) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.RequestNonce, + event.NewWalletPubKeyHash, + event.NewAnchorTxHash, + event.NewAnchorAmount, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rrs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rrs *RrReservationReanchoredSubscription) Pipe( + sink chan *abi.ReservationRouterReservationReanchored, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rrs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rrs.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rrs.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationReanchored events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rrs.contract.PastReservationReanchoredEvents( + fromBlock, + nil, + rrs.reservationKeyFilter, + rrs.newWalletPubKeyHashFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationReanchored events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rrs.contract.watchReservationReanchored( + sink, + rrs.reservationKeyFilter, + rrs.newWalletPubKeyHashFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationReanchored( + sink chan *abi.ReservationRouterReservationReanchored, + reservationKeyFilter []*big.Int, + newWalletPubKeyHashFilter [][20]byte, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationReanchored( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + newWalletPubKeyHashFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationReanchored had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationReanchored failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationReanchoredEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, + newWalletPubKeyHashFilter [][20]byte, +) ([]*abi.ReservationRouterReservationReanchored, error) { + iterator, err := rr.contract.FilterReservationReanchored( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + newWalletPubKeyHashFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationReanchored events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationReanchored, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationRetryCreditMintedEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, +) *RrReservationRetryCreditMintedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationRetryCreditMintedSubscription{ + rr, + opts, + reservationKeyFilter, + } +} + +type RrReservationRetryCreditMintedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int +} + +type reservationRouterReservationRetryCreditMintedFunc func( + ReservationKey *big.Int, + blockNumber uint64, +) + +func (rrcms *RrReservationRetryCreditMintedSubscription) OnEvent( + handler reservationRouterReservationRetryCreditMintedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationRetryCreditMinted) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rrcms.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rrcms *RrReservationRetryCreditMintedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationRetryCreditMinted, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rrcms.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rrcms.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rrcms.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationRetryCreditMinted events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rrcms.contract.PastReservationRetryCreditMintedEvents( + fromBlock, + nil, + rrcms.reservationKeyFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationRetryCreditMinted events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rrcms.contract.watchReservationRetryCreditMinted( + sink, + rrcms.reservationKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationRetryCreditMinted( + sink chan *abi.ReservationRouterReservationRetryCreditMinted, + reservationKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationRetryCreditMinted( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationRetryCreditMinted had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationRetryCreditMinted failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationRetryCreditMintedEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, +) ([]*abi.ReservationRouterReservationRetryCreditMinted, error) { + iterator, err := rr.contract.FilterReservationRetryCreditMinted( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationRetryCreditMinted events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationRetryCreditMinted, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationRouterSetEvent( + opts *ethereum.SubscribeOpts, +) *RrReservationRouterSetSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationRouterSetSubscription{ + rr, + opts, + } +} + +type RrReservationRouterSetSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts +} + +type reservationRouterReservationRouterSetFunc func( + ReservationRouter common.Address, + blockNumber uint64, +) + +func (rrss *RrReservationRouterSetSubscription) OnEvent( + handler reservationRouterReservationRouterSetFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationRouterSet) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationRouter, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rrss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rrss *RrReservationRouterSetSubscription) Pipe( + sink chan *abi.ReservationRouterReservationRouterSet, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rrss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rrss.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rrss.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationRouterSet events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rrss.contract.PastReservationRouterSetEvents( + fromBlock, + nil, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationRouterSet events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rrss.contract.watchReservationRouterSet( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationRouterSet( + sink chan *abi.ReservationRouterReservationRouterSet, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationRouterSet( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationRouterSet had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationRouterSet failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationRouterSetEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.ReservationRouterReservationRouterSet, error) { + iterator, err := rr.contract.FilterReservationRouterSet( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationRouterSet events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationRouterSet, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationStrandedEvent( + opts *ethereum.SubscribeOpts, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, + ownerFilter []common.Address, +) *RrReservationStrandedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationStrandedSubscription{ + rr, + opts, + reservationKeyFilter, + walletPubKeyHashFilter, + ownerFilter, + } +} + +type RrReservationStrandedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + reservationKeyFilter []*big.Int + walletPubKeyHashFilter [][20]byte + ownerFilter []common.Address +} + +type reservationRouterReservationStrandedFunc func( + ReservationKey *big.Int, + WalletPubKeyHash [20]byte, + Owner common.Address, + AnchorAmount uint64, + blockNumber uint64, +) + +func (rss *RrReservationStrandedSubscription) OnEvent( + handler reservationRouterReservationStrandedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationStranded) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationKey, + event.WalletPubKeyHash, + event.Owner, + event.AnchorAmount, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rss *RrReservationStrandedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationStranded, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rss.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rss.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationStranded events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rss.contract.PastReservationStrandedEvents( + fromBlock, + nil, + rss.reservationKeyFilter, + rss.walletPubKeyHashFilter, + rss.ownerFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationStranded events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rss.contract.watchReservationStranded( + sink, + rss.reservationKeyFilter, + rss.walletPubKeyHashFilter, + rss.ownerFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationStranded( + sink chan *abi.ReservationRouterReservationStranded, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, + ownerFilter []common.Address, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationStranded( + &bind.WatchOpts{Context: ctx}, + sink, + reservationKeyFilter, + walletPubKeyHashFilter, + ownerFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationStranded had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationStranded failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationStrandedEvents( + startBlock uint64, + endBlock *uint64, + reservationKeyFilter []*big.Int, + walletPubKeyHashFilter [][20]byte, + ownerFilter []common.Address, +) ([]*abi.ReservationRouterReservationStranded, error) { + iterator, err := rr.contract.FilterReservationStranded( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + reservationKeyFilter, + walletPubKeyHashFilter, + ownerFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationStranded events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationStranded, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservationVaultUpdatedEvent( + opts *ethereum.SubscribeOpts, +) *RrReservationVaultUpdatedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservationVaultUpdatedSubscription{ + rr, + opts, + } +} + +type RrReservationVaultUpdatedSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts +} + +type reservationRouterReservationVaultUpdatedFunc func( + ReservationVault common.Address, + blockNumber uint64, +) + +func (rvus *RrReservationVaultUpdatedSubscription) OnEvent( + handler reservationRouterReservationVaultUpdatedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservationVaultUpdated) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.ReservationVault, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rvus.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rvus *RrReservationVaultUpdatedSubscription) Pipe( + sink chan *abi.ReservationRouterReservationVaultUpdated, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rvus.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rvus.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rvus.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservationVaultUpdated events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rvus.contract.PastReservationVaultUpdatedEvents( + fromBlock, + nil, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservationVaultUpdated events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rvus.contract.watchReservationVaultUpdated( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservationVaultUpdated( + sink chan *abi.ReservationRouterReservationVaultUpdated, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservationVaultUpdated( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservationVaultUpdated had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservationVaultUpdated failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservationVaultUpdatedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.ReservationRouterReservationVaultUpdated, error) { + iterator, err := rr.contract.FilterReservationVaultUpdated( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservationVaultUpdated events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservationVaultUpdated, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (rr *ReservationRouter) ReservedDepositMarkedStaleEvent( + opts *ethereum.SubscribeOpts, + depositKeyFilter []*big.Int, +) *RrReservedDepositMarkedStaleSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &RrReservedDepositMarkedStaleSubscription{ + rr, + opts, + depositKeyFilter, + } +} + +type RrReservedDepositMarkedStaleSubscription struct { + contract *ReservationRouter + opts *ethereum.SubscribeOpts + depositKeyFilter []*big.Int +} + +type reservationRouterReservedDepositMarkedStaleFunc func( + DepositKey *big.Int, + blockNumber uint64, +) + +func (rdmss *RrReservedDepositMarkedStaleSubscription) OnEvent( + handler reservationRouterReservedDepositMarkedStaleFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.ReservationRouterReservedDepositMarkedStale) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.DepositKey, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rdmss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rdmss *RrReservedDepositMarkedStaleSubscription) Pipe( + sink chan *abi.ReservationRouterReservedDepositMarkedStale, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rdmss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rdmss.contract.blockCounter.CurrentBlock() + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rdmss.opts.PastBlocks + + rrLogger.Infof( + "subscription monitoring fetching past ReservedDepositMarkedStale events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rdmss.contract.PastReservedDepositMarkedStaleEvents( + fromBlock, + nil, + rdmss.depositKeyFilter, + ) + if err != nil { + rrLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + rrLogger.Infof( + "subscription monitoring fetched [%v] past ReservedDepositMarkedStale events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rdmss.contract.watchReservedDepositMarkedStale( + sink, + rdmss.depositKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rr *ReservationRouter) watchReservedDepositMarkedStale( + sink chan *abi.ReservationRouterReservedDepositMarkedStale, + depositKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return rr.contract.WatchReservedDepositMarkedStale( + &bind.WatchOpts{Context: ctx}, + sink, + depositKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + rrLogger.Warnf( + "subscription to event ReservedDepositMarkedStale had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + rrLogger.Errorf( + "subscription to event ReservedDepositMarkedStale failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (rr *ReservationRouter) PastReservedDepositMarkedStaleEvents( + startBlock uint64, + endBlock *uint64, + depositKeyFilter []*big.Int, +) ([]*abi.ReservationRouterReservedDepositMarkedStale, error) { + iterator, err := rr.contract.FilterReservedDepositMarkedStale( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + depositKeyFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past ReservedDepositMarkedStale events: [%v]", + err, + ) + } + + events := make([]*abi.ReservationRouterReservedDepositMarkedStale, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} diff --git a/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go b/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go index b5d3591e01..c22b9b1c7c 100644 --- a/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go +++ b/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go @@ -585,4 +585,95 @@ func (wpv *WalletProposalValidator) ValidateRedemptionProposalAtBlock( return result, err } +func (wpv *WalletProposalValidator) ValidateReservationAnchorProposal( + arg_proposal abi.WalletProposalValidatorReservationAnchorProposal, + arg_depositExtraInfo abi.WalletProposalValidatorDepositExtraInfo, +) (bool, error) { + result, err := wpv.contract.ValidateReservationAnchorProposal( + wpv.callerOptions, + arg_proposal, + arg_depositExtraInfo, + ) + + if err != nil { + return result, wpv.errorResolver.ResolveError( + err, + wpv.callerOptions.From, + nil, + "validateReservationAnchorProposal", + arg_proposal, + arg_depositExtraInfo, + ) + } + + return result, err +} + +func (wpv *WalletProposalValidator) ValidateReservationAnchorProposalAtBlock( + arg_proposal abi.WalletProposalValidatorReservationAnchorProposal, + arg_depositExtraInfo abi.WalletProposalValidatorDepositExtraInfo, + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + wpv.callerOptions.From, + blockNumber, + nil, + wpv.contractABI, + wpv.caller, + wpv.errorResolver, + wpv.contractAddress, + "validateReservationAnchorProposal", + &result, + arg_proposal, + arg_depositExtraInfo, + ) + + return result, err +} + +func (wpv *WalletProposalValidator) ValidateReservationReanchorProposal( + arg_proposal abi.WalletProposalValidatorReservationReanchorProposal, +) (bool, error) { + result, err := wpv.contract.ValidateReservationReanchorProposal( + wpv.callerOptions, + arg_proposal, + ) + + if err != nil { + return result, wpv.errorResolver.ResolveError( + err, + wpv.callerOptions.From, + nil, + "validateReservationReanchorProposal", + arg_proposal, + ) + } + + return result, err +} + +func (wpv *WalletProposalValidator) ValidateReservationReanchorProposalAtBlock( + arg_proposal abi.WalletProposalValidatorReservationReanchorProposal, + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + wpv.callerOptions.From, + blockNumber, + nil, + wpv.contractABI, + wpv.caller, + wpv.errorResolver, + wpv.contractAddress, + "validateReservationReanchorProposal", + &result, + arg_proposal, + ) + + return result, err +} + // ------ Events ------- diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index 1c9eef1be0..ea6e9edb11 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -16,8 +16,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-core/internal/testutils" + tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi" "github.com/keep-network/keep-core/pkg/chain/local_v1" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tbtc" ) func TestComputeOperatorsIDsHash(t *testing.T) { @@ -533,3 +535,268 @@ func TestBuildMovedFundsKey(t *testing.T) { movedFundsKey.Text(16), ) } + +func TestConvertReservationFromAbiType(t *testing.T) { + ownerAddress := common.HexToAddress( + "0x1234567890AbcdEF1234567890aBcdef12345678", + ) + anchorTxHash := [32]byte{0x01, 0x02, 0x03, 0x04} + + validAbiReservation := tbtcabi.ReservationReservationRequest{ + Owner: ownerAddress, + MintedAmount: 100000, + AcceptedAt: 1700000000, + WalletPubKeyHash: [20]byte{0xaa, 0xbb, 0xcc}, + AnchorAmount: 99000, + ExpiresAt: 1700100000, + AnchorTxHash: anchorTxHash, + AnchorTxOutputIndex: 1, + State: 1, // Active + RequestNonce: 7, + RetryCredit: true, + DissolutionEligibleAt: 1700200000, + // CumulativeReanchorFee is intentionally dropped on the Go + // boundary (see the function doc comment); set it to a nonzero + // value to prove it never leaks into tbtc.Reservation. + CumulativeReanchorFee: 12345, + } + + t.Run("valid states", func(t *testing.T) { + var tests = map[string]struct { + abiState uint8 + expectedState tbtc.ReservationState + }{ + "unknown": {0, tbtc.ReservationStateUnknown}, + "active": {1, tbtc.ReservationStateActive}, + "pending": {2, tbtc.ReservationStateActionPending}, + "closed": {3, tbtc.ReservationStateClosed}, + "stranded": {4, tbtc.ReservationStateStranded}, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + abiReservation := validAbiReservation + abiReservation.State = test.abiState + + reservation, err := convertReservationFromAbiType(abiReservation) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if reservation.State != test.expectedState { + t.Errorf("expected state [%v], got [%v]", test.expectedState, reservation.State) + } + }) + } + }) + + t.Run("invalid state", func(t *testing.T) { + invalidAbiReservation := validAbiReservation + invalidAbiReservation.State = 255 + + reservation, err := convertReservationFromAbiType(invalidAbiReservation) + if reservation != nil { + t.Errorf("expected nil reservation, got [%+v]", reservation) + } + if err == nil { + t.Fatal("expected error, got nil") + } + }) +} + +func TestConvertReservationActionFromAbiType(t *testing.T) { + targetWalletPKH := [20]byte{0x11, 0x22, 0x33} + redeemerAddress := common.HexToAddress( + "0xAbCdEf1234567890abcDef1234567890AbCdEf12", + ) + actionDataHash := [32]byte{0xde, 0xad, 0xbe, 0xef} + + baseAbiAction := tbtcabi.ReservationReservationAction{ + TargetWalletPubKeyHash: targetWalletPKH, + RequestedAt: 1700000000, + TimeoutAt: 1700003600, + TxMaxFee: 5000, + State: 1, // Pending + FeePaid: true, + Redeemer: redeemerAddress, + Amount: 50000, + ActionDataHash: actionDataHash, + IsPartial: true, + } + + // The action-type-to-hash-field routing (redemption -> redeemer output + // script hash, dissolution -> expected main UTXO hash, everything else + // -> neither) is the one non-trivial branch in this converter; exercise + // all three shapes. + var tests = map[string]struct { + abiActionType uint8 + expectedActionType tbtc.ReservationActionType + expectedRedeemerOutputScriptHash [32]byte + expectedExpectedMainUtxoHash [32]byte + }{ + "redemption routes hash to redeemer output script": { + abiActionType: 2, + expectedActionType: tbtc.ReservationActionTypeRedemption, + expectedRedeemerOutputScriptHash: actionDataHash, + expectedExpectedMainUtxoHash: [32]byte{}, + }, + "dissolution routes hash to expected main utxo": { + abiActionType: 4, + expectedActionType: tbtc.ReservationActionTypeDissolution, + expectedRedeemerOutputScriptHash: [32]byte{}, + expectedExpectedMainUtxoHash: actionDataHash, + }, + "acceptance leaves both hash fields zero": { + abiActionType: 1, + expectedActionType: tbtc.ReservationActionTypeAcceptance, + expectedRedeemerOutputScriptHash: [32]byte{}, + expectedExpectedMainUtxoHash: [32]byte{}, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + abiAction := baseAbiAction + abiAction.ActionType = test.abiActionType + + action, err := convertReservationActionFromAbiType(abiAction) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + expected := &tbtc.ReservationAction{ + TargetWalletPublicKeyHash: targetWalletPKH, + RequestedAt: 1700000000, + TimeoutAt: 1700003600, + TxMaxFee: 5000, + ActionType: test.expectedActionType, + State: tbtc.ReservationActionStatePending, + FeePaid: true, + Redeemer: chain.Address(redeemerAddress.String()), + Amount: 50000, + RedeemerOutputScriptHash: test.expectedRedeemerOutputScriptHash, + ExpectedMainUtxoHash: test.expectedExpectedMainUtxoHash, + IsPartial: true, + } + + if !reflect.DeepEqual(expected, action) { + t.Errorf( + "unexpected action\nexpected: [%+v]\nactual: [%+v]\n", + expected, + action, + ) + } + }) + } + + t.Run("invalid action type", func(t *testing.T) { + abiAction := baseAbiAction + abiAction.ActionType = 255 + + action, err := convertReservationActionFromAbiType(abiAction) + if action != nil { + t.Errorf("expected nil action, got [%+v]", action) + } + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("invalid action state", func(t *testing.T) { + abiAction := baseAbiAction + abiAction.ActionType = 1 + abiAction.State = 255 + + action, err := convertReservationActionFromAbiType(abiAction) + if action != nil { + t.Errorf("expected nil action, got [%+v]", action) + } + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("valid action states", func(t *testing.T) { + var tests = map[string]struct { + abiState uint8 + expectedState tbtc.ReservationActionState + }{ + "unknown": {0, tbtc.ReservationActionStateUnknown}, + "pending": {1, tbtc.ReservationActionStatePending}, + "settled": {2, tbtc.ReservationActionStateSettled}, + "timed out": {3, tbtc.ReservationActionStateTimedOut}, + "vetoed": {4, tbtc.ReservationActionStateVetoed}, + "superseded": {5, tbtc.ReservationActionStateSuperseded}, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + abiAction := baseAbiAction + abiAction.ActionType = 1 + abiAction.State = test.abiState + + action, err := convertReservationActionFromAbiType(abiAction) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if action.State != test.expectedState { + t.Errorf("expected state [%v], got [%v]", test.expectedState, action.State) + } + }) + } + }) +} + +func TestConvertReservationParametersFromAbiType(t *testing.T) { + vaultAddress := common.HexToAddress( + "0x9876543210FeDcBa9876543210fEdCbA98765432", + ) + + abiParameters := struct { + ReservationVault common.Address + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 + }{ + ReservationVault: vaultAddress, + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + ReservationTermSeconds: 1209600, + ReservationDissolutionDelay: 3600, + ReservationMaxTotalAmount: 10000000, + ReservationTotalAmount: 2500000, + MaxReservationsPerWallet: 5, + ReservationActionTimeout: 86400, + ReservationRenewalWindowSeconds: 604800, + } + + parameters := convertReservationParametersFromAbiType(abiParameters) + + expected := &tbtc.ReservationParameters{ + ReservationVault: chain.Address(vaultAddress.String()), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + ReservationTermSeconds: 1209600, + ReservationDissolutionDelay: 3600, + ReservationMaxTotalAmount: 10000000, + ReservationTotalAmount: 2500000, + MaxReservationsPerWallet: 5, + ReservationActionTimeout: 86400, + ReservationRenewalWindowSeconds: 604800, + } + + if !reflect.DeepEqual(expected, parameters) { + t.Errorf( + "unexpected parameters\nexpected: [%+v]\nactual: [%+v]\n", + expected, + parameters, + ) + } +} diff --git a/pkg/clientinfo/performance.go b/pkg/clientinfo/performance.go index bcc28137ca..54de8005f5 100644 --- a/pkg/clientinfo/performance.go +++ b/pkg/clientinfo/performance.go @@ -37,6 +37,12 @@ type PerformanceMetrics struct { registry *Registry cancel context.CancelFunc + // reservationsEnabled mirrors tbtc.Config.Reservations.Enabled. Gates + // registration of the reservation-specific wallet action metrics + // (reservation_anchor, reservation_reanchor) so a non-reservation + // deployment's metric surface does not change - see GetAllWalletActionTypes. + reservationsEnabled bool + // Counters track cumulative counts of events countersMutex sync.RWMutex counters map[string]*counter @@ -75,14 +81,21 @@ const ( ) // NewPerformanceMetrics creates a new performance metrics instance. -func NewPerformanceMetrics(ctx context.Context, registry *Registry) *PerformanceMetrics { +// reservationsEnabled gates registration of the reservation-specific wallet +// action metrics (see GetAllWalletActionTypes / registerAllMetrics). +func NewPerformanceMetrics( + ctx context.Context, + registry *Registry, + reservationsEnabled bool, +) *PerformanceMetrics { ctx, cancel := context.WithCancel(ctx) pm := &PerformanceMetrics{ - registry: registry, - cancel: cancel, - counters: make(map[string]*counter), - histograms: make(map[string]*histogram), - gauges: make(map[string]*gauge), + registry: registry, + cancel: cancel, + reservationsEnabled: reservationsEnabled, + counters: make(map[string]*counter), + histograms: make(map[string]*histogram), + gauges: make(map[string]*gauge), } // Register all metrics upfront with 0 values so they appear in /metrics endpoint @@ -179,7 +192,13 @@ func (pm *PerformanceMetrics) registerAllMetrics() { // Register per-action type wallet metrics // For each action type, register: total, success_total, failed_total, duration_seconds - for _, actionType := range GetAllWalletActionTypes() { + actionTypes := GetAllWalletActionTypes() + if pm.reservationsEnabled { + actionTypes = append(actionTypes, GetReservationWalletActionTypes()...) + } + + for _, actionType := range actionTypes { + actionCounters := []string{ WalletActionMetricName(actionType, "total"), WalletActionMetricName(actionType, "success_total"), @@ -746,8 +765,8 @@ func GetAllNetworkJoinFailureReasons() []string { } } -// GetAllWalletActionTypes returns all wallet action types that should be tracked. -// ActionNoop is excluded as it's a no-op action. +// GetAllWalletActionTypes returns all non-reservation wallet action types that +// should be tracked. ActionNoop is excluded as it's a no-op action. func GetAllWalletActionTypes() []string { return []string{ "heartbeat", @@ -757,3 +776,12 @@ func GetAllWalletActionTypes() []string { "moved_funds_sweep", } } + +// GetReservationWalletActionTypes returns all reservation-specific wallet +// action types that should be tracked. +func GetReservationWalletActionTypes() []string { + return []string{ + "reservation_anchor", + "reservation_reanchor", + } +} diff --git a/pkg/clientinfo/performance_test.go b/pkg/clientinfo/performance_test.go index 5ebf253288..622b1f86e5 100644 --- a/pkg/clientinfo/performance_test.go +++ b/pkg/clientinfo/performance_test.go @@ -17,7 +17,7 @@ func TestConcurrentCounterIncrement(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) const ( numGoroutines = 100 @@ -51,7 +51,7 @@ func TestConcurrentCounterDifferentMetrics(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) const ( numGoroutines = 50 @@ -115,7 +115,7 @@ func TestConcurrentDurationRecording(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) const ( numGoroutines = 50 @@ -169,7 +169,7 @@ func TestConcurrentGaugeSet(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) const ( numGoroutines = 100 @@ -205,7 +205,7 @@ func TestConcurrentDifferentOperations(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) const ( numGoroutines = 30 @@ -264,7 +264,7 @@ func TestHistogramBucketPlacement(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) metricName := "test_duration_seconds" @@ -320,7 +320,7 @@ func TestMetricsInitialization(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) // Test counters counters := []string{ @@ -359,7 +359,7 @@ func TestContextCancelation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) // Cancel context immediately cancel() @@ -404,7 +404,7 @@ func TestJoinFailureAndOnChainCountersRegistered(t *testing.T) { defer cancel() registry := &Registry{keepclientinfo.NewRegistry(), ctx} - pm := NewPerformanceMetrics(ctx, registry) + pm := NewPerformanceMetrics(ctx, registry, true) expectedCounters := []string{MetricFirewallOnChainChecksTotal} for _, reason := range GetAllNetworkJoinFailureReasons() { @@ -430,3 +430,114 @@ func TestJoinFailureAndOnChainCountersRegistered(t *testing.T) { } } } + +func TestWalletActionMetricsRegistered(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + registry := &Registry{keepclientinfo.NewRegistry(), ctx} + pm := NewPerformanceMetrics(ctx, registry, true) + + expectedActionTypes := []string{ + "heartbeat", + "deposit_sweep", + "redemption", + "moving_funds", + "moved_funds_sweep", + "reservation_anchor", + "reservation_reanchor", + } + + for _, actionType := range expectedActionTypes { + for _, metricType := range []string{ + "total", + "success_total", + "failed_total", + } { + metricName := WalletActionMetricName(actionType, metricType) + + pm.countersMutex.RLock() + _, exists := pm.counters[metricName] + pm.countersMutex.RUnlock() + if !exists { + t.Errorf("counter %s should be registered upfront", metricName) + } + } + + durationMetricName := WalletActionMetricName( + actionType, + "duration_seconds", + ) + pm.histogramsMutex.RLock() + _, exists := pm.histograms[durationMetricName] + pm.histogramsMutex.RUnlock() + if !exists { + t.Errorf( + "histogram %s should be registered upfront", + durationMetricName, + ) + } + } +} + +func TestWalletActionMetricsNotRegisteredWhenReservationsDisabled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + registry := &Registry{keepclientinfo.NewRegistry(), ctx} + pm := NewPerformanceMetrics(ctx, registry, false) + + nonReservationActionTypes := []string{ + "heartbeat", + "deposit_sweep", + "redemption", + "moving_funds", + "moved_funds_sweep", + } + reservationActionTypes := []string{ + "reservation_anchor", + "reservation_reanchor", + } + + for _, actionType := range nonReservationActionTypes { + metricName := WalletActionMetricName(actionType, "total") + pm.countersMutex.RLock() + _, exists := pm.counters[metricName] + pm.countersMutex.RUnlock() + if !exists { + t.Errorf( + "counter %s should still be registered when reservations "+ + "are disabled", + metricName, + ) + } + } + + for _, actionType := range reservationActionTypes { + for _, metricType := range []string{"total", "success_total", "failed_total"} { + metricName := WalletActionMetricName(actionType, metricType) + pm.countersMutex.RLock() + _, exists := pm.counters[metricName] + pm.countersMutex.RUnlock() + if exists { + t.Errorf( + "counter %s should not be registered when reservations "+ + "are disabled", + metricName, + ) + } + } + + durationMetricName := WalletActionMetricName(actionType, "duration_seconds") + pm.histogramsMutex.RLock() + _, exists := pm.histograms[durationMetricName] + pm.histogramsMutex.RUnlock() + if exists { + t.Errorf( + "histogram %s should not be registered when reservations "+ + "are disabled", + durationMetricName, + ) + } + } +} diff --git a/pkg/maintainer/spv/bitcoin_chain_test.go b/pkg/maintainer/spv/bitcoin_chain_test.go index 35ce5acf6d..ceaa292334 100644 --- a/pkg/maintainer/spv/bitcoin_chain_test.go +++ b/pkg/maintainer/spv/bitcoin_chain_test.go @@ -47,6 +47,7 @@ type localBitcoinChain struct { transactions []*bitcoin.Transaction transactionConfirmations map[bitcoin.Hash]uint blockHeaders map[uint]*bitcoin.BlockHeader + coinbaseTxHash *bitcoin.Hash } func newLocalBitcoinChain() *localBitcoinChain { @@ -138,11 +139,20 @@ func (lbc *localBitcoinChain) GetBlockHeader(blockHeight uint) ( return nil, fmt.Errorf("block header does not exist") } +// GetTransactionMerkleProof returns a trivial, always-valid proof: an empty +// merkle-node list means the given transaction hash is treated as the +// block's merkle root directly, at position 0. Sufficient to let +// bitcoin.AssembleSpvProof complete against this fake chain without a real +// merkle-tree fixture. func (lbc *localBitcoinChain) GetTransactionMerkleProof( transactionHash bitcoin.Hash, blockHeight uint, ) (*bitcoin.TransactionMerkleProof, error) { - panic("unsupported") + return &bitcoin.TransactionMerkleProof{ + BlockHeight: blockHeight, + MerkleNodes: nil, + Position: 0, + }, nil } func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( @@ -213,13 +223,33 @@ func (lbc *localBitcoinChain) EstimateSatPerVByteFee(blocks uint32) ( panic("unsupported") } +// GetCoinbaseTxHash returns the hash previously installed via +// setCoinbaseTxHash. Panics if never set, matching this fake chain's +// convention for exercising an unconfigured dependency. func (lbc *localBitcoinChain) GetCoinbaseTxHash(blockHeight uint) ( bitcoin.Hash, error, ) { + lbc.mutex.Lock() + defer lbc.mutex.Unlock() + + if lbc.coinbaseTxHash != nil { + return *lbc.coinbaseTxHash, nil + } panic("unsupported") } +// setCoinbaseTxHash installs the hash GetCoinbaseTxHash returns for every +// block height. The hash must belong to a transaction already known to +// GetTransaction (e.g. via BroadcastTransaction) since AssembleSpvProof +// looks the coinbase transaction up by this hash immediately after. +func (lbc *localBitcoinChain) setCoinbaseTxHash(hash bitcoin.Hash) { + lbc.mutex.Lock() + defer lbc.mutex.Unlock() + + lbc.coinbaseTxHash = &hash +} + func (lbc *localBitcoinChain) addBlockHeader( blockNumber uint, blockHeader *bitcoin.BlockHeader, diff --git a/pkg/maintainer/spv/chain.go b/pkg/maintainer/spv/chain.go index c9e060e9bf..e2023e2f84 100644 --- a/pkg/maintainer/spv/chain.go +++ b/pkg/maintainer/spv/chain.go @@ -4,6 +4,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" @@ -85,6 +86,69 @@ type Chain interface { mainUTXO bitcoin.UnspentTransactionOutput, ) error + // SubmitReservationProof submits an SPV proof for the given reservation + // action generation. proofType selects between Acceptance, Redemption, + // Reanchor, and Dissolution proofs; m1 invokes only Acceptance (1) and + // Reanchor (3). The call is restricted to the SPV maintainer registered + // against the Bridge. + SubmitReservationProof( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + proof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, + ) error + + // NotifyReservationActionTimeout notifies the Bridge that the timeout + // for the given reservation action generation has elapsed without the + // SPV proof being submitted. The walletMembersIDs carry the operator + // IDs of the wallet that was authorized for the action. + NotifyReservationActionTimeout( + reservationKey *big.Int, + walletMembersIDs []uint32, + ) error + + // NotifyStaleReservedDeposit notifies the Bridge that the given reserved + // deposit's wallet did not anchor it within the reservation-action + // timeout and should be released back to the default sweeping path. + NotifyStaleReservedDeposit(depositKey *big.Int) error + + // NotifyReservationStranded notifies the Bridge that the wallet + // custodying the given reservation has been closed or terminated and + // the anchor is therefore stranded. + NotifyReservationStranded(reservationKey *big.Int) error + + // GetReservation gets the on-chain reservation record for the given + // reservation key. Returns an error if the reservation was not found. + GetReservation(reservationKey *big.Int) (*tbtc.Reservation, error) + + // GetReservationAction gets the on-chain action record for the given + // reservation key and request nonce. Returns an error if the action + // generation was not found. + GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, + ) (*tbtc.ReservationAction, error) + + // ReservationParameters gets the current on-chain values of the Bridge + // reservation parameters. + ReservationParameters() (*tbtc.ReservationParameters, error) + + // WalletReservations returns the reservation keys for all reservations + // currently custodied by the given wallet. + WalletReservations(walletPublicKeyHash [20]byte) ([]*big.Int, error) + + // IsReservedDeposit returns true if the given deposit was revealed + // with the reservation vault address and is therefore a reservation + // rather than a default deposit. + IsReservedDeposit(depositKey *big.Int) (bool, error) + + // ReservedDepositWallet returns the wallet public key hash to which the + // given reserved deposit was revealed. Returns the zero hash if the + // deposit is not a reserved deposit. + ReservedDepositWallet(depositKey *big.Int) ([20]byte, error) + // PastDepositRevealedEvents fetches past deposit reveal events according // to the provided filter or unfiltered if the filter is nil. Returned // events are sorted by the block number in the ascending order, i.e. the @@ -109,4 +173,31 @@ type Chain interface { PastMovingFundsCommitmentSubmittedEvents( filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, ) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) + + // PastReservationAcceptanceRequestedEvents fetches past + // ReservationAcceptanceRequested events according to the provided filter + // or unfiltered if the filter is nil. Returned events are sorted by the + // block number in the ascending order. + PastReservationAcceptanceRequestedEvents( + filter *tbtc.ReservationAcceptanceRequestedEventFilter, + ) ([]*tbtc.ReservationAcceptanceRequestedEvent, error) + + // PastReservationReanchorRequestedEvents fetches past + // ReservationReanchorRequested events according to the provided filter + // or unfiltered if the filter is nil. Returned events are sorted by the + // block number in the ascending order. + PastReservationReanchorRequestedEvents( + filter *tbtc.ReservationReanchorRequestedEventFilter, + ) ([]*tbtc.ReservationReanchorRequestedEvent, error) + + // PastNewWalletRegisteredEvents fetches past NewWalletRegistered events + // according to the provided filter or unfiltered if the filter is nil. + // Returned events are sorted by the block number in the ascending order. + PastNewWalletRegisteredEvents( + filter *tbtc.NewWalletRegisteredEventFilter, + ) ([]*tbtc.NewWalletRegisteredEvent, error) + + // BuildDepositKey calculates the key used by the Bridge to store a + // deposit request, which is a unique identifier for a deposit on-chain. + BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int } diff --git a/pkg/maintainer/spv/chain_test.go b/pkg/maintainer/spv/chain_test.go index 2a0bcc0a89..a8e93de377 100644 --- a/pkg/maintainer/spv/chain_test.go +++ b/pkg/maintainer/spv/chain_test.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" @@ -43,6 +44,33 @@ type submittedMovedFundsSweepProof struct { mainUTXO bitcoin.UnspentTransactionOutput } +// submittedReservationStranded records a NotifyReservationStranded call for +// assertion in tests. +type submittedReservationStranded struct { + reservationKey *big.Int +} + +// submittedStaleReservedDeposit records a NotifyStaleReservedDeposit call for +// assertion in tests. +type submittedStaleReservedDeposit struct { + depositKey *big.Int +} + +// submittedReservationActionTimeout records a NotifyReservationActionTimeout +// call for assertion in tests. +type submittedReservationActionTimeout struct { + reservationKey *big.Int + walletMembersIDs []uint32 +} + +// reservedDepositRecord is the local-chain-side booking for a reserved +// deposit. WalletPublicKeyHash is the wallet currently assigned to the +// deposit; IsReserved drives the IsReservedDeposit return value. +type reservedDepositRecord struct { + walletPublicKeyHash [20]byte + isReserved bool +} + type localChain struct { mutex sync.Mutex @@ -59,10 +87,51 @@ type localChain struct { pastDepositRevealedEvents map[[32]byte][]*tbtc.DepositRevealedEvent pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent + // Reservation watcher state. Indexed by [16]byte / [24]byte map keys + // derived from the relevant big.Int so they fit the map type without + // per-test marshalling. + walletReservations map[[20]byte][]*big.Int + reservations map[string]*tbtc.Reservation + reservationActions map[string]*tbtc.ReservationAction + reservedDeposits map[string]*reservedDepositRecord + submittedStrandedKeys []*big.Int + submittedStaleDeposits []*big.Int + submittedActionTimeouts []*submittedReservationActionTimeout + reservationParameters *tbtc.ReservationParameters + + // Error-injection fields for the reservation watcher chain-error + // passthrough tests: nil (the default) means the corresponding method + // falls through to its normal, table-driven behavior. + walletReservationsErr error + isReservedDepositErr error + reservedDepositWalletErr error + notifyReservationActionTimeoutErr error + notifyStaleReservedDepositErr error + pastNewWalletRegisteredEventsErr error + notifyReservationStrandedErrByKey map[string]error + + // Wallet registration and pending-action-request event state for the + // watcher dispatch and reservation proof loop tests. + newWalletRegisteredEvents []*tbtc.NewWalletRegisteredEvent + reservationAcceptanceRequestedEvents []*tbtc.ReservationAcceptanceRequestedEvent + reservationReanchorRequestedEvents []*tbtc.ReservationReanchorRequestedEvent + txProofDifficultyFactor *big.Int currentEpoch uint64 currentEpochDifficulty *big.Int previousEpochDifficulty *big.Int + // submitReservationProofHook, when non-nil, overrides the default + // success stub and gives the test full control over + // SubmitReservationProof behavior (e.g. to assert arguments or return + // an error). + submitReservationProofHook func( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + proof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, + ) error } func newLocalChain() *localChain { @@ -74,9 +143,17 @@ func newLocalChain() *localChain { submittedRedemptionProofs: make([]*submittedRedemptionProof, 0), submittedDepositSweepProofs: make([]*submittedDepositSweepProof, 0), submittedMovingFundsProofs: make([]*submittedMovingFundsProof, 0), + submittedMovedFundsSweepProofs: make([]*submittedMovedFundsSweepProof, 0), pastRedemptionRequestedEvents: make(map[[32]byte][]*tbtc.RedemptionRequestedEvent), pastDepositRevealedEvents: make(map[[32]byte][]*tbtc.DepositRevealedEvent), pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent), + walletReservations: make(map[[20]byte][]*big.Int), + reservations: make(map[string]*tbtc.Reservation), + reservationActions: make(map[string]*tbtc.ReservationAction), + reservedDeposits: make(map[string]*reservedDepositRecord), + submittedStrandedKeys: make([]*big.Int, 0), + submittedStaleDeposits: make([]*big.Int, 0), + submittedActionTimeouts: make([]*submittedReservationActionTimeout, 0), } } @@ -720,3 +797,450 @@ func (mbc *mockBlockCounter) SetCurrentBlock(block uint64) { func (mbc *mockBlockCounter) WatchBlocks(ctx context.Context) <-chan uint64 { panic("unsupported") } + +// SubmitReservationProof is a stub matching the reservation additions on +// the production Chain interface. +func (lc *localChain) SubmitReservationProof( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + proof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, +) error { + if lc.submitReservationProofHook != nil { + return lc.submitReservationProofHook( + proofType, + txInfo, + proof, + mainUtxo, + reservationKey, + requestNonce, + ) + } + panic("unsupported") +} + +// NotifyReservationActionTimeout records the notification for assertion in +// tests. The action-timeout watcher builder invokes this through the +// Chain interface to drive the notification path. +func (lc *localChain) NotifyReservationActionTimeout( + reservationKey *big.Int, + walletMembersIDs []uint32, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.submittedActionTimeouts = append( + lc.submittedActionTimeouts, + &submittedReservationActionTimeout{ + reservationKey: reservationKey, + walletMembersIDs: walletMembersIDs, + }, + ) + + return lc.notifyReservationActionTimeoutErr +} + +// getSubmittedReservationActionTimeouts returns the recorded action-timeout +// notifications in submission order. +func (lc *localChain) getSubmittedReservationActionTimeouts() []*submittedReservationActionTimeout { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + out := make([]*submittedReservationActionTimeout, len(lc.submittedActionTimeouts)) + copy(out, lc.submittedActionTimeouts) + return out +} + +// NotifyStaleReservedDeposit records the notification for assertion in +// tests. The stale-deposit watcher builder invokes this through the +// Chain interface to drive the notification path. +func (lc *localChain) NotifyStaleReservedDeposit(depositKey *big.Int) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.submittedStaleDeposits = append( + lc.submittedStaleDeposits, + depositKey, + ) + + return lc.notifyStaleReservedDepositErr +} + +// getSubmittedStaleReservedDeposits returns the recorded stale-deposit +// notifications in submission order. +func (lc *localChain) getSubmittedStaleReservedDeposits() []*big.Int { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + out := make([]*big.Int, len(lc.submittedStaleDeposits)) + copy(out, lc.submittedStaleDeposits) + return out +} + +// NotifyReservationStranded records the notification for assertion in +// tests. The stranding watcher builder invokes this through the Chain +// interface to drive the notification path. +func (lc *localChain) NotifyReservationStranded(reservationKey *big.Int) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if err, ok := lc.notifyReservationStrandedErrByKey[reservationKey.String()]; ok { + return err + } + + lc.submittedStrandedKeys = append( + lc.submittedStrandedKeys, + reservationKey, + ) + + return nil +} + +// getSubmittedReservationStrandedKeys returns the recorded stranding +// notifications in submission order. +func (lc *localChain) getSubmittedReservationStrandedKeys() []*big.Int { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + out := make([]*big.Int, len(lc.submittedStrandedKeys)) + copy(out, lc.submittedStrandedKeys) + return out +} + +// GetReservation returns the reservation previously installed via +// setReservation. Returns an error if the reservation is not set, matching +// the production contract behavior. +func (lc *localChain) GetReservation( + reservationKey *big.Int, +) (*tbtc.Reservation, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key := bigIntKey(reservationKey) + reservation, ok := lc.reservations[key] + if !ok { + return nil, fmt.Errorf("no reservation for given key") + } + return reservation, nil +} + +// setReservation installs a reservation for GetReservation to return. +func (lc *localChain) setReservation( + reservationKey *big.Int, + reservation *tbtc.Reservation, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservations[bigIntKey(reservationKey)] = reservation +} + +// GetReservationAction returns the reservation action previously installed +// via setReservationAction. Returns an error if the action is not set. +func (lc *localChain) GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, +) (*tbtc.ReservationAction, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key := buildReservationActionKey(reservationKey, requestNonce) + action, ok := lc.reservationActions[key] + if !ok { + return nil, fmt.Errorf("no action for given reservation/nonce") + } + return action, nil +} + +// setReservationAction installs a reservation action for GetReservationAction +// to return. +func (lc *localChain) setReservationAction( + reservationKey *big.Int, + requestNonce uint64, + action *tbtc.ReservationAction, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservationActions[buildReservationActionKey(reservationKey, requestNonce)] = action +} + +// buildReservationActionKey produces a map key encoding the reservation +// identifier and the nonce, using the big.Int's full base-16 text +// representation so distinct reservation keys can never collide. +func buildReservationActionKey( + reservationKey *big.Int, + requestNonce uint64, +) string { + return fmt.Sprintf("%s/%d", bigIntKey(reservationKey), requestNonce) +} + +// bigIntKey returns a map key string from a big.Int using its full base-16 +// text representation, so distinct values can never collide. Returns the +// empty string for nil. +func bigIntKey(v *big.Int) string { + if v == nil { + return "" + } + return v.Text(16) +} + +// ReservationParameters returns the reservation parameters previously +// installed via setReservationParameters, or a default set if none was set. +func (lc *localChain) ReservationParameters() ( + *tbtc.ReservationParameters, + error, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if lc.reservationParameters != nil { + return lc.reservationParameters, nil + } + return &tbtc.ReservationParameters{ + ReservationActionTimeout: 3600, + }, nil +} + +// setReservationParameters installs reservation parameters for +// ReservationParameters to return. +func (lc *localChain) setReservationParameters( + params *tbtc.ReservationParameters, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservationParameters = params +} + +// WalletReservations returns the reservation keys previously installed via +// setWalletReservations. The slice is a copy so the caller can mutate it +// without affecting the local chain. +func (lc *localChain) WalletReservations( + walletPublicKeyHash [20]byte, +) ([]*big.Int, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if lc.walletReservationsErr != nil { + return nil, lc.walletReservationsErr + } + + keys := lc.walletReservations[walletPublicKeyHash] + out := make([]*big.Int, len(keys)) + copy(out, keys) + return out, nil +} + +// setWalletReservations installs the list of reservation keys for a wallet. +func (lc *localChain) setWalletReservations( + walletPublicKeyHash [20]byte, + keys []*big.Int, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.walletReservations[walletPublicKeyHash] = append( + []*big.Int{}, + keys..., + ) +} + +// IsReservedDeposit returns whether the deposit was previously booked via +// setReservedDeposit. +func (lc *localChain) IsReservedDeposit( + depositKey *big.Int, +) (bool, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if lc.isReservedDepositErr != nil { + return false, lc.isReservedDepositErr + } + + record, ok := lc.reservedDeposits[bigIntKey(depositKey)] + if !ok { + return false, nil + } + return record.isReserved, nil +} + +// ReservedDepositWallet returns the wallet previously assigned to the +// deposit via setReservedDeposit. +func (lc *localChain) ReservedDepositWallet( + depositKey *big.Int, +) ([20]byte, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if lc.reservedDepositWalletErr != nil { + return [20]byte{}, lc.reservedDepositWalletErr + } + + record, ok := lc.reservedDeposits[bigIntKey(depositKey)] + if !ok { + return [20]byte{}, nil + } + return record.walletPublicKeyHash, nil +} + +// setReservedDeposit installs the reserved-deposit booking for +// IsReservedDeposit and ReservedDepositWallet to return. +func (lc *localChain) setReservedDeposit( + depositKey *big.Int, + walletPublicKeyHash [20]byte, + isReserved bool, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservedDeposits[bigIntKey(depositKey)] = &reservedDepositRecord{ + walletPublicKeyHash: walletPublicKeyHash, + isReserved: isReserved, + } +} + +func (lc *localChain) PastReservationAcceptanceRequestedEvents( + filter *tbtc.ReservationAcceptanceRequestedEventFilter, +) ([]*tbtc.ReservationAcceptanceRequestedEvent, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + var result []*tbtc.ReservationAcceptanceRequestedEvent + for _, event := range lc.reservationAcceptanceRequestedEvents { + if filter != nil && event.BlockNumber < filter.StartBlock { + continue + } + if filter != nil && len(filter.ReservationKey) > 0 { + matched := false + for _, key := range filter.ReservationKey { + if key.Cmp(event.ReservationKey) == 0 { + matched = true + break + } + } + if !matched { + continue + } + } + result = append(result, event) + } + + return result, nil +} + +func (lc *localChain) addReservationAcceptanceRequestedEvent( + event *tbtc.ReservationAcceptanceRequestedEvent, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservationAcceptanceRequestedEvents = append( + lc.reservationAcceptanceRequestedEvents, + event, + ) +} + +func (lc *localChain) PastReservationReanchorRequestedEvents( + filter *tbtc.ReservationReanchorRequestedEventFilter, +) ([]*tbtc.ReservationReanchorRequestedEvent, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + var result []*tbtc.ReservationReanchorRequestedEvent + for _, event := range lc.reservationReanchorRequestedEvents { + if filter != nil && event.BlockNumber < filter.StartBlock { + continue + } + if filter != nil && len(filter.ReservationKey) > 0 { + matched := false + for _, key := range filter.ReservationKey { + if key.Cmp(event.ReservationKey) == 0 { + matched = true + break + } + } + if !matched { + continue + } + } + result = append(result, event) + } + + return result, nil +} + +func (lc *localChain) addReservationReanchorRequestedEvent( + event *tbtc.ReservationReanchorRequestedEvent, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservationReanchorRequestedEvents = append( + lc.reservationReanchorRequestedEvents, + event, + ) +} + +func (lc *localChain) PastNewWalletRegisteredEvents( + filter *tbtc.NewWalletRegisteredEventFilter, +) ([]*tbtc.NewWalletRegisteredEvent, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if lc.pastNewWalletRegisteredEventsErr != nil { + return nil, lc.pastNewWalletRegisteredEventsErr + } + + var result []*tbtc.NewWalletRegisteredEvent + for _, event := range lc.newWalletRegisteredEvents { + if filter != nil && event.BlockNumber < filter.StartBlock { + continue + } + if filter != nil && len(filter.EcdsaWalletID) > 0 { + matched := false + for _, id := range filter.EcdsaWalletID { + if id == event.EcdsaWalletID { + matched = true + break + } + } + if !matched { + continue + } + } + result = append(result, event) + } + + return result, nil +} + +func (lc *localChain) addNewWalletRegisteredEvent( + event *tbtc.NewWalletRegisteredEvent, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.newWalletRegisteredEvents = append(lc.newWalletRegisteredEvents, event) +} + +func (lc *localChain) setPastNewWalletRegisteredEventsErr(err error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.pastNewWalletRegisteredEventsErr = err +} + +// BuildDepositKey is a test-double implementation independent of the +// production keccak256-based algorithm (pkg/chain/ethereum's unexported +// buildDepositKey): only self-consistency within this fake chain matters +// for unit tests, since nothing here cross-checks against a real contract. +func (lc *localChain) BuildDepositKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) *big.Int { + key := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + return new(big.Int).SetBytes(key[:]) +} diff --git a/pkg/maintainer/spv/config.go b/pkg/maintainer/spv/config.go index 49cdfe40d9..a4a2654e8d 100644 --- a/pkg/maintainer/spv/config.go +++ b/pkg/maintainer/spv/config.go @@ -2,6 +2,8 @@ package spv import ( "time" + + "github.com/keep-network/keep-core/pkg/tbtc" ) const ( @@ -65,4 +67,15 @@ type Config struct { // IdleBackoffTime is a wait time which should be applied when there are no // more transaction proofs to submit. IdleBackoffTime time.Duration + + // Reservations controls SPV proof submission for reservation acceptance + // and re-anchor action generations. + // + // OPERATOR NOTE: This flag only controls SPV proof submission in the + // maintainer process. Proposal generation and watcher wiring in the client + // process are gated by the separate Tbtc.Reservations.Enabled flag. + // An operator MUST enable BOTH flags ([Tbtc.Reservations] in the client + // and [Maintainer.Spv.Reservations] in the maintainer) for the reservation + // feature to work end-to-end. + Reservations tbtc.ReservationsConfig } diff --git a/pkg/maintainer/spv/reservation_acceptance_proof.go b/pkg/maintainer/spv/reservation_acceptance_proof.go new file mode 100644 index 0000000000..bcc2256b76 --- /dev/null +++ b/pkg/maintainer/spv/reservation_acceptance_proof.go @@ -0,0 +1,75 @@ +package spv + +import ( + "math/big" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// ProofTypeReservationAcceptance is the value passed to +// SubmitReservationProof as proofType for a reservation acceptance SPV +// proof. The numeric value mirrors the on-chain ReservationProofType enum +// (1 = Acceptance). +const ProofTypeReservationAcceptance uint8 = 1 + +// SubmitReservationAcceptanceProof drives the SPV proof submission for a +// reservation acceptance action generation. The caller (the reservation +// proof loop) supplies the (reservationKey, requestNonce) pair of the +// on-chain action generation it is proving, plus the Bitcoin transaction +// hash of the anchor transaction already signed and broadcast by the wallet +// coordinator. The proof is fetched from btcChain, the anchor transaction +// is rebuilt locally to extract the deposit UTXO that was anchored, and the +// proof is submitted directly to the Bridge via the SPV maintainer's +// SubmitReservationProof entry point (not via MaintainerProxy: reservations +// are not reimbursed). +// +// requiredConfirmations must be > 0; the SPV maintainer relies on it to +// assemble the proof. +func SubmitReservationAcceptanceProof( + transactionHash bitcoin.Hash, + requiredConfirmations uint, + reservationKey *big.Int, + requestNonce uint64, + btcChain bitcoin.Chain, + spvChain Chain, +) error { + return submitReservationAcceptanceProof( + transactionHash, + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + bitcoin.AssembleSpvProof, + getGlobalMetricsRecorder(), + ) +} + +func submitReservationAcceptanceProof( + transactionHash bitcoin.Hash, + requiredConfirmations uint, + reservationKey *big.Int, + requestNonce uint64, + btcChain bitcoin.Chain, + spvChain Chain, + spvProofAssembler spvProofAssembler, + metricsRecorder interface { + IncrementCounter(name string, value float64) + }, +) error { + return submitReservationActionProof( + transactionHash, + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + spvProofAssembler, + metricsRecorder, + ProofTypeReservationAcceptance, + "reservation_acceptance_proof", + tbtc.ReservationActionTypeAcceptance, + "acceptance", + ) +} diff --git a/pkg/maintainer/spv/reservation_acceptance_proof_test.go b/pkg/maintainer/spv/reservation_acceptance_proof_test.go new file mode 100644 index 0000000000..fcb2be5ed5 --- /dev/null +++ b/pkg/maintainer/spv/reservation_acceptance_proof_test.go @@ -0,0 +1,239 @@ +package spv + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// TestSubmitReservationAcceptanceProof verifies that +// submitReservationAcceptanceProof correctly parses a 1-input-1-output +// reservation acceptance (anchor) transaction, looks up the matching +// reservation action generation, and submits the SPV proof to the chain. +// It also covers the failure paths for missing action, mismatched action +// type, and wrong target wallet. +func TestSubmitReservationAcceptanceProof(t *testing.T) { + requiredConfirmations := uint(6) + + btcChain := newLocalBitcoinChain() + spvChain := newLocalChain() + + // Funding transaction that the anchor transaction spends (the reserved + // deposit's own UTXO). + fundingTx := &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 600000, + PublicKeyScript: []byte{}, + }}, + } + if err := btcChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + fundingTxHash := fundingTx.Hash() + + // Anchor transaction: 1 input spending the deposit's funding UTXO, 1 + // output paying the accepting wallet. + walletPKH := [20]byte{0x92, 0xa6, 0xec, 0x88, 0x9a, 0x8f, 0xa3, 0x4f, 0x73, 0x1e, 0x63, 0x9e, 0xde, 0xde, 0x4c, 0x75, 0xe1, 0x84, 0x30, 0x7c} + walletScript, err := bitcoin.PayToWitnessPublicKeyHash(walletPKH) + if err != nil { + t.Fatal(err) + } + + anchorTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 590000, + PublicKeyScript: walletScript, + }}, + } + if err := btcChain.BroadcastTransaction(anchorTx); err != nil { + t.Fatal(err) + } + + proof := &bitcoin.SpvProof{ + MerkleProof: []byte{0x01}, + TxIndexInBlock: 2, + BitcoinHeaders: []byte{0x03}, + } + + mockSpvProofAssembler := func( + hash bitcoin.Hash, + confirmations uint, + btcChain bitcoin.Chain, + ) (*bitcoin.Transaction, *bitcoin.SpvProof, error) { + if hash == anchorTx.Hash() && confirmations == requiredConfirmations { + return anchorTx, proof, nil + } + return nil, nil, fmt.Errorf("unexpected proof assembly request") + } + + reservationKey := big.NewInt(43) + requestNonce := uint64(1) + + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeAcceptance, + State: tbtc.ReservationActionStatePending, + TargetWalletPublicKeyHash: walletPKH, + }) + + spvChain.submitReservationProofHook = func( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + txProof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + rk *big.Int, + rn uint64, + ) error { + if proofType != ProofTypeReservationAcceptance { + t.Errorf("unexpected proof type: got %d, want %d", proofType, ProofTypeReservationAcceptance) + } + if rk == nil || rk.Cmp(reservationKey) != 0 { + t.Errorf("unexpected reservation key: got %v, want %v", rk, reservationKey) + } + if rn != requestNonce { + t.Errorf("unexpected request nonce: got %d, want %d", rn, requestNonce) + } + if mainUtxo == nil { + t.Fatal("mainUtxo must not be nil") + } + if mainUtxo.TxOutputValue != 600000 { + t.Errorf("unexpected UTXO value: got %d, want %d", mainUtxo.TxOutputValue, 600000) + } + if txInfo == nil { + t.Fatal("txInfo must not be nil") + } + if !bytes.Equal(txProof.MerkleProof, proof.MerkleProof) { + t.Errorf("unexpected merkle proof") + } + return nil + } + + metricsRecorder := &mockMetricsRecorder{counts: make(map[string]float64)} + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + metricsRecorder, + ); err != nil { + t.Fatal(err) + } + // Check metrics. + if count := metricsRecorder.counts["reservation_acceptance_proof_submissions_total"]; count != 1 { + t.Errorf("unexpected metrics count: got %f, want 1", count) + } + + // Negative path: nil reservationKey. + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + requiredConfirmations, + nil, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for nil reservation key") + } + + // Negative path: zero requestNonce. + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + requiredConfirmations, + reservationKey, + 0, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for zero request nonce") + } + + // Negative path: action generation is not Pending. + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeAcceptance, + State: tbtc.ReservationActionStateSettled, + TargetWalletPublicKeyHash: walletPKH, + }) + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for settled action generation") + } + + // Negative path: action generation is the wrong type. + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeReanchor, + State: tbtc.ReservationActionStatePending, + TargetWalletPublicKeyHash: walletPKH, + }) + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for wrong action type") + } + + // Negative path: target wallet public key hash mismatch - the anchor + // output pays a different wallet than the action authorized. + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeAcceptance, + State: tbtc.ReservationActionStatePending, + TargetWalletPublicKeyHash: [20]byte{0xff}, + }) + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for target wallet public key hash mismatch") + } + + // Negative path: zero requiredConfirmations. + if err := submitReservationAcceptanceProof( + anchorTx.Hash(), + 0, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for zero required confirmations") + } +} diff --git a/pkg/maintainer/spv/reservation_action_timeout_watch.go b/pkg/maintainer/spv/reservation_action_timeout_watch.go new file mode 100644 index 0000000000..ea63c06ca5 --- /dev/null +++ b/pkg/maintainer/spv/reservation_action_timeout_watch.go @@ -0,0 +1,419 @@ +package spv + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// reservationActionTimeoutLookBackBlocks bounds the pending-action-request event +// scan performed on the very first pass, before an incremental cursor +// exists. Mirrors reservationProofLookBackBlocks in reservation_proof_loop.go: +// 30 days at 12s/block. +const reservationActionTimeoutLookBackBlocks = uint64(216000) + +// reservationActionTimeoutWalletScanLookBackBlocks is kept as an alias for +// backward-compatibility with earlier references to the lookback window. +const reservationActionTimeoutWalletScanLookBackBlocks = reservationActionTimeoutLookBackBlocks + +// ReservationActionTimeoutWatcher observes the reservation action set and +// notifies the Bridge when a pending action's on-chain deadline has elapsed +// without an SPV proof being submitted. +// +// The Bridge uses a per-action timeout window (snapshotted in the +// ReservationAction record at nonce creation time) to bound the lag between +// action creation and SPV proof submission. When the deadline passes without +// a proof, the SPV maintainer is no longer eligible to settle the action +// and the Bridge must be told to update the action to ReservationActionStateTimedOut. +// This triggers Bridge-side sweeping (e.g. fee slashing in m2+ and the +// fallback to owner late-settlement) and ensures the state machine can move +// forward. +// +// In m1 the operator-side penalty is a no-op; NotifyReservationActionTimeout +// is still called with the wallet member IDs as required by the Bridge +// function signature so that m2+ integrations only need to add the slashing +// logic without changing the call shape. +// +// The members resolver maps a wallet public key hash to the operator IDs +// composing that wallet's signing group. In production, node.ResolveWalletMembers +// resolves members for wallets the local operator co-signs and returns an error +// ("wallet not found") for other on-chain wallets. The watcher treats resolver +// errors for non-member wallets as an expected non-member condition and skips +// them silently at Debug log level, so the watcher only meaningfully monitors +// reservation action timeouts for wallets the local operator co-signs. +type ReservationActionTimeoutWatcher struct { + spvChain Chain + // nowFn returns the current UNIX timestamp the watcher treats as "now" + // for `now > timeoutAt` comparisons. Tests override it to drive the + // deadline forward; production wires it to time.Now in UTC. + nowFn func() uint32 + // interval is how often the background poll loop re-checks pending + // actions. The interval must be positive whenever Run is used to drive + // the background loop; tests and the synchronously driven integration + // code path use a positive duration. + interval time.Duration + // membersResolver turns a wallet public key hash into the operator IDs + // the Bridge expects for the slashing argument. The resolver is + // injected to keep the watcher independent of the chain interface used + // to look up operator addresses (the SPV maintainer chain interface + // does not expose GetOperatorID today). + membersResolver tbtc.WalletMembersResolver + + acceptanceLastScannedBlock uint64 + reanchorLastScannedBlock uint64 + + // pendingActions tracks still-pending reservation actions discovered from + // acceptance and re-anchor request events across successive poll passes. + pendingActions map[string]*pendingAction +} + +type pendingAction struct { + reservationKey *big.Int + requestNonce uint64 +} + +// actionEventKey identifies one reservation action generation. +func actionEventKey(reservationKey *big.Int, requestNonce uint64) string { + return fmt.Sprintf("%s#%d", reservationKey.String(), requestNonce) +} + +// NewReservationActionTimeoutWatcher constructs a watcher bound to the +// given chain, members resolver, and poll interval. +// +// The members resolver is mandatory: the watcher will refuse to operate +// without it because emitting NotifyReservationActionTimeout with a nil +// or empty member slice would be ill-formed on the Bridge side. +// +// The pollInterval must be positive whenever Run is used to drive the +// background loop; the watcher can otherwise be driven by +// CheckReservationActionTimeouts calls from the integration. +func NewReservationActionTimeoutWatcher( + spvChain Chain, + membersResolver tbtc.WalletMembersResolver, + pollInterval time.Duration, +) *ReservationActionTimeoutWatcher { + return &ReservationActionTimeoutWatcher{ + spvChain: spvChain, + nowFn: defaultActionTimeoutNowFn, + interval: pollInterval, + membersResolver: membersResolver, + pendingActions: make(map[string]*pendingAction), + } +} + +// defaultActionTimeoutNowFn returns time.Now() as a uint32 UNIX timestamp. +// Kept separate from the struct to allow tests to swap it deterministically. +func defaultActionTimeoutNowFn() uint32 { + return uint32(time.Now().Unix()) +} + +// nextScanRange calculates the start and current block numbers for the next +// event scan. On the first scan (lastScannedBlock == 0), the scan window is +// bounded by reservationActionTimeoutLookBackBlocks. On subsequent scans, it +// resumes from lastScannedBlock + 1. +func (ratw *ReservationActionTimeoutWatcher) nextScanRange( + lastScannedBlock uint64, +) (startBlock uint64, currentBlock uint64, err error) { + blockCounter, err := ratw.spvChain.BlockCounter() + if err != nil { + return 0, 0, fmt.Errorf("failed to get block counter: [%v]", err) + } + + currentBlock, err = blockCounter.CurrentBlock() + if err != nil { + return 0, 0, fmt.Errorf("failed to get current block: [%v]", err) + } + + if lastScannedBlock == 0 { + if currentBlock > reservationActionTimeoutLookBackBlocks { + startBlock = currentBlock - reservationActionTimeoutLookBackBlocks + } else { + startBlock = 0 + } + } else { + startBlock = lastScannedBlock + 1 + } + + return startBlock, currentBlock, nil +} + +// Run starts the background poll loop. It returns when ctx is done or when +// a fatal configuration error is detected. +// +// Each iteration discovers new reservation acceptance and re-anchor action +// request events incrementally, updates the tracked pending actions set, +// removes actions that are no longer pending, and calls +// CheckReservationActionTimeouts on any overdue pending action. +func (ratw *ReservationActionTimeoutWatcher) Run(ctx context.Context) error { + if ratw.membersResolver == nil { + return fmt.Errorf( + "action-timeout watcher requires a non-nil members resolver", + ) + } + if ratw.interval <= 0 { + return fmt.Errorf( + "action-timeout watcher requires a positive poll interval", + ) + } + + ticker := time.NewTicker(ratw.interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + } + + if err := ratw.pollPendingActions(); err != nil { + logger.Errorf("action-timeout watcher poll failed: [%v]", err) + } + } +} + +// pollPendingActions scans for newly requested reservation actions, updates the +// pendingActions map, evicts actions that are no longer pending, and checks +// overdue actions for timeout. +func (ratw *ReservationActionTimeoutWatcher) pollPendingActions() error { + // 1. Scan new ReservationAcceptanceRequestedEvents + acceptanceStartBlock, acceptanceCurrentBlock, err := ratw.nextScanRange( + ratw.acceptanceLastScannedBlock, + ) + if err != nil { + return fmt.Errorf("failed to get acceptance scan range: [%v]", err) + } + + acceptanceEvents, err := ratw.spvChain.PastReservationAcceptanceRequestedEvents( + &tbtc.ReservationAcceptanceRequestedEventFilter{ + StartBlock: acceptanceStartBlock, + EndBlock: &acceptanceCurrentBlock, + }, + ) + if err != nil { + return fmt.Errorf( + "failed to get past reservation acceptance requested events: [%v]", + err, + ) + } + + for _, event := range acceptanceEvents { + key := actionEventKey(event.ReservationKey, event.RequestNonce) + ratw.pendingActions[key] = &pendingAction{ + reservationKey: event.ReservationKey, + requestNonce: event.RequestNonce, + } + } + ratw.acceptanceLastScannedBlock = acceptanceCurrentBlock + + // 2. Scan new ReservationReanchorRequestedEvents + reanchorStartBlock, reanchorCurrentBlock, err := ratw.nextScanRange( + ratw.reanchorLastScannedBlock, + ) + if err != nil { + return fmt.Errorf("failed to get reanchor scan range: [%v]", err) + } + + reanchorEvents, err := ratw.spvChain.PastReservationReanchorRequestedEvents( + &tbtc.ReservationReanchorRequestedEventFilter{ + StartBlock: reanchorStartBlock, + EndBlock: &reanchorCurrentBlock, + }, + ) + if err != nil { + return fmt.Errorf( + "failed to get past reservation reanchor requested events: [%v]", + err, + ) + } + + for _, event := range reanchorEvents { + key := actionEventKey(event.ReservationKey, event.RequestNonce) + ratw.pendingActions[key] = &pendingAction{ + reservationKey: event.ReservationKey, + requestNonce: event.RequestNonce, + } + } + ratw.reanchorLastScannedBlock = reanchorCurrentBlock + + now := ratw.nowFn() + + // 3. Re-check each tracked action and remove entries that are no longer pending + for key, item := range ratw.pendingActions { + action, err := ratw.spvChain.GetReservationAction( + item.reservationKey, + item.requestNonce, + ) + if err != nil { + logger.Errorf( + "failed to load reservation action [%v]/%d: [%v]", + item.reservationKey, + item.requestNonce, + err, + ) + continue + } + + if action.State != tbtc.ReservationActionStatePending { + delete(ratw.pendingActions, key) + continue + } + + if now > action.TimeoutAt { + if err := ratw.CheckReservationActionTimeouts( + item.reservationKey, + now, + ); err != nil { + logger.Errorf( + "action-timeout watcher failed to check reservation [%v]: [%v]", + item.reservationKey, + err, + ) + } else { + // Once a timeout check has successfully completed (either notified + // or cleanly skipped for non-member/empty set), remove it from + // pendingActions so subsequent poll ticks do not repeat notifications. + delete(ratw.pendingActions, key) + } + } + } + + return nil +} + +// CheckReservationActionTimeouts inspects the current action generation of a +// single reservation and notifies the Bridge if it is Pending and its +// TimeoutAt has elapsed. The caller controls the iteration; the watcher +// does not background-loop on its own (see Run for the poll-driven caller). +// +// Parameters: +// +// - reservationKey: the reservation identifier used by the Bridge's +// ReservationRouter. +// - now: a UNIX timestamp used to compare against TimeoutAt. Tests pass +// an explicit value; production passes time.Now().Unix() cast to uint32. +// +// The function resolves the custodying wallet, looks up the operator member +// IDs through the injected resolver, then inspects only the action +// generation at reservation.RequestNonce. By the Bridge invariant, only the +// most-recent action generation can be Pending - older nonces have already +// settled, timed out, or been superseded - so a single lookup suffices; no +// walk from nonce 0 is needed. A RequestNonce of 0 means no action +// generation has ever been requested against the reservation, so there is +// nothing to check. +func (ratw *ReservationActionTimeoutWatcher) CheckReservationActionTimeouts( + reservationKey *big.Int, + now uint32, +) error { + if ratw.membersResolver == nil { + return fmt.Errorf( + "action-timeout watcher requires a non-nil members resolver", + ) + } + if reservationKey == nil { + return fmt.Errorf("reservation key must not be nil") + } + + reservation, err := ratw.spvChain.GetReservation(reservationKey) + if err != nil { + return fmt.Errorf( + "failed to load reservation [%v]: [%v]", + reservationKey, + err, + ) + } + + if reservation.RequestNonce == 0 { + // No action generation has ever been requested; nothing pending. + return nil + } + + walletPublicKeyHash := reservation.WalletPublicKeyHash + if walletPublicKeyHash == ([20]byte{}) { + logger.Debugf("reservation [%v] has no wallet; skipping", reservationKey) + return nil + } + + nonce := reservation.RequestNonce + + action, err := ratw.spvChain.GetReservationAction(reservationKey, nonce) + if err != nil { + return fmt.Errorf( + "failed to load action for reservation [%v] at nonce %d: [%v]", + reservationKey, + nonce, + err, + ) + } + + if action.State != tbtc.ReservationActionStatePending { + logger.Debugf( + "reservation [%v] action nonce %d state=%s; not pending, "+ + "nothing to time out", + reservationKey, + nonce, + action.State, + ) + return nil + } + + if now <= action.TimeoutAt { + logger.Debugf( + "reservation [%v] action nonce %d timeout at [%d] "+ + "not yet reached (now=%d); skipping", + reservationKey, + nonce, + action.TimeoutAt, + now, + ) + return nil + } + + memberIDs, err := ratw.membersResolver.ResolveWalletMembers(walletPublicKeyHash) + if err != nil { + logger.Debugf( + "operator is not a member of wallet [0x%x]; skipping action timeout check for reservation [%v]: [%v]", + walletPublicKeyHash, + reservationKey, + err, + ) + return nil + } + + if len(memberIDs) == 0 { + logger.Debugf( + "wallet [0x%x] members resolver returned an empty set; "+ + "skipping action timeout check for reservation [%v]", + walletPublicKeyHash, + reservationKey, + ) + return nil + } + + if err := ratw.spvChain.NotifyReservationActionTimeout( + reservationKey, + memberIDs, + ); err != nil { + return fmt.Errorf( + "failed to notify action timeout for "+ + "reservation [%v] nonce %d: [%v]", + reservationKey, + nonce, + err, + ) + } + + logger.Infof( + "notified action timeout for reservation [%v] nonce %d "+ + "(timeout=%d, members=%d)", + reservationKey, + nonce, + action.TimeoutAt, + len(memberIDs), + ) + + return nil +} diff --git a/pkg/maintainer/spv/reservation_action_timeout_watch_test.go b/pkg/maintainer/spv/reservation_action_timeout_watch_test.go new file mode 100644 index 0000000000..c24f9c2ef0 --- /dev/null +++ b/pkg/maintainer/spv/reservation_action_timeout_watch_test.go @@ -0,0 +1,679 @@ +package spv + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/tbtc" + + "github.com/go-test/deep" +) + +// recordingActionTimeoutMembers is a test double for the +// tbtc.WalletMembersResolver interface. It returns the operator IDs configured +// at construction time and records the wallet PKHs it was asked to resolve. +type recordingActionTimeoutMembers struct { + walletIDs map[[20]byte][]uint32 + calls [][20]byte + errByPKH map[[20]byte]error +} + +func (r *recordingActionTimeoutMembers) ResolveWalletMembers( + walletPublicKeyHash [20]byte, +) ([]uint32, error) { + r.calls = append(r.calls, walletPublicKeyHash) + if err, ok := r.errByPKH[walletPublicKeyHash]; ok { + return nil, err + } + return r.walletIDs[walletPublicKeyHash], nil +} + +// seededReservation installs a reservation and (optionally) a list of +// action generations under spvChain for use in the action-timeout watcher +// tests. Helper reduces per-test noise. actions[0] is stored as generation +// nonce 1, actions[1] as nonce 2, etc., matching the 1-based action +// generation convention (a reservation has no generation 0; the first +// ever-requested action is nonce 1). +func seededReservation( + t *testing.T, + spvChain *localChain, + key *big.Int, + wallet [20]byte, + actions []*tbtc.ReservationAction, + requestNonce uint64, +) { + t.Helper() + spvChain.setReservation(key, &tbtc.Reservation{ + WalletPublicKeyHash: wallet, + RequestNonce: requestNonce, + }) + for i, action := range actions { + spvChain.setReservationAction(key, uint64(i)+1, action) + } +} + +func TestReservationActionTimeoutWatcher_NotifiesTimedOutPendingAction(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xC001) + members := []uint32{11, 22, 33} + + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet: members}, + } + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 1, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + calls := spvChain.getSubmittedReservationActionTimeouts() + if len(calls) != 1 { + t.Fatalf("expected one timeout notification, got %d", len(calls)) + } + if diff := deep.Equal(key, calls[0].reservationKey); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } + if diff := deep.Equal(members, calls[0].walletMembersIDs); diff != nil { + t.Errorf("unexpected notified members: %v", diff) + } + // The resolver must be consulted exactly once per Check call, not per + // nonce, because the Bridge requires the member IDs to be consistent + // across all notifications emitted in response to a single reservation. + if len(resolver.calls) != 1 { + t.Errorf("expected resolver to be called once, got %d", len(resolver.calls)) + } +} + +func TestReservationActionTimeoutWatcher_DoesNotNotifyBeforeTimeout(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xC002) + + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet: {1, 2}}, + } + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 10_000, + }, + }, + 1, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 0 { + t.Fatalf( + "action not yet timed out; expected zero notifications, got %d", + len(calls), + ) + } +} + +func TestReservationActionTimeoutWatcher_IgnoresSettledOlderGeneration(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xC003) + + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet: {1, 2}}, + } + // Generation 1 (an old re-anchor, say) is already Settled; generation 2 + // is the current pending generation and is past its deadline. The + // watcher must inspect only the current generation (RequestNonce = 2) + // and notify for it - this is the fix for the bug where an older + // walk-from-zero implementation stopped at the first non-pending + // generation and never reached the real timed-out one. + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStateSettled, + TimeoutAt: 100, + }, + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 2, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 1 { + t.Fatalf( + "current generation is pending and past deadline; expected one "+ + "notification, got %d", + len(calls), + ) + } +} + +func TestReservationActionTimeoutWatcher_NotifiesCurrentGenerationOnly(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xC004) + + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet: {7, 8, 9}}, + } + // Generation 1 is still pending and NOT past its deadline; generation 2 + // is the current pending generation and IS past its deadline. Only + // generation 2 (RequestNonce) is ever inspected, so exactly one + // notification fires regardless of generation 1's state. + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 10_000, + }, + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 2, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 1 { + t.Fatalf( + "expected exactly one notification for the current generation, got %d", + len(calls), + ) + } +} + +func TestReservationActionTimeoutWatcher_SkipsReservationWithoutWallet(t *testing.T) { + spvChain := newLocalChain() + + key := reservationKey(0xC005) + // No wallet PKH assigned. + + resolver := &recordingActionTimeoutMembers{} + spvChain.setReservation(key, &tbtc.Reservation{ + WalletPublicKeyHash: [20]byte{}, + RequestNonce: 0, + }) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 0 { + t.Fatalf("zero-wallet reservation must skip, got %d notifications", len(calls)) + } + if len(resolver.calls) != 0 { + t.Fatalf("resolver must not be called for zero-wallet reservation, got %d calls", len(resolver.calls)) + } +} + +func TestReservationActionTimeoutWatcher_MembersResolverError(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xC006) + + // In production, node.ResolveWalletMembers errors ("wallet not found") for + // wallets the local operator is not a signing member of. The watcher must + // treat this as an expected non-membership condition and skip cleanly without + // returning an error or notifying. + resolver := &recordingActionTimeoutMembers{ + errByPKH: map[[20]byte]error{wallet: errors.New("wallet not found")}, + } + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 1, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("expected nil error on resolver non-member error, got: %v", err) + } + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 0 { + t.Fatalf("no notifications should fire on resolver error, got %d", len(calls)) + } +} + +func TestReservationActionTimeoutWatcher_MembersResolverEmpty(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xC007) + + // An empty member set must also skip cleanly without emitting a notification. + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet: {}}, + } + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 1, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("expected nil error on empty member set, got: %v", err) + } + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 0 { + t.Fatalf("no notifications should fire on empty member set, got %d", len(calls)) + } +} + +func TestReservationActionTimeoutWatcher_NilResolverError(t *testing.T) { + spvChain := newLocalChain() + + watcher := NewReservationActionTimeoutWatcher(spvChain, nil, 0) + if err := watcher.CheckReservationActionTimeouts(reservationKey(0xC008), 5_000); err == nil { + t.Fatal("expected error for nil resolver, got nil") + } +} + +func TestReservationActionTimeoutWatcher_NilKeyError(t *testing.T) { + spvChain := newLocalChain() + resolver := &recordingActionTimeoutMembers{} + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(nil, 5_000); err == nil { + t.Fatal("expected error for nil reservation key, got nil") + } +} + +func TestReservationActionTimeoutWatcher_SkipsWalletZeroBranch(t *testing.T) { + // Isolates the wallet-zero skip branch from the RequestNonce == 0 skip + // branch: RequestNonce is nonzero (a real action generation exists) but + // WalletPublicKeyHash is zero, so the reservation exists yet has no + // wallet assigned. This must skip via the wallet-zero check, not be + // short-circuited by the (separate) RequestNonce == 0 check that an + // earlier version of this test file conflated by zeroing both fields + // together. + spvChain := newLocalChain() + resolver := &recordingActionTimeoutMembers{} + + key := reservationKey(0xC00B) + spvChain.setReservation(key, &tbtc.Reservation{ + WalletPublicKeyHash: [20]byte{}, + RequestNonce: 1, + }) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + if err := watcher.CheckReservationActionTimeouts(key, 5_000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 0 { + t.Fatalf("zero-wallet reservation must skip, got %d notifications", len(calls)) + } + if len(resolver.calls) != 0 { + t.Fatalf("resolver must not be called for zero-wallet reservation, got %d calls", len(resolver.calls)) + } +} + +func TestReservationActionTimeoutWatcher_NotifierErrorPropagates(t *testing.T) { + spvChain := newLocalChain() + errFromNotifier := errors.New("downstream") + + wallet := walletPKH() + key := reservationKey(0xC00A) + + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet: {1, 2, 3}}, + } + // The current generation is pending and past its deadline, but the + // Bridge notify call fails. With only one generation ever inspected per + // Check call, the failure must surface as an error from + // CheckReservationActionTimeouts (not be silently swallowed), so a + // poll-loop caller logs and retries on the next tick instead of + // wrongly treating it as settled. + spvChain.notifyReservationActionTimeoutErr = errFromNotifier + seededReservation( + t, + spvChain, + key, + wallet, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 1, + ) + + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, 0) + err := watcher.CheckReservationActionTimeouts(key, 5_000) + if err == nil { + t.Fatal("expected the notifier error to propagate, got nil") + } + + if calls := spvChain.getSubmittedReservationActionTimeouts(); len(calls) != 1 { + t.Fatalf("expected exactly one notification attempt, got %d", len(calls)) + } +} + +func TestReservationActionTimeoutWatcher_NextScanRange(t *testing.T) { + spvChain := newLocalChain() + blockCounter := newMockBlockCounter() + spvChain.setBlockCounter(blockCounter) + + resolver := &recordingActionTimeoutMembers{} + watcher := NewReservationActionTimeoutWatcher(spvChain, resolver, time.Minute) + + // Case 1: First scan (lastScannedBlock == 0) and currentBlock > lookback. + blockCounter.SetCurrentBlock(300_000) + startBlock, currentBlock, err := watcher.nextScanRange(0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedStart := uint64(300_000) - reservationActionTimeoutLookBackBlocks + if startBlock != expectedStart { + t.Errorf("expected start block %d, got %d", expectedStart, startBlock) + } + if currentBlock != 300_000 { + t.Errorf("expected current block 300000, got %d", currentBlock) + } + + // Case 2: First scan (lastScannedBlock == 0) and currentBlock <= lookback. + blockCounter.SetCurrentBlock(100_000) + startBlock, currentBlock, err = watcher.nextScanRange(0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if startBlock != 0 { + t.Errorf("expected start block 0, got %d", startBlock) + } + if currentBlock != 100_000 { + t.Errorf("expected current block 100000, got %d", currentBlock) + } + + // Case 3: Subsequent scan (lastScannedBlock > 0). + blockCounter.SetCurrentBlock(500_000) + startBlock, currentBlock, err = watcher.nextScanRange(450_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if startBlock != 450_001 { + t.Errorf("expected start block 450001, got %d", startBlock) + } + if currentBlock != 500_000 { + t.Errorf("expected current block 500000, got %d", currentBlock) + } +} + +func TestReservationActionTimeoutWatcher_RunLoop_IncrementalTracking(t *testing.T) { + spvChain := newLocalChain() + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + spvChain.setBlockCounter(blockCounter) + + wallet1 := walletPKH() + members := []uint32{1, 2, 3} + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet1: members}, + } + + pollInterval := 10 * time.Millisecond + ratw := NewReservationActionTimeoutWatcher( + spvChain, + resolver, + pollInterval, + ) + ratw.nowFn = func() uint32 { return 500 } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Tick 1: acceptance event for key1 (nonce 1). + key1 := reservationKey(0x1001) + spvChain.addReservationAcceptanceRequestedEvent(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: key1, + RequestNonce: 1, + WalletPublicKeyHash: wallet1, + BlockNumber: 500, + }) + seededReservation( + t, + spvChain, + key1, + wallet1, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, // Timed out (now=500 > 100) + }, + }, + 1, + ) + + errChan := make(chan error, 1) + go func() { + errChan <- ratw.Run(ctx) + }() + + // Wait for tick 1 to process key1. + time.Sleep(50 * time.Millisecond) + + // Verify key1 was notified. + calls := spvChain.getSubmittedReservationActionTimeouts() + if len(calls) != 1 { + t.Fatalf("expected 1 notification after tick 1, got %d", len(calls)) + } + if diff := deep.Equal(key1, calls[0].reservationKey); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } + + // Tick 2: key1 is now settled (no longer pending), and a reanchor event + // arrives for key2 (nonce 2) at block 1500. + spvChain.setReservationAction(key1, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStateSettled, + TimeoutAt: 100, + }) + + blockCounter.SetCurrentBlock(2000) + key2 := reservationKey(0x1002) + spvChain.addReservationReanchorRequestedEvent(&tbtc.ReservationReanchorRequestedEvent{ + ReservationKey: key2, + RequestNonce: 2, + SourceWalletPublicKeyHash: wallet1, + BlockNumber: 1500, + }) + seededReservation( + t, + spvChain, + key2, + wallet1, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStateSettled, + TimeoutAt: 100, + }, + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 200, // Timed out (now=500 > 200) + }, + }, + 2, + ) + + // Wait for tick 2 to process key2 and evict key1. + time.Sleep(50 * time.Millisecond) + + cancel() + if err := <-errChan; err != nil { + t.Errorf("Run returned error: %v", err) + } + + // Total timeout notifications should now be 2 (key1 on tick 1, key2 on tick 2). + calls = spvChain.getSubmittedReservationActionTimeouts() + if len(calls) != 2 { + t.Fatalf("expected 2 notifications after tick 2, got %d", len(calls)) + } + if diff := deep.Equal(key2, calls[1].reservationKey); diff != nil { + t.Errorf("unexpected second notified key: %v", diff) + } + + // key1 should have been evicted from pendingActions because it became Settled. + key1EventKey := actionEventKey(key1, 1) + if _, ok := ratw.pendingActions[key1EventKey]; ok { + t.Errorf("key1 should have been evicted from pendingActions once Settled") + } +} + +func TestReservationActionTimeoutWatcher_RunLoop_BoundedFirstScan(t *testing.T) { + spvChain := newLocalChain() + blockCounter := newMockBlockCounter() + // Set current block high enough that lookback applies. + currentBlock := uint64(500_000) + blockCounter.SetCurrentBlock(currentBlock) + spvChain.setBlockCounter(blockCounter) + + wallet1 := walletPKH() + members := []uint32{1, 2, 3} + resolver := &recordingActionTimeoutMembers{ + walletIDs: map[[20]byte][]uint32{wallet1: members}, + } + + pollInterval := 10 * time.Millisecond + ratw := NewReservationActionTimeoutWatcher( + spvChain, + resolver, + pollInterval, + ) + ratw.nowFn = func() uint32 { return 500 } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Event 1 is old: block 100,000 (before startBlock = 500,000 - 216,000 = 284,000). + oldKey := reservationKey(0x9001) + spvChain.addReservationAcceptanceRequestedEvent(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: oldKey, + RequestNonce: 1, + WalletPublicKeyHash: wallet1, + BlockNumber: 100_000, + }) + seededReservation( + t, + spvChain, + oldKey, + wallet1, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 1, + ) + + // Event 2 is within lookback: block 300,000. + recentKey := reservationKey(0x9002) + spvChain.addReservationAcceptanceRequestedEvent(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: recentKey, + RequestNonce: 1, + WalletPublicKeyHash: wallet1, + BlockNumber: 300_000, + }) + seededReservation( + t, + spvChain, + recentKey, + wallet1, + []*tbtc.ReservationAction{ + { + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }, + }, + 1, + ) + + errChan := make(chan error, 1) + go func() { + errChan <- ratw.Run(ctx) + }() + + time.Sleep(50 * time.Millisecond) + cancel() + if err := <-errChan; err != nil { + t.Errorf("Run returned error: %v", err) + } + + // Only recentKey should have been discovered and notified. + calls := spvChain.getSubmittedReservationActionTimeouts() + if len(calls) != 1 { + t.Fatalf("expected exactly 1 notification (recent event only), got %d", len(calls)) + } + if diff := deep.Equal(recentKey, calls[0].reservationKey); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } + + // oldKey should not be tracked in pendingActions. + oldEventKey := actionEventKey(oldKey, 1) + if _, ok := ratw.pendingActions[oldEventKey]; ok { + t.Errorf("oldKey should not have been discovered by bounded initial scan") + } +} diff --git a/pkg/maintainer/spv/reservation_proof_loop.go b/pkg/maintainer/spv/reservation_proof_loop.go new file mode 100644 index 0000000000..07bb4b3793 --- /dev/null +++ b/pkg/maintainer/spv/reservation_proof_loop.go @@ -0,0 +1,677 @@ +package spv + +import ( + "bytes" + "context" + "fmt" + "math/big" + "time" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/maintainer/btcdiff" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// reservationProofLookBackBlocks bounds the pending-action-request event +// scan performed on the very first pass, before an incremental cursor +// exists. Mirrors ReservationAcceptanceLookBackBlocks / +// ReservationReanchorLookBackBlocks in pkg/tbtcpg: 30 days at 12s/block. +const reservationProofLookBackBlocks = uint64(216000) + +// reservationProofScanState persists the incremental event-scan cursor and +// the set of still-pending action-request events across successive passes +// of runReservationProofLoop, so proveReservationAcceptanceActions and +// proveReservationReanchorActions scan only the event/Bitcoin history that +// has appeared since the previous pass instead of rescanning the full +// reservationProofLookBackBlocks window - and refetching Bitcoin history +// for every wallet in it - every config.IdleBackoffTime. +type reservationProofScanState struct { + acceptanceLastScannedBlock uint64 + pendingAcceptanceEvents map[string]*tbtc.ReservationAcceptanceRequestedEvent + acceptanceRetries map[string]uint + + reanchorLastScannedBlock uint64 + pendingReanchorEvents map[string]*tbtc.ReservationReanchorRequestedEvent + reanchorRetries map[string]uint +} + +func newReservationProofScanState() *reservationProofScanState { + return &reservationProofScanState{ + pendingAcceptanceEvents: make(map[string]*tbtc.ReservationAcceptanceRequestedEvent), + acceptanceRetries: make(map[string]uint), + pendingReanchorEvents: make(map[string]*tbtc.ReservationReanchorRequestedEvent), + reanchorRetries: make(map[string]uint), + } +} + +// maxReservationActionLoadRetries is the maximum number of consecutive +// passes GetReservationAction may fail for a tracked pending event before +// the event is evicted from the pending map to avoid unbounded map growth +// and log spam on unrecoverable RPC/chain errors. +const maxReservationActionLoadRetries = 3 + +// reservationEventKey identifies one reservation action generation, unique +// across both the acceptance and re-anchor pending-event maps. +func reservationEventKey(reservationKey *big.Int, requestNonce uint64) string { + return fmt.Sprintf("%s:%d", reservationKey.String(), requestNonce) +} + +// reservationProofNextScanRange returns the block range to scan for new +// pending-action-request events this pass: the bounded +// reservationProofLookBackBlocks catch-up window on the very first pass +// (lastScannedBlock == 0), or just the delta since the previous pass's +// cursor on every pass thereafter, so a steady-state loop no longer +// re-fetches the full ~30-day window on every config.IdleBackoffTime tick. +func reservationProofNextScanRange( + spvChain Chain, + lastScannedBlock uint64, +) (startBlock uint64, currentBlock uint64, err error) { + blockCounter, err := spvChain.BlockCounter() + if err != nil { + return 0, 0, fmt.Errorf("failed to get block counter: [%v]", err) + } + + currentBlock, err = blockCounter.CurrentBlock() + if err != nil { + return 0, 0, fmt.Errorf("failed to get current block: [%v]", err) + } + + if lastScannedBlock == 0 { + if currentBlock > reservationProofLookBackBlocks { + return currentBlock - reservationProofLookBackBlocks, currentBlock, nil + } + return 0, currentBlock, nil + } + + return lastScannedBlock + 1, currentBlock, nil +} + +// maintainReservationProofs runs the SPV proof submission loop for +// reservation acceptance and re-anchor action generations. It is a +// dedicated loop, separate from spvMaintainer's generic proofTypes-driven +// control loop (see spv.go's Initialize), because SubmitReservationProof +// requires the (reservationKey, requestNonce) pair of the action generation +// being proven - context the generic +// unprovenTransactionsGetter/transactionProofSubmitter signatures (shared +// by deposit sweep, redemption, moving funds, and moved funds sweep) cannot +// carry. +// +// The loop shape mirrors spvMaintainer.startControlLoop/maintainSpv: an +// outer restart-backoff loop wraps an inner idle-backoff loop, so a +// transient error restarts after config.RestartBackoffTime and a clean pass +// with nothing to prove waits config.IdleBackoffTime before trying again. +func maintainReservationProofs( + ctx context.Context, + config Config, + spvChain Chain, + btcDiffChain btcdiff.Chain, + btcChain bitcoin.Chain, +) { + logger.Info("starting reservation proof maintainer") + + defer func() { + logger.Info("stopping reservation proof maintainer") + }() + + for { + err := runReservationProofLoop(ctx, config, spvChain, btcDiffChain, btcChain) + if err != nil { + logger.Errorf( + "error while maintaining reservation proofs: [%v]; "+ + "restarting reservation proof maintainer", + err, + ) + } + + select { + case <-time.After(config.RestartBackoffTime): + case <-ctx.Done(): + return + } + } +} + +// runReservationProofLoop repeatedly proves pending reservation acceptance +// and re-anchor action generations until ctx is done or an unrecoverable +// error occurs. Per-action errors (a single reservation's proof failing to +// assemble or submit) are logged and skipped rather than propagated, so one +// bad action generation does not block the rest; only a chain-wide failure +// (e.g. cannot read the current block) aborts the pass and triggers the +// outer restart backoff. +// +// A single reservationProofScanState is created once and threaded through +// every pass for the lifetime of the loop, carrying the incremental event +// cursor and pending-action set described on that type. +func runReservationProofLoop( + ctx context.Context, + config Config, + spvChain Chain, + btcDiffChain btcdiff.Chain, + btcChain bitcoin.Chain, +) error { + state := newReservationProofScanState() + + for { + if err := proveReservationAcceptanceActions( + state, + config, + spvChain, + btcDiffChain, + btcChain, + ); err != nil { + return fmt.Errorf( + "error while proving reservation acceptance actions: [%v]", + err, + ) + } + + if err := proveReservationReanchorActions( + state, + config, + spvChain, + btcDiffChain, + btcChain, + ); err != nil { + return fmt.Errorf( + "error while proving reservation re-anchor actions: [%v]", + err, + ) + } + + select { + case <-time.After(config.IdleBackoffTime): + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// proveReservationAcceptanceActions finds pending ReservationAcceptance +// action generations, locates each one's already-broadcast anchor +// transaction on the Bitcoin chain (if any), and submits its SPV proof once +// it has accumulated enough confirmations. +func proveReservationAcceptanceActions( + state *reservationProofScanState, + config Config, + spvChain Chain, + btcDiffChain btcdiff.Chain, + btcChain bitcoin.Chain, +) error { + startBlock, currentBlock, err := reservationProofNextScanRange( + spvChain, + state.acceptanceLastScannedBlock, + ) + if err != nil { + return err + } + + newEvents, err := spvChain.PastReservationAcceptanceRequestedEvents( + &tbtc.ReservationAcceptanceRequestedEventFilter{ + StartBlock: startBlock, + EndBlock: ¤tBlock, + }, + ) + if err != nil { + return fmt.Errorf( + "failed to get past reservation acceptance requested "+ + "events: [%v]", + err, + ) + } + + for _, event := range newEvents { + key := reservationEventKey(event.ReservationKey, event.RequestNonce) + state.pendingAcceptanceEvents[key] = event + } + + // Re-check every tracked event's on-chain action state, evict settled/stale + // ones, and group still-pending events by wallet public key hash. + walletEvents := make(map[[20]byte][]*tbtc.ReservationAcceptanceRequestedEvent) + for key, event := range state.pendingAcceptanceEvents { + action, err := spvChain.GetReservationAction( + event.ReservationKey, + event.RequestNonce, + ) + if err != nil { + state.acceptanceRetries[key]++ + if state.acceptanceRetries[key] >= maxReservationActionLoadRetries { + logger.Errorf( + "failed to load reservation acceptance action [%v]/%d: [%v]; "+ + "exceeded max retries (%d), evicting event", + event.ReservationKey, + event.RequestNonce, + err, + maxReservationActionLoadRetries, + ) + delete(state.pendingAcceptanceEvents, key) + delete(state.acceptanceRetries, key) + } else { + logger.Errorf( + "failed to load reservation acceptance action [%v]/%d (retry %d/%d): [%v]", + event.ReservationKey, + event.RequestNonce, + state.acceptanceRetries[key], + maxReservationActionLoadRetries, + err, + ) + } + continue + } + + delete(state.acceptanceRetries, key) + + if action.State != tbtc.ReservationActionStatePending { + delete(state.pendingAcceptanceEvents, key) + continue + } + + walletEvents[event.WalletPublicKeyHash] = append( + walletEvents[event.WalletPublicKeyHash], + event, + ) + } + + for walletPublicKeyHash, events := range walletEvents { + walletTransactions, err := btcChain.GetTransactionsForPublicKeyHash( + walletPublicKeyHash, + config.TransactionLimit, + ) + if err != nil { + logger.Errorf("failed to get transactions for wallet: [%v]", err) + continue + } + + // Index wallet transactions by deposit key for O(1) matching. + candidateTransactions := make(map[string]*bitcoin.Transaction) + for _, transaction := range walletTransactions { + if len(transaction.Inputs) == 1 && len(transaction.Outputs) == 1 && transaction.Inputs[0].Outpoint != nil { + input := transaction.Inputs[0] + depositKey := spvChain.BuildDepositKey( + input.Outpoint.TransactionHash, + input.Outpoint.OutputIndex, + ) + candidateTransactions[depositKey.String()] = transaction + } + } + + for _, event := range events { + transaction, ok := candidateTransactions[event.ReservationKey.String()] + if !ok { + continue + } + + if !isMatchingReservationAcceptanceTransaction(spvChain, event, transaction) { + continue + } + + if err := proveReservationTransaction( + transaction, + btcChain, + spvChain, + btcDiffChain, + func(transactionHash bitcoin.Hash, requiredConfirmations uint) error { + return SubmitReservationAcceptanceProof( + transactionHash, + requiredConfirmations, + event.ReservationKey, + event.RequestNonce, + btcChain, + spvChain, + ) + }, + ); err != nil { + logger.Errorf( + "failed to prove reservation acceptance transaction [%s] "+ + "for reservation [%v]: [%v]", + transaction.Hash().Hex(bitcoin.ReversedByteOrder), + event.ReservationKey, + err, + ) + continue + } + } + } + + state.acceptanceLastScannedBlock = currentBlock + + return nil +} + +// findReservationAcceptanceTransaction scans the candidate wallet's Bitcoin +// transaction history for the 1-input-1-output acceptance (anchor) +// transaction whose sole input spends the deposit identified by +// event.ReservationKey (== the deposit key; see the m1 identity mapping +// documented in reservation_stale_deposit_watch.go), whose sole output is +// P2WPKH to the custody wallet, and whose output value equals depositAmount - anchorFee. +// Returns nil, nil if no matching transaction has been broadcast yet. +func findReservationAcceptanceTransaction( + spvChain Chain, + event *tbtc.ReservationAcceptanceRequestedEvent, + walletTransactions []*bitcoin.Transaction, +) (*bitcoin.Transaction, error) { + for _, transaction := range walletTransactions { + if isMatchingReservationAcceptanceTransaction(spvChain, event, transaction) { + return transaction, nil + } + } + + return nil, nil +} + +func isMatchingReservationAcceptanceTransaction( + spvChain Chain, + event *tbtc.ReservationAcceptanceRequestedEvent, + transaction *bitcoin.Transaction, +) bool { + if len(transaction.Inputs) != 1 || len(transaction.Outputs) != 1 || transaction.Inputs[0].Outpoint == nil { + return false + } + + input := transaction.Inputs[0] + depositKey := spvChain.BuildDepositKey( + input.Outpoint.TransactionHash, + input.Outpoint.OutputIndex, + ) + + if depositKey.Cmp(event.ReservationKey) != 0 { + return false + } + + expectedScript, err := bitcoin.PayToWitnessPublicKeyHash( + event.WalletPublicKeyHash, + ) + if err != nil || !bytes.Equal(transaction.Outputs[0].PublicKeyScript, expectedScript) { + return false + } + + if depositRequest, found, err := spvChain.GetDepositRequest( + input.Outpoint.TransactionHash, + input.Outpoint.OutputIndex, + ); err != nil { + return false + } else if found { + fee := int64(depositRequest.Amount) - transaction.Outputs[0].Value + if fee <= 0 || (event.TxMaxFee > 0 && uint64(fee) > event.TxMaxFee) { + return false + } + if transaction.Outputs[0].Value != int64(depositRequest.Amount)-fee { + return false + } + } else { + if transaction.Outputs[0].Value <= 0 { + return false + } + } + + return true +} + +// proveReservationReanchorActions finds pending ReservationReanchor action +// generations, locates each one's already-broadcast re-anchor transaction +// on the Bitcoin chain (if any), and submits its SPV proof once it has +// accumulated enough confirmations. +func proveReservationReanchorActions( + state *reservationProofScanState, + config Config, + spvChain Chain, + btcDiffChain btcdiff.Chain, + btcChain bitcoin.Chain, +) error { + startBlock, currentBlock, err := reservationProofNextScanRange( + spvChain, + state.reanchorLastScannedBlock, + ) + if err != nil { + return err + } + + newEvents, err := spvChain.PastReservationReanchorRequestedEvents( + &tbtc.ReservationReanchorRequestedEventFilter{ + StartBlock: startBlock, + EndBlock: ¤tBlock, + }, + ) + if err != nil { + return fmt.Errorf( + "failed to get past reservation re-anchor requested events: [%v]", + err, + ) + } + + for _, event := range newEvents { + key := reservationEventKey(event.ReservationKey, event.RequestNonce) + state.pendingReanchorEvents[key] = event + } + + // Re-check every tracked event's on-chain action state and drop the + // Re-check every tracked event's on-chain action state, evict settled/stale + // ones, and group still-pending events by source wallet public key hash. + walletEvents := make(map[[20]byte][]*tbtc.ReservationReanchorRequestedEvent) + for key, event := range state.pendingReanchorEvents { + action, err := spvChain.GetReservationAction( + event.ReservationKey, + event.RequestNonce, + ) + if err != nil { + state.reanchorRetries[key]++ + if state.reanchorRetries[key] >= maxReservationActionLoadRetries { + logger.Errorf( + "failed to load reservation re-anchor action [%v]/%d: [%v]; "+ + "exceeded max retries (%d), evicting event", + event.ReservationKey, + event.RequestNonce, + err, + maxReservationActionLoadRetries, + ) + delete(state.pendingReanchorEvents, key) + delete(state.reanchorRetries, key) + } else { + logger.Errorf( + "failed to load reservation re-anchor action [%v]/%d (retry %d/%d): [%v]", + event.ReservationKey, + event.RequestNonce, + state.reanchorRetries[key], + maxReservationActionLoadRetries, + err, + ) + } + continue + } + + delete(state.reanchorRetries, key) + + if action.State != tbtc.ReservationActionStatePending { + delete(state.pendingReanchorEvents, key) + continue + } + + walletEvents[event.SourceWalletPublicKeyHash] = append( + walletEvents[event.SourceWalletPublicKeyHash], + event, + ) + } + + for walletPublicKeyHash, events := range walletEvents { + walletTransactions, err := btcChain.GetTransactionsForPublicKeyHash( + walletPublicKeyHash, + config.TransactionLimit, + ) + if err != nil { + logger.Errorf("failed to get transactions for wallet: [%v]", err) + continue + } + + // Index wallet transactions by spent outpoint for O(1) matching. + candidateTransactions := make(map[bitcoin.TransactionOutpoint]*bitcoin.Transaction) + for _, transaction := range walletTransactions { + if len(transaction.Inputs) == 1 && len(transaction.Outputs) == 1 && transaction.Inputs[0].Outpoint != nil { + candidateTransactions[*transaction.Inputs[0].Outpoint] = transaction + } + } + + for _, event := range events { + reservation, err := spvChain.GetReservation(event.ReservationKey) + if err != nil { + logger.Errorf( + "failed to load reservation [%v]: [%v]", + event.ReservationKey, + err, + ) + continue + } + if reservation.AnchorUtxo == nil || + reservation.AnchorUtxo.Value == 0 || + reservation.AnchorUtxo.Outpoint == nil || + reservation.AnchorUtxo.Outpoint.TransactionHash == (bitcoin.Hash{}) { + logger.Errorf( + "reservation [%v] has no anchor UTXO to re-anchor from", + event.ReservationKey, + ) + continue + } + + transaction, ok := candidateTransactions[*reservation.AnchorUtxo.Outpoint] + if !ok { + continue + } + + if !isMatchingReservationReanchorTransaction(event, reservation.AnchorUtxo, transaction) { + continue + } + + if err := proveReservationTransaction( + transaction, + btcChain, + spvChain, + btcDiffChain, + func(transactionHash bitcoin.Hash, requiredConfirmations uint) error { + return SubmitReservationReanchorProof( + transactionHash, + requiredConfirmations, + event.ReservationKey, + event.RequestNonce, + btcChain, + spvChain, + ) + }, + ); err != nil { + logger.Errorf( + "failed to prove reservation re-anchor transaction [%s] "+ + "for reservation [%v]: [%v]", + transaction.Hash().Hex(bitcoin.ReversedByteOrder), + event.ReservationKey, + err, + ) + continue + } + } + } + + state.reanchorLastScannedBlock = currentBlock + + return nil +} + +// findReservationReanchorTransaction scans the source wallet's Bitcoin +// transaction history for the 1-input-1-output re-anchor transaction whose +// sole input spends the reservation's current anchor UTXO, whose sole output is +// P2WPKH to the target wallet, and whose output value equals anchorUtxo.Value - reanchorFee. +// Returns nil, nil if no matching transaction has been broadcast yet. +func findReservationReanchorTransaction( + event *tbtc.ReservationReanchorRequestedEvent, + anchorUtxo *bitcoin.UnspentTransactionOutput, + walletTransactions []*bitcoin.Transaction, +) (*bitcoin.Transaction, error) { + for _, transaction := range walletTransactions { + if isMatchingReservationReanchorTransaction(event, anchorUtxo, transaction) { + return transaction, nil + } + } + + return nil, nil +} + +func isMatchingReservationReanchorTransaction( + event *tbtc.ReservationReanchorRequestedEvent, + anchorUtxo *bitcoin.UnspentTransactionOutput, + transaction *bitcoin.Transaction, +) bool { + if len(transaction.Inputs) != 1 || len(transaction.Outputs) != 1 || transaction.Inputs[0].Outpoint == nil { + return false + } + + input := transaction.Inputs[0] + if input.Outpoint.TransactionHash != anchorUtxo.Outpoint.TransactionHash || + input.Outpoint.OutputIndex != anchorUtxo.Outpoint.OutputIndex { + return false + } + + expectedScript, err := bitcoin.PayToWitnessPublicKeyHash( + event.TargetWalletPublicKeyHash, + ) + if err != nil || !bytes.Equal(transaction.Outputs[0].PublicKeyScript, expectedScript) { + return false + } + + fee := int64(anchorUtxo.Value) - transaction.Outputs[0].Value + if fee <= 0 || (event.TxMaxFee > 0 && uint64(fee) > event.TxMaxFee) { + return false + } + if transaction.Outputs[0].Value != int64(anchorUtxo.Value)-fee { + return false + } + + return true +} + +// proveReservationTransaction assembles and submits the SPV proof for a +// single reservation acceptance or re-anchor transaction, once it has +// accumulated enough confirmations and its proof falls within the relay's +// difficulty range. +func proveReservationTransaction( + transaction *bitcoin.Transaction, + btcChain bitcoin.Chain, + spvChain Chain, + btcDiffChain btcdiff.Chain, + submit func(transactionHash bitcoin.Hash, requiredConfirmations uint) error, +) error { + transactionHashStr := transaction.Hash().Hex(bitcoin.ReversedByteOrder) + + isProofWithinRelayRange, accumulatedConfirmations, requiredConfirmations, err := + getProofInfo(transaction.Hash(), btcChain, spvChain, btcDiffChain) + if err != nil { + return fmt.Errorf("failed to get proof info: [%v]", err) + } + + if !isProofWithinRelayRange { + logger.Warnf( + "skipped proving transaction [%s]; the range of the "+ + "required proof goes outside the previous and current "+ + "difficulty epochs as seen by the relay", + transactionHashStr, + ) + return nil + } + + if accumulatedConfirmations < requiredConfirmations { + logger.Infof( + "skipped proving transaction [%s]; transaction has [%v/%v] "+ + "confirmations", + transactionHashStr, + accumulatedConfirmations, + requiredConfirmations, + ) + return nil + } + + if err := submit(transaction.Hash(), requiredConfirmations); err != nil { + return err + } + + logger.Infof( + "successfully submitted proof for transaction [%s]", + transactionHashStr, + ) + + return nil +} diff --git a/pkg/maintainer/spv/reservation_proof_loop_test.go b/pkg/maintainer/spv/reservation_proof_loop_test.go new file mode 100644 index 0000000000..0c12d6a236 --- /dev/null +++ b/pkg/maintainer/spv/reservation_proof_loop_test.go @@ -0,0 +1,1059 @@ +package spv + +import ( + "fmt" + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// TestReservationProofNextScanRange covers the incremental scan-range +// arithmetic: the very first pass (lastScannedBlock == 0) is bounded to +// reservationProofLookBackBlocks behind the current block (or 0 if the +// chain is younger than that window); every later pass starts exactly one +// block after the previous pass's cursor, so a steady-state loop never +// rescans the full look-back window again. +func TestReservationProofNextScanRange(t *testing.T) { + tests := map[string]struct { + currentBlock uint64 + lastScannedBlock uint64 + expectedStart uint64 + }{ + "first pass, current block below the look-back window": { + currentBlock: 1000, + lastScannedBlock: 0, + expectedStart: 0, + }, + "first pass, current block at the look-back window boundary": { + currentBlock: reservationProofLookBackBlocks, + lastScannedBlock: 0, + expectedStart: 0, + }, + "first pass, current block beyond the look-back window": { + currentBlock: reservationProofLookBackBlocks + 500, + lastScannedBlock: 0, + expectedStart: 500, + }, + "later pass starts one block after the cursor, ignoring the look-back window": { + currentBlock: reservationProofLookBackBlocks * 3, + lastScannedBlock: reservationProofLookBackBlocks * 2, + expectedStart: reservationProofLookBackBlocks*2 + 1, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + spvChain := newLocalChain() + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(test.currentBlock) + spvChain.setBlockCounter(blockCounter) + + startBlock, currentBlock, err := reservationProofNextScanRange( + spvChain, + test.lastScannedBlock, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if startBlock != test.expectedStart { + t.Errorf( + "unexpected start block\nexpected: %v\nactual: %v", + test.expectedStart, + startBlock, + ) + } + if currentBlock != test.currentBlock { + t.Errorf( + "unexpected current block\nexpected: %v\nactual: %v", + test.currentBlock, + currentBlock, + ) + } + }) + } +} + +// TestFindReservationAcceptanceTransaction verifies the acceptance +// transaction matcher: it must find the 1-input-1-output transaction whose +// sole input spends the deposit UTXO identified by event.ReservationKey (via +// BuildDepositKey), whose sole output is P2WPKH to the custody wallet, +// and whose value is depositAmount - fee (with fee <= TxMaxFee), skip +// transactions with wrong shape, wrong script, or invalid value, and return nil +// when nothing matches. +func TestFindReservationAcceptanceTransaction(t *testing.T) { + spvChain := newLocalChain() + + fundingTxHash, err := bitcoin.NewHashFromString( + "585b6699f42291d1a9d0776b75f04c295ea203f83504349db11e94fdae7d1b2c", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + reservationKey := spvChain.BuildDepositKey(fundingTxHash, 0) + + walletPublicKeyHash := [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + walletScript, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) + if err != nil { + t.Fatal(err) + } + + otherWalletPKH := [20]byte{99, 99, 99} + otherWalletScript, err := bitcoin.PayToWitnessPublicKeyHash(otherWalletPKH) + if err != nil { + t.Fatal(err) + } + + spvChain.setDepositRequest(fundingTxHash, 0, &tbtc.DepositChainRequest{ + Amount: 150000, + }) + + matchingTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: walletScript, + }}, + } + + // Wrong script: pays to a different wallet. + wrongScriptTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: otherWalletScript, + }}, + } + + // Wrong value: output value >= depositAmount (zero or negative fee). + wrongValueTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 160000, + PublicKeyScript: walletScript, + }}, + } + + // Excess fee: fee 100000 > TxMaxFee 60000. + excessFeeTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 50000, + PublicKeyScript: walletScript, + }}, + } + + // Wrong shape: two outputs, must be skipped even though it otherwise + // spends the right outpoint. + wrongShapeTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{ + {Value: 50000, PublicKeyScript: walletScript}, + {Value: 50000, PublicKeyScript: walletScript}, + }, + } + + // Non-matching: correct shape, different outpoint. + otherTxHash, err := bitcoin.NewHashFromString( + "7cff663e3e08847a5579913f6a66bc6c01f5f48c6ae1783be77418ed188021e6", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + nonMatchingTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: otherTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: walletScript, + }}, + } + + event := &tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: reservationKey, + WalletPublicKeyHash: walletPublicKeyHash, + TxMaxFee: 60000, + } + + t.Run("finds the matching transaction among candidates", func(t *testing.T) { + found, err := findReservationAcceptanceTransaction( + spvChain, + event, + []*bitcoin.Transaction{wrongShapeTx, nonMatchingTx, matchingTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != matchingTx { + t.Errorf("expected to find the matching transaction, got %v", found) + } + }) + + t.Run("returns nil when nothing matches", func(t *testing.T) { + found, err := findReservationAcceptanceTransaction( + spvChain, + event, + []*bitcoin.Transaction{wrongShapeTx, nonMatchingTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil, got %v", found) + } + }) + + t.Run("returns nil for an empty candidate list", func(t *testing.T) { + found, err := findReservationAcceptanceTransaction( + spvChain, + event, + nil, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil, got %v", found) + } + }) + + t.Run("skips transaction with wrong output script", func(t *testing.T) { + found, err := findReservationAcceptanceTransaction( + spvChain, + event, + []*bitcoin.Transaction{wrongScriptTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil for wrong script transaction, got %v", found) + } + }) + + t.Run("skips transaction with wrong output value", func(t *testing.T) { + found, err := findReservationAcceptanceTransaction( + spvChain, + event, + []*bitcoin.Transaction{wrongValueTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil for wrong value transaction, got %v", found) + } + }) + + t.Run("skips transaction with excess fee", func(t *testing.T) { + found, err := findReservationAcceptanceTransaction( + spvChain, + event, + []*bitcoin.Transaction{excessFeeTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil for excess fee transaction, got %v", found) + } + }) +} + +// TestFindReservationReanchorTransaction verifies the re-anchor transaction +// matcher: it must find the 1-input-1-output transaction whose sole input +// spends the reservation's current anchor UTXO outpoint exactly, whose sole output +// is P2WPKH to the target wallet, and whose value is anchorUtxo.Value - fee +// (with fee <= TxMaxFee), skip wrong-shape, wrong-script, or invalid-value +// transactions, and return nil when nothing matches. +func TestFindReservationReanchorTransaction(t *testing.T) { + anchorTxHash, err := bitcoin.NewHashFromString( + "2222222222222222222222222222222222222222222222222222222222222222", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + anchorUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + Value: 600000, + } + + targetWalletPKH := [20]byte{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40} + targetWalletScript, err := bitcoin.PayToWitnessPublicKeyHash(targetWalletPKH) + if err != nil { + t.Fatal(err) + } + + otherPKH := [20]byte{99, 99, 99} + otherScript, err := bitcoin.PayToWitnessPublicKeyHash(otherPKH) + if err != nil { + t.Fatal(err) + } + + matchingTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 590000, + PublicKeyScript: targetWalletScript, + }}, + } + + wrongScriptTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 590000, + PublicKeyScript: otherScript, + }}, + } + + wrongValueTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 600000, + PublicKeyScript: targetWalletScript, + }}, + } + + excessFeeTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 500000, + PublicKeyScript: targetWalletScript, + }}, + } + + // Same transaction hash, wrong output index: must not match. + wrongIndexTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 590000, + PublicKeyScript: targetWalletScript, + }}, + } + + wrongShapeTx := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{ + {Value: 300000, PublicKeyScript: targetWalletScript}, + {Value: 290000, PublicKeyScript: targetWalletScript}, + }, + } + + event := &tbtc.ReservationReanchorRequestedEvent{ + TargetWalletPublicKeyHash: targetWalletPKH, + TxMaxFee: 20000, + } + + t.Run("finds the matching transaction among candidates", func(t *testing.T) { + found, err := findReservationReanchorTransaction( + event, + anchorUtxo, + []*bitcoin.Transaction{wrongShapeTx, wrongIndexTx, matchingTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != matchingTx { + t.Errorf("expected to find the matching transaction, got %v", found) + } + }) + + t.Run("returns nil when nothing matches", func(t *testing.T) { + found, err := findReservationReanchorTransaction( + event, + anchorUtxo, + []*bitcoin.Transaction{wrongShapeTx, wrongIndexTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil, got %v", found) + } + }) + + t.Run("skips transaction with wrong output script", func(t *testing.T) { + found, err := findReservationReanchorTransaction( + event, + anchorUtxo, + []*bitcoin.Transaction{wrongScriptTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil for wrong script transaction, got %v", found) + } + }) + + t.Run("skips transaction with wrong output value", func(t *testing.T) { + found, err := findReservationReanchorTransaction( + event, + anchorUtxo, + []*bitcoin.Transaction{wrongValueTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil for wrong value transaction, got %v", found) + } + }) + + t.Run("skips transaction with excess fee", func(t *testing.T) { + found, err := findReservationReanchorTransaction( + event, + anchorUtxo, + []*bitcoin.Transaction{excessFeeTx}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if found != nil { + t.Errorf("expected nil for excess fee transaction, got %v", found) + } + }) +} + +// TestProveReservationTransaction covers the submit-vs-skip decision: a +// transaction with enough confirmations and a proof within relay range must +// invoke submit exactly once; a transaction with too few confirmations must +// not invoke submit at all. +func TestProveReservationTransaction(t *testing.T) { + // Fixture mirrors TestGetProofInfo's "proof entirely within current + // epoch" case in spv_test.go: factor 6, 20 confirmations, headers + // spanning the proof window all at the current epoch's difficulty. + const proofStart = 790270 + diff := func(d int64) *big.Int { return big.NewInt(d) } + + transaction := &bitcoin.Transaction{} + transactionHash := transaction.Hash() + + newFixture := func(confirmations uint) (*localChain, *localBitcoinChain) { + spvChain := newLocalChain() + btcChain := newLocalBitcoinChain() + + if err := populateBlockHeaders( + btcChain, + proofStart, + proofStart+19, + func(uint) *big.Int { return diff(32) }, + ); err != nil { + t.Fatal(err) + } + btcChain.addTransactionConfirmations(transactionHash, confirmations) + + spvChain.setTxProofDifficultyFactor(big.NewInt(6)) + spvChain.setCurrentEpoch(392) + spvChain.setCurrentAndPrevEpochDifficulty(diff(32), diff(16)) + + return spvChain, btcChain + } + + t.Run("submits when confirmations and relay range are sufficient", func(t *testing.T) { + spvChain, btcChain := newFixture(20) + + submitted := false + err := proveReservationTransaction( + transaction, + btcChain, + spvChain, + spvChain, + func(hash bitcoin.Hash, requiredConfirmations uint) error { + submitted = true + if hash != transaction.Hash() { + t.Errorf("unexpected submitted hash") + } + if requiredConfirmations != 6 { + t.Errorf( + "unexpected required confirmations: got %d, want 6", + requiredConfirmations, + ) + } + return nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !submitted { + t.Error("expected submit to be called") + } + }) + + t.Run("skips without submitting when confirmations are insufficient", func(t *testing.T) { + spvChain, btcChain := newFixture(2) + + submitted := false + err := proveReservationTransaction( + transaction, + btcChain, + spvChain, + spvChain, + func(hash bitcoin.Hash, requiredConfirmations uint) error { + submitted = true + return nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if submitted { + t.Error("expected submit not to be called for insufficient confirmations") + } + }) + + t.Run("propagates submit errors", func(t *testing.T) { + spvChain, btcChain := newFixture(20) + + err := proveReservationTransaction( + transaction, + btcChain, + spvChain, + spvChain, + func(hash bitcoin.Hash, requiredConfirmations uint) error { + return fmt.Errorf("submission failed") + }, + ) + if err == nil { + t.Fatal("expected submit error to propagate") + } + }) +} + +// TestProveReservationAcceptanceActions is an end-to-end test of the +// top-level orchestration function wired into production via +// runReservationProofLoop: it seeds a requested event, a matching pending +// action, and a matching wallet transaction, then asserts the submit hook +// fires with the correct (reservationKey, requestNonce) pair on the first pass. +// On a second pass with the on-chain action state mutated to Settled, it +// asserts no second submission occurs and the event is evicted from the pending map. +func TestProveReservationAcceptanceActions(t *testing.T) { + const proofStart = 790270 + diff := func(d int64) *big.Int { return big.NewInt(d) } + + spvChain := newLocalChain() + btcChain := newLocalBitcoinChain() + + if err := populateBlockHeaders( + btcChain, + proofStart, + proofStart+19, + func(uint) *big.Int { return diff(32) }, + ); err != nil { + t.Fatal(err) + } + spvChain.setTxProofDifficultyFactor(big.NewInt(6)) + spvChain.setCurrentEpoch(392) + spvChain.setCurrentAndPrevEpochDifficulty(diff(32), diff(16)) + + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + spvChain.setBlockCounter(blockCounter) + + fundingTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{Value: 150000}}, + } + if err := btcChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + fundingTxHash := fundingTx.Hash() + reservationKey := spvChain.BuildDepositKey(fundingTxHash, 0) + const requestNonce = 1 + + spvChain.setDepositRequest(fundingTxHash, 0, &tbtc.DepositChainRequest{ + Amount: 150000, + }) + + walletPublicKeyHash := [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + walletScript, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) + if err != nil { + t.Fatal(err) + } + + transaction := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: walletScript, + }}, + } + if err := btcChain.BroadcastTransaction(transaction); err != nil { + t.Fatal(err) + } + if err := btcChain.addTransactionConfirmations( + transaction.Hash(), + 20, + ); err != nil { + t.Fatal(err) + } + btcChain.setCoinbaseTxHash(transaction.Hash()) + + spvChain.addReservationAcceptanceRequestedEvent(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: reservationKey, + RequestNonce: requestNonce, + WalletPublicKeyHash: walletPublicKeyHash, + BlockNumber: 500, + }) + spvChain.setReservationAction( + reservationKey, + requestNonce, + &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + ActionType: tbtc.ReservationActionTypeAcceptance, + TargetWalletPublicKeyHash: walletPublicKeyHash, + }, + ) + + var submittedReservationKey *big.Int + var submittedRequestNonce uint64 + submissions := 0 + spvChain.submitReservationProofHook = func( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + proof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, + ) error { + submissions++ + submittedReservationKey = reservationKey + submittedRequestNonce = requestNonce + return nil + } + + config := Config{TransactionLimit: 100} + scanState := newReservationProofScanState() + + if err := proveReservationAcceptanceActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if submissions != 1 { + t.Fatalf("expected exactly one proof submission, got %d", submissions) + } + if submittedReservationKey.Cmp(reservationKey) != 0 { + t.Errorf( + "unexpected submitted reservation key\nexpected: %v\nactual: %v", + reservationKey, + submittedReservationKey, + ) + } + if submittedRequestNonce != requestNonce { + t.Errorf( + "unexpected submitted request nonce\nexpected: %d\nactual: %d", + requestNonce, + submittedRequestNonce, + ) + } + + // Second pass: action transitions to Settled. Verify it is not resubmitted + // and is evicted from the pending map. + spvChain.setReservationAction( + reservationKey, + requestNonce, + &tbtc.ReservationAction{ + State: tbtc.ReservationActionStateSettled, + ActionType: tbtc.ReservationActionTypeAcceptance, + TargetWalletPublicKeyHash: walletPublicKeyHash, + }, + ) + + if err := proveReservationAcceptanceActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error on second pass: %v", err) + } + + if submissions != 1 { + t.Errorf("expected submissions to remain 1 on second pass, got %d", submissions) + } + key := reservationEventKey(reservationKey, requestNonce) + if _, exists := scanState.pendingAcceptanceEvents[key]; exists { + t.Errorf("expected settled event to be evicted from pendingAcceptanceEvents") + } +} + +// TestProveReservationReanchorActions is an end-to-end test of the +// top-level orchestration function wired into production via +// runReservationProofLoop: it seeds a requested event, a matching +// reservation with an anchor UTXO, a matching pending action, and a +// matching wallet transaction, then asserts the submit hook fires with the +// correct (reservationKey, requestNonce) pair on the first pass. +// On a second pass with the on-chain action state mutated to Settled, it +// asserts no second submission occurs and the event is evicted from the pending map. +func TestProveReservationReanchorActions(t *testing.T) { + const proofStart = 790270 + diff := func(d int64) *big.Int { return big.NewInt(d) } + + spvChain := newLocalChain() + btcChain := newLocalBitcoinChain() + + if err := populateBlockHeaders( + btcChain, + proofStart, + proofStart+19, + func(uint) *big.Int { return diff(32) }, + ); err != nil { + t.Fatal(err) + } + spvChain.setTxProofDifficultyFactor(big.NewInt(6)) + spvChain.setCurrentEpoch(392) + spvChain.setCurrentAndPrevEpochDifficulty(diff(32), diff(16)) + + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + spvChain.setBlockCounter(blockCounter) + + reservationKey := big.NewInt(424242) + const requestNonce = 2 + + priorAnchorTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{ + {Value: 10000}, + {Value: 600000}, + }, + } + if err := btcChain.BroadcastTransaction(priorAnchorTx); err != nil { + t.Fatal(err) + } + anchorTxHash := priorAnchorTx.Hash() + anchorUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + Value: 600000, + } + + sourceWalletPublicKeyHash := [20]byte{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40} + walletScript, err := bitcoin.PayToWitnessPublicKeyHash(sourceWalletPublicKeyHash) + if err != nil { + t.Fatal(err) + } + + transaction := &bitcoin.Transaction{ + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 1, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 590000, + PublicKeyScript: walletScript, + }}, + } + if err := btcChain.BroadcastTransaction(transaction); err != nil { + t.Fatal(err) + } + if err := btcChain.addTransactionConfirmations( + transaction.Hash(), + 20, + ); err != nil { + t.Fatal(err) + } + btcChain.setCoinbaseTxHash(transaction.Hash()) + + spvChain.addReservationReanchorRequestedEvent(&tbtc.ReservationReanchorRequestedEvent{ + ReservationKey: reservationKey, + RequestNonce: requestNonce, + SourceWalletPublicKeyHash: sourceWalletPublicKeyHash, + TargetWalletPublicKeyHash: sourceWalletPublicKeyHash, + BlockNumber: 500, + }) + spvChain.setReservationAction( + reservationKey, + requestNonce, + &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + ActionType: tbtc.ReservationActionTypeReanchor, + TargetWalletPublicKeyHash: sourceWalletPublicKeyHash, + }, + ) + spvChain.setReservation(reservationKey, &tbtc.Reservation{ + AnchorUtxo: anchorUtxo, + }) + + var submittedReservationKey *big.Int + var submittedRequestNonce uint64 + submissions := 0 + spvChain.submitReservationProofHook = func( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + proof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, + ) error { + submissions++ + submittedReservationKey = reservationKey + submittedRequestNonce = requestNonce + return nil + } + + config := Config{TransactionLimit: 100} + scanState := newReservationProofScanState() + + if err := proveReservationReanchorActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if submissions != 1 { + t.Fatalf("expected exactly one proof submission, got %d", submissions) + } + if submittedReservationKey.Cmp(reservationKey) != 0 { + t.Errorf( + "unexpected submitted reservation key\nexpected: %v\nactual: %v", + reservationKey, + submittedReservationKey, + ) + } + if submittedRequestNonce != requestNonce { + t.Errorf( + "unexpected submitted request nonce\nexpected: %d\nactual: %d", + requestNonce, + submittedRequestNonce, + ) + } + + // Second pass: action transitions to Settled. Verify it is not resubmitted + // and is evicted from the pending map. + spvChain.setReservationAction( + reservationKey, + requestNonce, + &tbtc.ReservationAction{ + State: tbtc.ReservationActionStateSettled, + ActionType: tbtc.ReservationActionTypeReanchor, + TargetWalletPublicKeyHash: sourceWalletPublicKeyHash, + }, + ) + + if err := proveReservationReanchorActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error on second pass: %v", err) + } + + if submissions != 1 { + t.Errorf("expected submissions to remain 1 on second pass, got %d", submissions) + } + key := reservationEventKey(reservationKey, requestNonce) + if _, exists := scanState.pendingReanchorEvents[key]; exists { + t.Errorf("expected settled event to be evicted from pendingReanchorEvents") + } +} + +func TestProveReservationAcceptanceActions_EvictsOnExceededRetries(t *testing.T) { + spvChain := newLocalChain() + btcChain := newLocalBitcoinChain() + + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + spvChain.setBlockCounter(blockCounter) + + reservationKey := big.NewInt(12345) + const requestNonce = 1 + + spvChain.addReservationAcceptanceRequestedEvent(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: reservationKey, + RequestNonce: requestNonce, + WalletPublicKeyHash: [20]byte{1}, + BlockNumber: 500, + }) + // Intentionally do NOT set the reservation action on spvChain, so GetReservationAction fails. + + scanState := newReservationProofScanState() + config := Config{TransactionLimit: 100} + key := reservationEventKey(reservationKey, requestNonce) + + for i := uint(1); i < maxReservationActionLoadRetries; i++ { + if err := proveReservationAcceptanceActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error on pass %d: %v", i, err) + } + + if _, exists := scanState.pendingAcceptanceEvents[key]; !exists { + t.Fatalf("expected event to remain pending on pass %d (retries %d)", i, i) + } + if scanState.acceptanceRetries[key] != i { + t.Errorf("expected retries to be %d, got %d", i, scanState.acceptanceRetries[key]) + } + } + + // Final pass: should exceed max retries and be evicted. + if err := proveReservationAcceptanceActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error on final pass: %v", err) + } + + if _, exists := scanState.pendingAcceptanceEvents[key]; exists { + t.Errorf("expected event to be evicted after exceeding max retries") + } + if _, exists := scanState.acceptanceRetries[key]; exists { + t.Errorf("expected retry entry to be cleaned up after eviction") + } +} + +func TestProveReservationReanchorActions_EvictsOnExceededRetries(t *testing.T) { + spvChain := newLocalChain() + btcChain := newLocalBitcoinChain() + + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + spvChain.setBlockCounter(blockCounter) + + reservationKey := big.NewInt(67890) + const requestNonce = 1 + + spvChain.addReservationReanchorRequestedEvent(&tbtc.ReservationReanchorRequestedEvent{ + ReservationKey: reservationKey, + RequestNonce: requestNonce, + SourceWalletPublicKeyHash: [20]byte{2}, + TargetWalletPublicKeyHash: [20]byte{2}, + BlockNumber: 500, + }) + // Intentionally do NOT set the reservation action on spvChain, so GetReservationAction fails. + + scanState := newReservationProofScanState() + config := Config{TransactionLimit: 100} + key := reservationEventKey(reservationKey, requestNonce) + + for i := uint(1); i < maxReservationActionLoadRetries; i++ { + if err := proveReservationReanchorActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error on pass %d: %v", i, err) + } + + if _, exists := scanState.pendingReanchorEvents[key]; !exists { + t.Fatalf("expected event to remain pending on pass %d (retries %d)", i, i) + } + if scanState.reanchorRetries[key] != i { + t.Errorf("expected retries to be %d, got %d", i, scanState.reanchorRetries[key]) + } + } + + // Final pass: should exceed max retries and be evicted. + if err := proveReservationReanchorActions( + scanState, + config, + spvChain, + spvChain, + btcChain, + ); err != nil { + t.Fatalf("unexpected error on final pass: %v", err) + } + + if _, exists := scanState.pendingReanchorEvents[key]; exists { + t.Errorf("expected event to be evicted after exceeding max retries") + } + if _, exists := scanState.reanchorRetries[key]; exists { + t.Errorf("expected retry entry to be cleaned up after eviction") + } +} diff --git a/pkg/maintainer/spv/reservation_reanchor_proof.go b/pkg/maintainer/spv/reservation_reanchor_proof.go new file mode 100644 index 0000000000..f37da53e64 --- /dev/null +++ b/pkg/maintainer/spv/reservation_reanchor_proof.go @@ -0,0 +1,304 @@ +package spv + +import ( + "fmt" + "math/big" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// ProofTypeReservationReanchor is the value passed to +// SubmitReservationProof as proofType for a reservation re-anchor SPV proof. +// The numeric value mirrors the on-chain ReservationProofType enum (3 = +// Reanchor). +const ProofTypeReservationReanchor uint8 = 3 + +// SubmitReservationReanchorProof drives the SPV proof submission for a +// reservation re-anchor action generation. The caller (the reservation +// proof loop) supplies the (reservationKey, requestNonce) +// pair of the on-chain action generation it is proving, plus the Bitcoin +// transaction hash of the re-anchor transaction already signed and +// broadcast by the wallet coordinator. The proof is fetched from btcChain, +// the re-anchor transaction is rebuilt locally to extract the anchor UTXO +// and target wallet, and the proof is submitted directly to the Bridge via +// the SPV maintainer's SubmitReservationProof entry point (not via +// MaintainerProxy: reservations are not reimbursed). +// +// requiredConfirmations must be > 0; the SPV maintainer relies on it to +// assemble the proof. +func SubmitReservationReanchorProof( + transactionHash bitcoin.Hash, + requiredConfirmations uint, + reservationKey *big.Int, + requestNonce uint64, + btcChain bitcoin.Chain, + spvChain Chain, +) error { + return submitReservationReanchorProof( + transactionHash, + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + bitcoin.AssembleSpvProof, + getGlobalMetricsRecorder(), + ) +} + +func submitReservationReanchorProof( + transactionHash bitcoin.Hash, + requiredConfirmations uint, + reservationKey *big.Int, + requestNonce uint64, + btcChain bitcoin.Chain, + spvChain Chain, + spvProofAssembler spvProofAssembler, + metricsRecorder interface { + IncrementCounter(name string, value float64) + }, +) error { + return submitReservationActionProof( + transactionHash, + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + spvProofAssembler, + metricsRecorder, + ProofTypeReservationReanchor, + "reservation_reanchor_proof", + tbtc.ReservationActionTypeReanchor, + "re-anchor", + ) +} + +// spentOutputAsUtxo fetches the single previous output spent by transaction's +// sole input and returns it as an UnspentTransactionOutput. Shared by +// parseReservationTransaction, which parses a 1-input-1-output reservation +// transaction and needs the spent outpoint's value to build the SPV proof's +// main UTXO. +func spentOutputAsUtxo( + btcChain bitcoin.Chain, + transaction *bitcoin.Transaction, +) (*bitcoin.UnspentTransactionOutput, error) { + if len(transaction.Inputs) != 1 { + return nil, fmt.Errorf( + "reservation transaction must have exactly one input", + ) + } + + spentOutpoint := transaction.Inputs[0].Outpoint + + previousTransaction, err := btcChain.GetTransaction( + spentOutpoint.TransactionHash, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot fetch previous transaction: [%v]", + err, + ) + } + + if int(spentOutpoint.OutputIndex) >= len(previousTransaction.Outputs) { + return nil, fmt.Errorf( + "spent output index [%v] out of bounds for previous "+ + "transaction with [%v] outputs", + spentOutpoint.OutputIndex, + len(previousTransaction.Outputs), + ) + } + + spentOutput := previousTransaction.Outputs[spentOutpoint.OutputIndex] + + return &bitcoin.UnspentTransactionOutput{ + Outpoint: spentOutpoint, + Value: spentOutput.Value, + }, nil +} + +// parseReservationTransaction parses the single input and single output +// of a reservation transaction and returns the UTXO that was spent and +// the target wallet's public key hash from the new output script. +func parseReservationTransaction( + btcChain bitcoin.Chain, + transaction *bitcoin.Transaction, + txType string, +) (*bitcoin.UnspentTransactionOutput, [20]byte, error) { + utxo, err := spentOutputAsUtxo(btcChain, transaction) + if err != nil { + return nil, [20]byte{}, err + } + + if len(transaction.Outputs) != 1 { + return nil, [20]byte{}, fmt.Errorf( + "reservation %v transaction must have exactly one output", + txType, + ) + } + + publicKeyHash, err := bitcoin.ExtractPublicKeyHash( + transaction.Outputs[0].PublicKeyScript, + ) + if err != nil { + return nil, [20]byte{}, fmt.Errorf( + "cannot extract %v public key hash: [%v]", + txType, + err, + ) + } + + return utxo, publicKeyHash, nil +} + +// buildReservationProofTxInfo serializes the relevant parts of the +// transaction into the BitcoinTxInfo structure expected by +// SubmitReservationProof. +func buildReservationProofTxInfo( + transaction *bitcoin.Transaction, +) *tbtc.BitcoinTxInfo { + return &tbtc.BitcoinTxInfo{ + Version: transaction.SerializeVersion(), + InputVector: transaction.SerializeInputs(), + OutputVector: transaction.SerializeOutputs(), + Locktime: transaction.SerializeLocktime(), + } +} + +// buildReservationProofTxProof converts a bitcoin.SpvProof into the +// BitcoinTxProof structure expected by SubmitReservationProof. +func buildReservationProofTxProof( + proof *bitcoin.SpvProof, +) *tbtc.BitcoinTxProof { + txIndexInBlock := big.NewInt(int64(proof.TxIndexInBlock)) + + return &tbtc.BitcoinTxProof{ + MerkleProof: proof.MerkleProof, + TxIndexInBlock: txIndexInBlock, + BitcoinHeaders: proof.BitcoinHeaders, + CoinbasePreimage: proof.CoinbasePreimage, + CoinbaseProof: proof.CoinbaseProof, + } +} + +// buildReservationProofMainUtxo packages the spent deposit or anchor UTXO +// into the BitcoinTxUTXO structure expected by SubmitReservationProof. +func buildReservationProofMainUtxo( + spentUtxo *bitcoin.UnspentTransactionOutput, +) *tbtc.BitcoinTxUTXO { + var ( + txHash [32]byte + txOutIndex uint32 + txOutValue uint64 + ) + + if spentUtxo.Outpoint != nil { + txHash = spentUtxo.Outpoint.TransactionHash + txOutIndex = spentUtxo.Outpoint.OutputIndex + } + if spentUtxo.Value < 0 { + txOutValue = 0 + } else { + txOutValue = uint64(spentUtxo.Value) + } + + return &tbtc.BitcoinTxUTXO{ + TxHash: txHash, + TxOutputIndex: txOutIndex, + TxOutputValue: txOutValue, + } +} + +func submitReservationActionProof( + transactionHash bitcoin.Hash, + requiredConfirmations uint, + reservationKey *big.Int, + requestNonce uint64, + btcChain bitcoin.Chain, + spvChain Chain, + spvProofAssembler spvProofAssembler, + metricsRecorder interface { + IncrementCounter(name string, value float64) + }, + proofType uint8, + metricsPrefix string, + expectedActionType tbtc.ReservationActionType, + txType string, +) error { + if metricsRecorder != nil { + metricsRecorder.IncrementCounter(metricsPrefix+"_submissions_total", 1) + } + + if requiredConfirmations == 0 { + if metricsRecorder != nil { + metricsRecorder.IncrementCounter(metricsPrefix+"_submissions_failed_total", 1) + } + return fmt.Errorf("provided required confirmations count must be greater than 0") + } + if reservationKey == nil { + return fmt.Errorf("reservation key is required") + } + if requestNonce == 0 { + return fmt.Errorf("request nonce must be > 0") + } + + transaction, proof, err := spvProofAssembler( + transactionHash, + requiredConfirmations, + btcChain, + ) + if err != nil { + if metricsRecorder != nil { + metricsRecorder.IncrementCounter(metricsPrefix+"_submissions_failed_total", 1) + } + return fmt.Errorf("failed to assemble transaction spv proof: [%v]", err) + } + + utxo, pkh, err := parseReservationTransaction(btcChain, transaction, txType) + if err != nil { + if metricsRecorder != nil { + metricsRecorder.IncrementCounter(metricsPrefix+"_submissions_failed_total", 1) + } + return fmt.Errorf("error while parsing reservation transaction inputs: [%v]", err) + } + + action, err := spvChain.GetReservationAction(reservationKey, requestNonce) + if err != nil { + return fmt.Errorf("cannot fetch reservation action generation: [%v]", err) + } + + // The action snapshot is the on-chain authorization for the destination, so verify the PKH match. + if pkh != action.TargetWalletPublicKeyHash { + if metricsRecorder != nil { + metricsRecorder.IncrementCounter(metricsPrefix+"_submissions_failed_total", 1) + } + return fmt.Errorf("target wallet public key hash mismatch") + } + + if action.ActionType != expectedActionType { + return fmt.Errorf("reservation action generation is not expected type") + } + + if action.State != tbtc.ReservationActionStatePending { + return fmt.Errorf("reservation action generation is not pending") + } + + txInfo := buildReservationProofTxInfo(transaction) + txProof := buildReservationProofTxProof(proof) + mainUtxo := buildReservationProofMainUtxo(utxo) + + if err := spvChain.SubmitReservationProof( + proofType, + txInfo, + txProof, + mainUtxo, + reservationKey, + requestNonce, + ); err != nil { + return fmt.Errorf("failed to submit reservation proof: [%v]", err) + } + + return nil +} diff --git a/pkg/maintainer/spv/reservation_reanchor_proof_test.go b/pkg/maintainer/spv/reservation_reanchor_proof_test.go new file mode 100644 index 0000000000..18828c6436 --- /dev/null +++ b/pkg/maintainer/spv/reservation_reanchor_proof_test.go @@ -0,0 +1,241 @@ +package spv + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +type mockMetricsRecorder struct { + counts map[string]float64 +} + +func (m *mockMetricsRecorder) IncrementCounter(name string, value float64) { + m.counts[name] += value +} + +// TestSubmitReservationReanchorProof verifies that submitReservationReanchorProof +// correctly parses a 1-input-1-output re-anchor transaction, looks up the +// matching reservation action generation, and submits the SPV proof to the +// chain. It also covers the failure paths for missing action and mismatched +// action type. +func TestSubmitReservationReanchorProof(t *testing.T) { + requiredConfirmations := uint(6) + + btcChain := newLocalBitcoinChain() + spvChain := newLocalChain() + + // Anchor transaction that the re-anchor spends. + anchorTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{}, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 600000, + PublicKeyScript: []byte{}, + }}, + } + if err := btcChain.BroadcastTransaction(anchorTx); err != nil { + t.Fatal(err) + } + anchorTxHash := anchorTx.Hash() + + // Re-anchor transaction: 1 input spending anchorTx output 0, 1 output + // paying to the target wallet. + targetWalletPKH := [20]byte{0x92, 0xa6, 0xec, 0x88, 0x9a, 0x8f, 0xa3, 0x4f, 0x73, 0x1e, 0x63, 0x9e, 0xde, 0xde, 0x4c, 0x75, 0xe1, 0x84, 0x30, 0x7c} + targetScript, err := bitcoin.PayToWitnessPublicKeyHash(targetWalletPKH) + if err != nil { + t.Fatal(err) + } + + reanchorTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: 0, + }, + }}, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 590000, + PublicKeyScript: targetScript, + }}, + } + if err := btcChain.BroadcastTransaction(reanchorTx); err != nil { + t.Fatal(err) + } + + proof := &bitcoin.SpvProof{ + MerkleProof: []byte{0x01}, + TxIndexInBlock: 2, + BitcoinHeaders: []byte{0x03}, + } + + mockSpvProofAssembler := func( + hash bitcoin.Hash, + confirmations uint, + btcChain bitcoin.Chain, + ) (*bitcoin.Transaction, *bitcoin.SpvProof, error) { + if hash == reanchorTx.Hash() && confirmations == requiredConfirmations { + return reanchorTx, proof, nil + } + return nil, nil, fmt.Errorf("unexpected proof assembly request") + } + + reservationKey := big.NewInt(42) + requestNonce := uint64(7) + + spvChain.setReservation(reservationKey, &tbtc.Reservation{ + WalletPublicKeyHash: targetWalletPKH, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: reanchorTx.Inputs[0].Outpoint, + Value: 600000, + }, + State: tbtc.ReservationStateActive, + RequestNonce: requestNonce, + }) + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeReanchor, + State: tbtc.ReservationActionStatePending, + TargetWalletPublicKeyHash: targetWalletPKH, + }) + + // Override SubmitReservationProof on the localChain to capture the call. + spvChain.submitReservationProofHook = func( + proofType uint8, + txInfo *tbtc.BitcoinTxInfo, + txProof *tbtc.BitcoinTxProof, + mainUtxo *tbtc.BitcoinTxUTXO, + rk *big.Int, + rn uint64, + ) error { + if proofType != ProofTypeReservationReanchor { + t.Errorf("unexpected proof type: got %d, want %d", proofType, ProofTypeReservationReanchor) + } + if rk == nil || rk.Cmp(reservationKey) != 0 { + t.Errorf("unexpected reservation key: got %v, want %v", rk, reservationKey) + } + if rn != requestNonce { + t.Errorf("unexpected request nonce: got %d, want %d", rn, requestNonce) + } + if mainUtxo == nil { + t.Fatal("mainUtxo must not be nil") + } + if mainUtxo.TxOutputValue != 600000 { + t.Errorf("unexpected UTXO value: got %d, want %d", mainUtxo.TxOutputValue, 600000) + } + if txInfo == nil { + t.Fatal("txInfo must not be nil") + } + if !bytes.Equal(txProof.MerkleProof, proof.MerkleProof) { + t.Errorf("unexpected merkle proof") + } + return nil + } + + metricsRecorder := &mockMetricsRecorder{counts: make(map[string]float64)} + if err := submitReservationReanchorProof( + reanchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + metricsRecorder, + ); err != nil { + t.Fatal(err) + } + // Check metrics. + if count := metricsRecorder.counts["reservation_reanchor_proof_submissions_total"]; count != 1 { + t.Errorf("unexpected metrics count: got %f, want 1", count) + } + + // Negative path: nil reservationKey. + if err := submitReservationReanchorProof( + reanchorTx.Hash(), + requiredConfirmations, + nil, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for nil reservation key") + } + + // Negative path: zero requestNonce. + if err := submitReservationReanchorProof( + reanchorTx.Hash(), + requiredConfirmations, + reservationKey, + 0, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for zero request nonce") + } + + // Negative path: action generation is not Pending. + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeReanchor, + State: tbtc.ReservationActionStateSettled, + TargetWalletPublicKeyHash: targetWalletPKH, + }) + if err := submitReservationReanchorProof( + reanchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for settled action generation") + } + + // Negative path: action generation is the wrong type. + spvChain.setReservationAction(reservationKey, requestNonce, &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeAcceptance, + State: tbtc.ReservationActionStatePending, + TargetWalletPublicKeyHash: targetWalletPKH, + }) + if err := submitReservationReanchorProof( + reanchorTx.Hash(), + requiredConfirmations, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for wrong action type") + } + + // Negative path: zero requiredConfirmations. + if err := submitReservationReanchorProof( + reanchorTx.Hash(), + 0, + reservationKey, + requestNonce, + btcChain, + spvChain, + mockSpvProofAssembler, + nil, + ); err == nil { + t.Fatal("expected error for zero required confirmations") + } +} diff --git a/pkg/maintainer/spv/reservation_stale_deposit_watch.go b/pkg/maintainer/spv/reservation_stale_deposit_watch.go new file mode 100644 index 0000000000..51ac7655bf --- /dev/null +++ b/pkg/maintainer/spv/reservation_stale_deposit_watch.go @@ -0,0 +1,348 @@ +package spv + +import ( + "fmt" + "math/big" + + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// staleDepositRevealScanLookBackBlocks bounds the reveal-timestamp fallback +// scan. 30 days at 12s/block, mirroring the convention used across this +// package. +const staleDepositRevealScanLookBackBlocks = uint64(216000) + +// StaleDepositResolution indicates the outcome of a stale deposit check +// to help callers (e.g. pollers) decide whether to keep or drop the deposit +// from tracking. +type StaleDepositResolution uint8 + +const ( + // StaleDepositResolutionUnknown is the zero value representing an unknown or errored resolution. + StaleDepositResolutionUnknown StaleDepositResolution = iota + // StaleDepositResolutionKeep indicates the deposit is still pending-stale and should be retained in the tracking set. + StaleDepositResolutionKeep + // StaleDepositResolutionDrop indicates the deposit is no longer a candidate for staleness (e.g. not reserved, live wallet, settled action) and can be dropped from tracking. + StaleDepositResolutionDrop + // StaleDepositResolutionNotified indicates the deposit was confirmed stale and the notification was submitted. + StaleDepositResolutionNotified +) + +// ReservationStaleDepositWatcher observes deposit-revealed events and +// notifies the Bridge when a reserved deposit's acceptance window expired +// without the assigned wallet becoming live. +// +// A reserved deposit is a deposit that was revealed against a reservation +// vault address. The Bridge records the assigned wallet via +// `ReservedDepositWallet`. If that wallet fails to transition to StateLive +// within the reservation action timeout window, the deposit must be released +// back to the default deposit sweep path; otherwise it sits orphaned +// because the anchor can never be produced. The watcher is the backstop that +// flips the deposit's bookkeeping when the wallet never shows up. +type ReservationStaleDepositWatcher struct { + spvChain Chain + notified map[string]struct{} +} + +// NewReservationStaleDepositWatcher constructs a stale-deposit watcher +// bound to the given chain. +func NewReservationStaleDepositWatcher( + spvChain Chain, +) *ReservationStaleDepositWatcher { + return &ReservationStaleDepositWatcher{ + spvChain: spvChain, + notified: make(map[string]struct{}), + } +} + +// CheckStaleReservedDeposit is the synchronous core of the watcher. It is +// invoked by the integration's polling or deferred callback once the action +// timeout window may have elapsed. +// +// The function is intentionally pure: given the chain state and a `now` +// timestamp, it either notifies the Bridge of a stale deposit or skips +// silently. There is no internal scheduling; the caller owns the lifecycle. +// +// Conditions for notification: +// +// 1. `IsReservedDeposit(depositKey)` returns true. A non-reserved deposit +// is the default sweep path's responsibility; the watcher must not +// interfere with it. +// 2. The reservation's assigned wallet exists and is NOT in StateLive. +// A live wallet is expected to anchor the deposit itself; the action +// timeout window only applies when the wallet is missing or has not +// progressed to live. +// 3. The action timeout has elapsed. The watcher derives the timeout +// from the reservation action record at the current reservation +// RequestNonce. If the action has already been advanced +// (Settled/TimedOut/Superseded/Vetoed), the deposit is no longer in +// the pending-stale window and the watcher skips it without notifying. +// +// Parameters: +// - depositKey: the deposit identifier reported by the Bridge. +// - now: the UNIX timestamp against which the action timeout is +// compared. Tests pass an explicit value; production passes +// time.Now().Unix() cast to uint32. +func (rsdw *ReservationStaleDepositWatcher) CheckStaleReservedDeposit( + depositKey *big.Int, + now uint32, +) (StaleDepositResolution, error) { + if depositKey == nil { + return StaleDepositResolutionUnknown, fmt.Errorf("deposit key must not be nil") + } + if _, ok := rsdw.notified[depositKey.String()]; ok { + return StaleDepositResolutionNotified, nil + } + + isReserved, err := rsdw.spvChain.IsReservedDeposit(depositKey) + if err != nil { + return StaleDepositResolutionUnknown, fmt.Errorf( + "failed to determine if deposit [%v] is reserved: [%v]", + depositKey, + err, + ) + } + if !isReserved { + logger.Debugf( + "deposit [%v] is not a reserved deposit; skipping stale check", + depositKey, + ) + return StaleDepositResolutionDrop, nil + } + + walletPublicKeyHash, err := rsdw.spvChain.ReservedDepositWallet(depositKey) + if err != nil { + return StaleDepositResolutionUnknown, fmt.Errorf( + "failed to fetch wallet for reserved deposit [%v]: [%v]", + depositKey, + err, + ) + } + + // The Bridge only assigns a non-zero wallet to a reserved deposit. + // Defensive: if the wallet is zero the deposit bookkeeping is broken; + // rather than notify on partial information, we skip with a warning. + if walletPublicKeyHash == ([20]byte{}) { + logger.Warnf( + "reserved deposit [%v] has no wallet assigned; "+ + "skipping stale notification", + depositKey, + ) + return StaleDepositResolutionDrop, nil + } + + wallet, err := rsdw.spvChain.GetWallet(walletPublicKeyHash) + if err != nil { + return StaleDepositResolutionUnknown, fmt.Errorf( + "failed to fetch wallet [0x%x] for reserved deposit [%v]: [%v]", + walletPublicKeyHash, + depositKey, + err, + ) + } + + // Wallet live: anchor is expected on its own. The watcher does not + // interfere. + if wallet.State == tbtc.StateLive { + logger.Debugf( + "reserved deposit [%v] assigned to live wallet [0x%x]; "+ + "anchor expected; skipping stale notification", + depositKey, + walletPublicKeyHash, + ) + return StaleDepositResolutionDrop, nil + } + + // In m1 the reservation key and deposit key share the same identifier + // space exposed by the Bridge (ReservedDepositWallet and Reservation + // are both keyed by the same value); future revisions of the Bridge + // may introduce disjoint identifiers, in which case this direct use + // of depositKey as reservationKey must be replaced with a real lookup. + reservationKey := depositKey + + reservation, err := rsdw.spvChain.GetReservation(reservationKey) + if err != nil { + return StaleDepositResolutionUnknown, fmt.Errorf( + "failed to fetch reservation [%v]: [%v]", + reservationKey, + err, + ) + } + + var timeoutAt uint32 + if reservation.RequestNonce == 0 { + // No acceptance action generation has ever been requested on-chain + // for this reservation. Derive the staleness deadline from the + // deposit's own reveal timestamp instead of the (nonexistent) action's + // TimeoutAt: find the DepositRevealed event for this deposit key among + // the wallet's events, then load the deposit request's RevealedAt. + derivedTimeout, err := rsdw.deriveTimeoutFromReveal( + depositKey, + walletPublicKeyHash, + ) + if err != nil { + return StaleDepositResolutionUnknown, err + } + timeoutAt = derivedTimeout + } else { + action, err := rsdw.spvChain.GetReservationAction( + reservationKey, + reservation.RequestNonce, + ) + if err != nil { + return StaleDepositResolutionUnknown, fmt.Errorf( + "failed to fetch reservation action for [%v] nonce [%d]: [%v]", + reservationKey, + reservation.RequestNonce, + err, + ) + } + + if action.State == tbtc.ReservationActionStateUnknown { + // Confirmed no action generation exists yet on-chain for this nonce. + // Derive the staleness deadline from the deposit's own reveal timestamp. + derivedTimeout, err := rsdw.deriveTimeoutFromReveal( + depositKey, + walletPublicKeyHash, + ) + if err != nil { + return StaleDepositResolutionUnknown, err + } + timeoutAt = derivedTimeout + } else if action.State != tbtc.ReservationActionStatePending { + logger.Debugf( + "reservation [%v] acceptance action state=%s; "+ + "deposit [%v] is no longer pending-stale; skipping", + reservationKey, + action.State, + depositKey, + ) + return StaleDepositResolutionDrop, nil + } else { + timeoutAt = action.TimeoutAt + } + } + + if now <= timeoutAt { + logger.Debugf( + "reserved deposit [%v] action timeout at [%d] not yet reached "+ + "(now=%d); deferring stale notification", + depositKey, + timeoutAt, + now, + ) + return StaleDepositResolutionKeep, nil + } + + if err := rsdw.spvChain.NotifyStaleReservedDeposit(depositKey); err != nil { + return StaleDepositResolutionUnknown, fmt.Errorf( + "failed to notify stale reserved deposit [%v]: [%v]", + depositKey, + err, + ) + } + rsdw.notified[depositKey.String()] = struct{}{} + + logger.Infof( + "notified stale reserved deposit [%v] "+ + "(wallet [0x%x] state=%s, action timeout %d)", + depositKey, + walletPublicKeyHash, + wallet.State, + timeoutAt, + ) + + return StaleDepositResolutionNotified, nil +} + +func (rsdw *ReservationStaleDepositWatcher) deriveTimeoutFromReveal( + depositKey *big.Int, + walletPublicKeyHash [20]byte, +) (uint32, error) { + blockCounter, err := rsdw.spvChain.BlockCounter() + if err != nil { + return 0, fmt.Errorf( + "failed to get block counter for staleness deadline derivation: [%v]", + err, + ) + } + if blockCounter == nil { + return 0, fmt.Errorf( + "failed to get block counter for staleness deadline derivation: nil block counter", + ) + } + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return 0, fmt.Errorf( + "failed to get current block for staleness deadline derivation: [%v]", + err, + ) + } + + startBlock := uint64(0) + if currentBlock > staleDepositRevealScanLookBackBlocks { + startBlock = currentBlock - staleDepositRevealScanLookBackBlocks + } + + events, eventsErr := rsdw.spvChain.PastDepositRevealedEvents( + &tbtc.DepositRevealedEventFilter{ + StartBlock: startBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + ) + if eventsErr != nil { + return 0, fmt.Errorf( + "failed to fetch deposit revealed events for staleness "+ + "deadline derivation: [%v]", + eventsErr, + ) + } + + var matchingEvent *tbtc.DepositRevealedEvent + for _, event := range events { + if rsdw.spvChain.BuildDepositKey( + event.FundingTxHash, + event.FundingOutputIndex, + ).Cmp(depositKey) == 0 { + matchingEvent = event + break + } + } + if matchingEvent == nil { + return 0, fmt.Errorf( + "no matching DepositRevealed event for deposit [%v]", + depositKey, + ) + } + + depositRequest, found, requestErr := rsdw.spvChain.GetDepositRequest( + matchingEvent.FundingTxHash, + matchingEvent.FundingOutputIndex, + ) + if requestErr != nil { + return 0, fmt.Errorf( + "failed to load deposit request for staleness deadline "+ + "derivation: [%v]", + requestErr, + ) + } + if !found { + return 0, fmt.Errorf( + "deposit request not found for deposit [%v]", + depositKey, + ) + } + + params, paramsErr := rsdw.spvChain.ReservationParameters() + if paramsErr != nil { + return 0, fmt.Errorf( + "failed to load reservation parameters for staleness "+ + "deadline derivation: [%v]", + paramsErr, + ) + } + + return uint32(depositRequest.RevealedAt.Unix()) + + params.ReservationActionTimeout, nil +} diff --git a/pkg/maintainer/spv/reservation_stale_deposit_watch_test.go b/pkg/maintainer/spv/reservation_stale_deposit_watch_test.go new file mode 100644 index 0000000000..e0877638f9 --- /dev/null +++ b/pkg/maintainer/spv/reservation_stale_deposit_watch_test.go @@ -0,0 +1,693 @@ +package spv + +import ( + "fmt" + "math/big" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" + + "github.com/go-test/deep" +) + +// reservationDepositKey returns a big.Int constructed from a uint64 to act +// as a reserved-deposit identifier in the stale-deposit watcher tests. +func reservationDepositKey(low uint64) *big.Int { + return new(big.Int).SetUint64(low) +} + +// reservationActionTimeout is the fixed action timeout used by the tests. +// It is large enough to keep the timeout ordering robust against any +// timestamp arithmetic in the watcher. +const reservationActionTimeout uint32 = 3600 + +func seedPastDepositRevealedEvent( + t *testing.T, + spvChain *localChain, + wallet [20]byte, + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, + currentBlock uint64, +) { + t.Helper() + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + spvChain.setBlockCounter(blockCounter) + + startBlock := uint64(0) + if currentBlock > staleDepositRevealScanLookBackBlocks { + startBlock = currentBlock - staleDepositRevealScanLookBackBlocks + } + endBlock := currentBlock + if err := spvChain.addPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: startBlock, + EndBlock: &endBlock, + WalletPublicKeyHash: [][20]byte{wallet}, + }, + &tbtc.DepositRevealedEvent{ + FundingTxHash: fundingTxHash, + FundingOutputIndex: fundingOutputIndex, + WalletPublicKeyHash: wallet, + }, + ); err != nil { + t.Fatal(err) + } +} + +func TestReservationStaleDepositWatcher_NonReservedDepositIsSkipped(t *testing.T) { + spvChain := newLocalChain() + + // Deposit is NOT booked as reserved. + spvChain.setReservedDeposit(reservationDepositKey(0xB001), walletPKH(), false) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(reservationDepositKey(0xB001), 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionDrop { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionDrop, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("non-reserved deposit must not notify, got %d calls", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_LiveWalletDoesNotNotify(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB002) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateLive, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 10_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionDrop { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionDrop, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("live wallet must not trigger stale notification, got %d calls", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_NotifiesAfterTimeout(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB003) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + + // Inject the acceptance (nonce 1) action with a deadline well below + // `now`. + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + // now (5_000) > action.TimeoutAt (100). + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionNotified { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionNotified, res) + } + + calls := spvChain.getSubmittedStaleReservedDeposits() + if len(calls) != 1 { + t.Fatalf("expected one stale notification, got %d", len(calls)) + } + if diff := deep.Equal(key, calls[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +func TestReservationStaleDepositWatcher_DoesNotNotifyBeforeTimeout(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB004) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + + // Action has a deadline of 10_000; we ask the watcher to evaluate at + // now=5_000, which is before the deadline. + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + TimeoutAt: 10_000, + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionKeep { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionKeep, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("action not yet timed out; expected zero notifications, got %d", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_SettledActionIsSkipped(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB005) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + + // Action is already settled (no longer pending). The watcher must skip + // the stale notification even though the wall clock has passed the + // deadline. + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStateSettled, + TimeoutAt: 100, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionDrop { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionDrop, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("settled action must skip stale notification, got %d calls", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_ZeroWalletSkips(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB006) + spvChain.setReservedDeposit(key, [20]byte{}, true) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionDrop { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionDrop, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("zero-wallet deposit must skip, got %d calls", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_AlreadyNotifiedReturnsNotified(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB007) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionNotified { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionNotified, res) + } + + // Second check for already notified deposit returns Notified without resubmitting. + res, err = watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionNotified { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionNotified, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 1 { + t.Fatalf("expected exactly one notification, got %d", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_NilDepositKeyError(t *testing.T) { + spvChain := newLocalChain() + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(nil, 5_000) + if err == nil { + t.Fatal("expected error for nil deposit key, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } +} + +func TestReservationStaleDepositWatcher_IsReservedDepositChainError(t *testing.T) { + spvChain := newLocalChain() + + spvChain.isReservedDepositErr = fmt.Errorf("rpc unavailable") + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(reservationDepositKey(0xB010), 5_000) + if err == nil { + t.Fatal("expected error when IsReservedDeposit fails, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("expected no notifications on chain error, got %d", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_ReservedDepositWalletChainError(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB011) + spvChain.setReservedDeposit(key, walletPKH(), true) + spvChain.reservedDepositWalletErr = fmt.Errorf("rpc unavailable") + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err == nil { + t.Fatal("expected error when ReservedDepositWallet fails, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("expected no notifications on chain error, got %d", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_GetWalletChainError(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB012) + spvChain.setReservedDeposit(key, walletPKH(), true) + // No spvChain.setWallet call: GetWallet errors naturally. + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err == nil { + t.Fatal("expected error when GetWallet fails, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("expected no notifications on chain error, got %d", len(calls)) + } +} + +func TestReservationStaleDepositWatcher_GetReservationChainError(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB016) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + // No spvChain.setReservation: GetReservation returns an error. + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err == nil { + t.Fatal("expected error when GetReservation fails, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("expected no notifications on chain error, got %d", len(calls)) + } +} + +// TestReservationStaleDepositWatcher_GetReservationActionChainError_DoesNotNotifyEvenPastDeadline +// verifies Fix 1 (P1): a transient RPC error on GetReservationAction must be +// propagated as an error and MUST NOT fall through to the reveal-timestamp +// staleness path or trigger a premature stale deposit notification. +func TestReservationStaleDepositWatcher_GetReservationActionChainError_DoesNotNotifyEvenPastDeadline(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + fundingTxHash, err := bitcoin.NewHashFromString( + "585b6699f42291d1a9d0776b75f04c295ea203f83504349db11e94fdae7d1b2c", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + fundingOutputIndex := uint32(0) + + key := spvChain.BuildDepositKey(fundingTxHash, fundingOutputIndex) + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + // Seed deposit revealed event and request with a deadline in the past: + // RevealedAt (1_000) + Timeout (3600) = 4_600. + seedPastDepositRevealedEvent(t, spvChain, wallet, fundingTxHash, fundingOutputIndex, 0) + spvChain.setDepositRequest(fundingTxHash, fundingOutputIndex, &tbtc.DepositChainRequest{ + RevealedAt: time.Unix(1_000, 0), + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, + }) + + // GetReservationAction is NOT seeded, so it returns an error ("no action for given reservation/nonce"). + watcher := NewReservationStaleDepositWatcher(spvChain) + // now = 10_000 is well past 4_600. Under the buggy code (which conflated + // err != nil with Unknown state), this would fall through to the reveal + // fallback and submit a premature stale deposit notification. + res, err := watcher.CheckStaleReservedDeposit(key, 10_000) + if err == nil { + t.Fatal("expected error on transient GetReservationAction RPC failure, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf( + "transient RPC error must NOT trigger stale deposit notification, got %d calls", + len(calls), + ) + } +} + +// TestReservationStaleDepositWatcher_AdvancingNonceEvaluatesActiveGeneration +// verifies Fix 2 (P2): the watcher reads reservation.RequestNonce via +// GetReservation rather than assuming a hardcoded nonce = 1. If nonce 1 +// timed out and a retry advanced the nonce to 2, the watcher must evaluate +// nonce 2's action generation. +func TestReservationStaleDepositWatcher_AdvancingNonceEvaluatesActiveGeneration(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB020) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + // Reservation has advanced to nonce 2 (e.g. after retry). + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 2, + }) + + // Nonce 1 is TimedOut (stale generation). + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStateTimedOut, + TimeoutAt: 100, + }) + // Nonce 2 is Pending with a timeout in the past relative to now (5_000). + spvChain.setReservationAction(key, 2, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + TimeoutAt: 200, + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + // now = 5_000 > nonce 2's TimeoutAt (200). + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionNotified { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionNotified, res) + } + + calls := spvChain.getSubmittedStaleReservedDeposits() + if len(calls) != 1 { + t.Fatalf("expected one stale notification for nonce 2 timeout, got %d", len(calls)) + } + if diff := deep.Equal(key, calls[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +// TestReservationStaleDepositWatcher_NoActionRequestedYetPropagatesWithoutMatchingEvent +// covers the "no acceptance action generation exists yet" branch +// (reservation.RequestNonce == 0 or action.State == ReservationActionStateUnknown) +// when no matching DepositRevealed event has been seeded either: the +// watcher cannot derive a staleness deadline from nothing, so it must +func TestReservationStaleDepositWatcher_NoActionRequestedYetPropagatesWithoutMatchingEvent(t *testing.T) { + spvChain := newLocalChain() + spvChain.setBlockCounter(newMockBlockCounter()) + + key := reservationDepositKey(0xB013) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 0, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err == nil { + t.Fatal("expected error when no matching deposit revealed event exists, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf("expected no notifications on chain error, got %d", len(calls)) + } +} + +// TestReservationStaleDepositWatcher_NoActionRequestedYetNotifiesFromRevealTimestamp +// tests the reveal-timestamp derivation: a reserved deposit whose wallet never +// became Live, so its acceptance action generation was never requested +// on-chain (RequestNonce == 0). The watcher must derive the staleness deadline +// from the deposit's own reveal timestamp plus ReservationActionTimeout, +// and notify once that derived deadline has passed. +func TestReservationStaleDepositWatcher_NoActionRequestedYetNotifiesFromRevealTimestamp(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + fundingTxHash, err := bitcoin.NewHashFromString( + "585b6699f42291d1a9d0776b75f04c295ea203f83504349db11e94fdae7d1b2c", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + fundingOutputIndex := uint32(0) + + key := spvChain.BuildDepositKey(fundingTxHash, fundingOutputIndex) + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 0, + }) + + seedPastDepositRevealedEvent(t, spvChain, wallet, fundingTxHash, fundingOutputIndex, 0) + spvChain.setDepositRequest(fundingTxHash, fundingOutputIndex, &tbtc.DepositChainRequest{ + RevealedAt: time.Unix(1_000, 0), + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, // 3600 + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + // Derived deadline = RevealedAt (1_000) + ReservationActionTimeout + // (3600) = 4_600. now = 10_000 > 4_600, so the deposit is stale. + res, err := watcher.CheckStaleReservedDeposit(key, 10_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionNotified { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionNotified, res) + } + + calls := spvChain.getSubmittedStaleReservedDeposits() + if len(calls) != 1 { + t.Fatalf("expected one stale notification, got %d", len(calls)) + } + if diff := deep.Equal(key, calls[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +// TestReservationStaleDepositWatcher_NoActionRequestedYetDoesNotNotifyBeforeDerivedDeadline +// mirrors the notifying case above but asks at a `now` before the derived +// deadline, asserting the watcher correctly defers rather than notifying early. +func TestReservationStaleDepositWatcher_NoActionRequestedYetDoesNotNotifyBeforeDerivedDeadline(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + fundingTxHash, err := bitcoin.NewHashFromString( + "7cff663e3e08847a5579913f6a66bc6c01f5f48c6ae1783be77418ed188021e6", + bitcoin.InternalByteOrder, + ) + if err != nil { + t.Fatal(err) + } + fundingOutputIndex := uint32(1) + + key := spvChain.BuildDepositKey(fundingTxHash, fundingOutputIndex) + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 0, + }) + + seedPastDepositRevealedEvent(t, spvChain, wallet, fundingTxHash, fundingOutputIndex, 0) + spvChain.setDepositRequest(fundingTxHash, fundingOutputIndex, &tbtc.DepositChainRequest{ + RevealedAt: time.Unix(1_000, 0), + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: reservationActionTimeout, // 3600 + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + // Derived deadline = 1_000 + 3600 = 4_600. now = 2_000 < 4_600. + res, err := watcher.CheckStaleReservedDeposit(key, 2_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionKeep { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionKeep, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf( + "derived deadline not yet reached; expected zero notifications, got %d", + len(calls), + ) + } +} + +// TestReservationStaleDepositWatcher_NotifierError verifies that the +// stale-deposit watcher propagates a NotifyStaleReservedDeposit failure. +func TestReservationStaleDepositWatcher_NotifierError(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB014) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + TimeoutAt: 100, + }) + spvChain.notifyStaleReservedDepositErr = fmt.Errorf("notifier unavailable") + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err == nil { + t.Fatal("expected error when the notifier fails, got nil") + } + if res != StaleDepositResolutionUnknown { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionUnknown, res) + } +} + +// TestReservationStaleDepositWatcher_ExactTimeoutBoundaryDoesNotNotify +// covers the `now == action.TimeoutAt` boundary explicitly: the watcher's +// condition is `now <= action.TimeoutAt` (must NOT notify), so equality +// must defer exactly like "before the deadline" does. +func TestReservationStaleDepositWatcher_ExactTimeoutBoundaryDoesNotNotify(t *testing.T) { + spvChain := newLocalChain() + + key := reservationDepositKey(0xB015) + wallet := walletPKH() + spvChain.setReservedDeposit(key, wallet, true) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: tbtc.StateUnknown, + }) + spvChain.setReservation(key, &tbtc.Reservation{ + RequestNonce: 1, + }) + spvChain.setReservationAction(key, 1, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStatePending, + TimeoutAt: 5_000, + }) + + watcher := NewReservationStaleDepositWatcher(spvChain) + res, err := watcher.CheckStaleReservedDeposit(key, 5_000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != StaleDepositResolutionKeep { + t.Fatalf("expected resolution %v, got %v", StaleDepositResolutionKeep, res) + } + + if calls := spvChain.getSubmittedStaleReservedDeposits(); len(calls) != 0 { + t.Fatalf( + "now == action.TimeoutAt must not notify, got %d calls", + len(calls), + ) + } +} diff --git a/pkg/maintainer/spv/reservation_stranding_watch.go b/pkg/maintainer/spv/reservation_stranding_watch.go new file mode 100644 index 0000000000..8e0d728aa8 --- /dev/null +++ b/pkg/maintainer/spv/reservation_stranding_watch.go @@ -0,0 +1,101 @@ +package spv + +import ( + "fmt" + + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// ReservationStrandingWatcher observes wallet close/termination events and +// notifies the Bridge of any reservation whose anchor is now stranded. +// +// In tBTC v2 wallets, a live reservation anchor is held in a wallet-controlled +// output. When the wallet is closed or terminated the anchor is stranded: +// the keyset can no longer sign a redemption, reanchor, or dissolution +// transaction for that reservation. The Bridge must be informed so the +// reservation can transition to ReservationStateStranded and the anchor can +// be reconciled via the owner-facing late settlement path. +type ReservationStrandingWatcher struct { + spvChain Chain +} + +// NewReservationStrandingWatcher constructs a stranding watcher bound to the +// given chain. +// +// The watcher is intended to be wired to wallet-close events via a subscription +func NewReservationStrandingWatcher(spvChain Chain) *ReservationStrandingWatcher { + return &ReservationStrandingWatcher{ + spvChain: spvChain, + } +} + +// CheckReservationStrandingForWallet walks the reservations currently +// custodied by walletPublicKeyHash and forwards a stray notification to the +// Bridge for every reservation whose state is Active. +// +// This is the single-shot form used both by tests and by the integration +// wiring that subscribes to wallet close/termination events. It is +// intentionally synchronous and per-wallet: the caller decides which wallets +// to inspect, and the watcher does not run a background loop of its own. + +// The function is idempotent at the chain level: notifying an already-stranded +// reservation is a no-op on the Bridge side. It is the caller's +// responsibility to dedupe notifications across watcher restarts; the watcher +// never silently drops or coalesces calls. +func (rsw *ReservationStrandingWatcher) CheckReservationStrandingForWallet( + walletPublicKeyHash [20]byte, +) error { + keys, err := rsw.spvChain.WalletReservations(walletPublicKeyHash) + if err != nil { + return fmt.Errorf( + "failed to fetch reservations for wallet [0x%x]: [%v]", + walletPublicKeyHash, + err, + ) + } + + for _, key := range keys { + if key == nil { + continue + } + + reservation, err := rsw.spvChain.GetReservation(key) + if err != nil { + logger.Errorf( + "failed to fetch reservation [%v]: [%v]; skipping", + key, + err, + ) + continue + } + + // A reservation with a pending action generation must be left for the + // action-timeout watcher. Marking it stranded would preempt a healthy + // settlement path and trigger gratuitous reconciliation cost for the + // owner. + if reservation.State != tbtc.ReservationStateActive { + logger.Debugf( + "reservation [%v] is not Active (state: %v); "+ + "deferring stray notification to action-timeout watcher", + key, + reservation.State, + ) + continue + } + + if err := rsw.spvChain.NotifyReservationStranded(key); err != nil { + logger.Errorf( + "failed to notify stranded reservation [%v]: [%v]", + key, + err, + ) + // Continue with the remaining reservations: a single failure + // must not starve the others. + continue + } + + logger.Infof("notified stranded reservation [%v]", key) + } + + return nil +} diff --git a/pkg/maintainer/spv/reservation_stranding_watch_test.go b/pkg/maintainer/spv/reservation_stranding_watch_test.go new file mode 100644 index 0000000000..e973aa205b --- /dev/null +++ b/pkg/maintainer/spv/reservation_stranding_watch_test.go @@ -0,0 +1,296 @@ +package spv + +import ( + "fmt" + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/tbtc" + + "github.com/go-test/deep" +) + +// reservationKey constructs an in-memory reservation key with the given low +// 16 bytes set. The watcher only consults `GetReservation` via the local +// chain's [16]byte map key, so compact test keys avoid accidental collisions +// across test cases. +func reservationKey(low uint64) *big.Int { + return new(big.Int).SetUint64(low) +} + +// walletPKH is a deterministic wallet public key hash used in the stranding +// tests. Tests that need a different wallet use walletPKHAt. +func walletPKH() [20]byte { + var out [20]byte + out[19] = 0x42 + return out +} + +// walletPKHAt returns a wallet PKH with the trailing byte set to byte b. It +// exists to make multi-wallet tests readable. +func walletPKHAt(b byte) [20]byte { + var out [20]byte + out[19] = b + return out +} + +func TestReservationStrandingWatcher_NoReservations(t *testing.T) { + spvChain := newLocalChain() + + watcher := NewReservationStrandingWatcher(spvChain) + if watcher == nil { + t.Fatal("expected non-nil watcher") + } + + if err := watcher.CheckReservationStrandingForWallet(walletPKH()); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationStrandedKeys(); len(calls) != 0 { + t.Fatalf( + "expected no notifications, got %d", + len(calls), + ) + } +} + +func TestReservationStrandingWatcher_NotifiesActiveReservation(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xAA01) + + spvChain.setWalletReservations(wallet, []*big.Int{key}) + spvChain.setReservation(key, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + calls := spvChain.getSubmittedReservationStrandedKeys() + if len(calls) != 1 { + t.Fatalf("expected one notification, got %d", len(calls)) + } + if diff := deep.Equal(key, calls[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +func TestReservationStrandingWatcher_SkipsClosedReservation(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xAA02) + + spvChain.setWalletReservations(wallet, []*big.Int{key}) + spvChain.setReservation(key, &tbtc.Reservation{ + State: tbtc.ReservationStateClosed, + }) + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationStrandedKeys(); len(calls) != 0 { + t.Fatalf("expected no notifications, got %d", len(calls)) + } +} + +func TestReservationStrandingWatcher_SkipsPendingReservation(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + key := reservationKey(0xAA03) + + spvChain.setWalletReservations(wallet, []*big.Int{key}) + spvChain.setReservation(key, &tbtc.Reservation{ + State: tbtc.ReservationStateActionPending, + }) + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if calls := spvChain.getSubmittedReservationStrandedKeys(); len(calls) != 0 { + t.Fatalf( + "expected pending reservation to defer to action-timeout, "+ + "got %d notifications", + len(calls), + ) + } +} + +func TestReservationStrandingWatcher_MultipleReservations(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + active := reservationKey(0xAA10) + closed := reservationKey(0xAA11) + pending := reservationKey(0xAA12) + stranded := reservationKey(0xAA13) + + spvChain.setWalletReservations( + wallet, + []*big.Int{active, closed, pending, stranded}, + ) + spvChain.setReservation(active, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + spvChain.setReservation(closed, &tbtc.Reservation{ + State: tbtc.ReservationStateClosed, + }) + spvChain.setReservation(pending, &tbtc.Reservation{ + State: tbtc.ReservationStateActionPending, + }) + spvChain.setReservation(stranded, &tbtc.Reservation{ + State: tbtc.ReservationStateStranded, + }) + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // The watcher must notify only for reservations in the Active state + // (finding #27's allow-list fix): closed, pending, and stranded + // reservations must all be skipped - a stranded reservation has + // already been notified once and re-notifying it is redundant, and a + // closed reservation was already resolved through in-kind redemption + // or another terminal path, not stranding. + calls := spvChain.getSubmittedReservationStrandedKeys() + if len(calls) != 1 { + t.Fatalf( + "expected exactly one notification (active only), "+ + "got %d: %v", + len(calls), + calls, + ) + } + if diff := deep.Equal(active, calls[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +func TestReservationStrandingWatcher_UnknownReservationIsSkipped(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + staleKey := reservationKey(0xAA20) + freshKey := reservationKey(0xAA21) + + // walletReservations references staleKey but the chain has no record of + // it. freshKey is properly recorded. + spvChain.setWalletReservations( + wallet, + []*big.Int{staleKey, freshKey}, + ) + spvChain.setReservation(freshKey, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + calls := spvChain.getSubmittedReservationStrandedKeys() + if len(calls) != 1 { + t.Fatalf("expected one notification (freshKey), got %d", len(calls)) + } + if diff := deep.Equal(freshKey, calls[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +func TestReservationStrandingWatcher_WalletChainError(t *testing.T) { + spvChain := newLocalChain() + + // No walletReservations entry: WalletReservations returns (nil, nil) for + // unknown wallets; the watcher iterates over a nil slice and exits + // cleanly, so we expect no error here. Reserve the case for wallets that + // have an entry which the chain then refuses to enumerate. + wallet := walletPKH() + spvChain.setWalletReservations(wallet, nil) + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf("unexpected error for empty wallet: %v", err) + } + + if calls := spvChain.getSubmittedReservationStrandedKeys(); len(calls) != 0 { + t.Fatalf( + "expected zero notifications on empty wallet list, got %d", + len(calls), + ) + } +} + +// TestReservationStrandingWatcher_NotifierErrorContinuesProcessing mirrors +// TestReservationActionTimeoutWatcher_NotifiesOncePerQualifyingNonce's +// resilience property for the sibling action-timeout watcher: a single +// NotifyReservationStranded failure must not starve the remaining +// reservations in the same wallet. +func TestReservationStrandingWatcher_NotifierErrorContinuesProcessing(t *testing.T) { + spvChain := newLocalChain() + + wallet := walletPKH() + failing := reservationKey(0xAA40) + succeeding := reservationKey(0xAA41) + + spvChain.setWalletReservations(wallet, []*big.Int{failing, succeeding}) + spvChain.setReservation(failing, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + spvChain.setReservation(succeeding, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + spvChain.notifyReservationStrandedErrByKey = map[string]error{ + failing.String(): fmt.Errorf("notifier unavailable"), + } + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(wallet); err != nil { + t.Fatalf( + "a single notifier failure must not fail the whole check: %v", + err, + ) + } + + notified := spvChain.getSubmittedReservationStrandedKeys() + if len(notified) != 1 { + t.Fatalf( + "expected the remaining reservation to still be notified, got %d", + len(notified), + ) + } + if diff := deep.Equal(succeeding, notified[0]); diff != nil { + t.Errorf("unexpected notified key: %v", diff) + } +} + +// TestReservationStrandingWatcher_WalletReservationsChainError covers the +// case TestReservationStrandingWatcher_WalletChainError's comment +// explicitly calls out as unreserved: WalletReservations itself failing +// (as opposed to returning an empty list for an unknown wallet). +func TestReservationStrandingWatcher_WalletReservationsChainError(t *testing.T) { + spvChain := newLocalChain() + + spvChain.walletReservationsErr = fmt.Errorf("rpc unavailable") + + watcher := NewReservationStrandingWatcher(spvChain) + if err := watcher.CheckReservationStrandingForWallet(walletPKH()); err == nil { + t.Fatal("expected error when WalletReservations fails, got nil") + } + + if calls := spvChain.getSubmittedReservationStrandedKeys(); len(calls) != 0 { + t.Fatalf( + "expected no notifications on chain error, got %d", + len(calls), + ) + } +} diff --git a/pkg/maintainer/spv/reservation_wiring.go b/pkg/maintainer/spv/reservation_wiring.go new file mode 100644 index 0000000000..ce61f8ebc4 --- /dev/null +++ b/pkg/maintainer/spv/reservation_wiring.go @@ -0,0 +1,346 @@ +package spv + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/keep-network/keep-core/pkg/subscription" + "github.com/keep-network/keep-core/pkg/tbtc" + + "github.com/ipfs/go-log/v2" +) + +var reservationWiringLogger = log.Logger("keep-maintainer-spv-reservations") + +// DefaultReservationStaleDepositPollInterval is the default poll interval used +// by the stale-deposit watcher fallback loop. The Bridge does not expose a +// live subscription for DepositRevealed in m1, so the wiring layer falls back +// to PastDepositRevealedEvents on a coarse interval and dispatches each new +// reveal to the watcher. The interval mirrors the action-timeout poll +// cadence so a single tick covers both reservation timers. +const DefaultReservationStaleDepositPollInterval = 1 * time.Minute + +// DefaultReservationActionTimeoutPollInterval is the default poll interval +// for the action-timeout watcher's Run loop. It is intentionally conservative +// (1 minute) to limit Bridge load until the production wiring tightens the +// cadence. The interval is fixed. +const DefaultReservationActionTimeoutPollInterval = 1 * time.Minute + +// WalletClosedChain defines the chain interface required to subscribe to +// wallet close events. +type WalletClosedChain interface { + OnWalletClosed( + handler func(event *tbtc.WalletClosedEvent), + ) subscription.EventSubscription +} + +// WireReservationWatchers is the integration entry point that cmd/start.go +// calls directly when config.Reservations.Enabled is true. It constructs +// the three reservation watchers (stranding, stale-deposit, action-timeout), +// wires their Bridge-facing notifiers to the chain, and subscribes/starts +// each watcher against its source. +// +// `walletClosedChain` supplies the OnWalletClosed event subscription; +// `spvChain` supplies the reservation data reads, event queries, and +// Notify* writes. `ctx` controls the goroutine lifetimes started by the +// wiring function. +func WireReservationWatchers( + ctx context.Context, + walletClosedChain WalletClosedChain, + spvChain Chain, + walletMembersResolver tbtc.WalletMembersResolver, +) error { + if walletClosedChain == nil { + return fmt.Errorf("wallet closed chain must not be nil") + } + if spvChain == nil { + return fmt.Errorf("spv chain must not be nil") + } + if walletMembersResolver == nil { + return fmt.Errorf("wallet members resolver must not be nil") + } + + reservationWiringLogger.Infof( + "wiring reservation watchers; ensure Maintainer.Spv.Reservations.Enabled " + + "is also enabled in the SPV maintainer config for end-to-end operation", + ) + + strandingWatcher := NewReservationStrandingWatcher(spvChain) + + // Startup catch-up scan: a wallet closed/terminated while this + // maintainer was down would otherwise never notify, since the live + // OnWalletClosed subscription only sees events from this point forward. + // We scan all past wallet registrations starting from block 0 and check + // the ones already Closed/Terminated now. Transient per-wallet errors + // log warnings rather than failing client startup. + registeredEvents, err := spvChain.PastNewWalletRegisteredEvents( + &tbtc.NewWalletRegisteredEventFilter{StartBlock: 0}, + ) + if err != nil { + reservationWiringLogger.Warnf( + "stranding startup scan failed to fetch wallet registration events: [%v]", + err, + ) + } else { + for _, event := range registeredEvents { + wallet, err := spvChain.GetWallet(event.WalletPublicKeyHash) + if err != nil { + reservationWiringLogger.Warnf( + "stranding startup scan failed to fetch wallet [0x%x]: [%v]", + event.WalletPublicKeyHash, + err, + ) + continue + } + if wallet.State != tbtc.StateClosed && + wallet.State != tbtc.StateTerminated { + continue + } + if err := strandingWatcher.CheckReservationStrandingForWallet( + event.WalletPublicKeyHash, + ); err != nil { + reservationWiringLogger.Warnf( + "stranding startup scan failed to check wallet [0x%x]: [%v]", + event.WalletPublicKeyHash, + err, + ) + continue + } + } + } + + staleDepositWatcher := NewReservationStaleDepositWatcher(spvChain) + + actionTimeoutWatcher := NewReservationActionTimeoutWatcher( + spvChain, + walletMembersResolver, + DefaultReservationActionTimeoutPollInterval, + ) + + subscription := subscribeReservationWalletClosed(ctx, walletClosedChain, spvChain, strandingWatcher) + go func() { + <-ctx.Done() + subscription.Unsubscribe() + }() + startStaleDepositPoll(ctx, spvChain, staleDepositWatcher) + go func() { + if err := actionTimeoutWatcher.Run(ctx); err != nil { + reservationWiringLogger.Errorf( + "failed to run reservation action-timeout watcher: [%v]", + err, + ) + } + }() + + return nil +} + +// subscribeReservationWalletClosed registers the stranding watcher against +// the chain's wallet close / termination events. Each event dispatches a +// worker goroutine that resolves the closed wallet's ECDSA wallet ID (the +// only identifier WalletClosedEvent carries) to its public key hash and +// runs the watcher's stranding check for that wallet. +func subscribeReservationWalletClosed( + ctx context.Context, + walletClosedChain WalletClosedChain, + spvChain Chain, + watcher *ReservationStrandingWatcher, +) subscription.EventSubscription { + return walletClosedChain.OnWalletClosed(func(event *tbtc.WalletClosedEvent) { + go func() { + select { + case <-ctx.Done(): + return + default: + } + walletPublicKeyHash, err := resolveWalletPublicKeyHash( + spvChain, + event.WalletID, + ) + if err != nil { + reservationWiringLogger.Errorf( + "failed to resolve public key hash for closed "+ + "wallet ID [0x%x]: [%v]", + event.WalletID, + err, + ) + return + } + + if err := watcher.CheckReservationStrandingForWallet( + walletPublicKeyHash, + ); err != nil { + reservationWiringLogger.Errorf( + "failed to check reservation stranding for closed "+ + "wallet [0x%x] (ID [0x%x]): [%v]", + walletPublicKeyHash, + event.WalletID, + err, + ) + } + }() + }) +} + +// resolveWalletPublicKeyHash maps an ECDSA wallet ID to the wallet's public +// key hash via its NewWalletRegistered event. Every wallet is registered +// exactly once before it can be closed, and the filter is indexed on the +// wallet ID, so this is a targeted lookup rather than a history scan. +func resolveWalletPublicKeyHash( + spvChain Chain, + walletID [32]byte, +) ([20]byte, error) { + events, err := spvChain.PastNewWalletRegisteredEvents( + &tbtc.NewWalletRegisteredEventFilter{ + EcdsaWalletID: [][32]byte{walletID}, + }, + ) + if err != nil { + return [20]byte{}, fmt.Errorf( + "failed to fetch wallet registration event: [%w]", + err, + ) + } + if len(events) == 0 { + return [20]byte{}, fmt.Errorf( + "no wallet registration event found for wallet ID [0x%x]", + walletID, + ) + } + + // A wallet ID is registered at most once; take the latest match + // defensively in case of a duplicate log delivery. + return events[len(events)-1].WalletPublicKeyHash, nil +} + +// reservationStaleDepositLookBackBlocks bounds the first stale-deposit poll +// tick's DepositRevealed scan. 30 days at 12s/block, mirroring +// ReservationAcceptanceLookBackBlocks in pkg/tbtcpg. Subsequent ticks scan +// incrementally from the previous tick's block, so this bound only matters +// once, at startup. +const reservationStaleDepositLookBackBlocks = uint64(216000) + +// startStaleDepositPoll runs the stale-deposit watcher's live source as a +// polling loop over PastDepositRevealedEvents: the Bridge does not expose a +// live subscription for DepositRevealed in m1. Each tick fetches reveals +// since the previously scanned block, adds every reserved deposit among +// them to a tracked pending set, then re-runs CheckStaleReservedDeposit for +// every deposit already in the set. A deposit is dropped from the set once +// it is no longer reserved (released to the default sweep path, or swept) +// or its assigned wallet has gone Live - both mean it can never go stale +// again, so re-checking it forever would be wasted RPCs. +// +// The poller is intentionally tolerant of chain errors: a transient RPC +// failure logs and continues rather than aborting the wiring. +func startStaleDepositPoll( + ctx context.Context, + spvChain Chain, + watcher *ReservationStaleDepositWatcher, +) { + go func() { + ticker := time.NewTicker(DefaultReservationStaleDepositPollInterval) + defer ticker.Stop() + + var lastSeenBlock uint64 + pending := make(map[string]*big.Int) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + + blockCounter, err := spvChain.BlockCounter() + if err != nil { + reservationWiringLogger.Errorf( + "stale-deposit poll failed to get block counter: [%v]", + err, + ) + continue + } + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + reservationWiringLogger.Errorf( + "stale-deposit poll failed to get current block: [%v]", + err, + ) + continue + } + + startBlock := lastSeenBlock + if startBlock == 0 && currentBlock > reservationStaleDepositLookBackBlocks { + startBlock = currentBlock - reservationStaleDepositLookBackBlocks + } + + events, err := spvChain.PastDepositRevealedEvents( + &tbtc.DepositRevealedEventFilter{ + StartBlock: startBlock + 1, + EndBlock: ¤tBlock, + }, + ) + if err != nil { + reservationWiringLogger.Errorf( + "stale-deposit poll failed to fetch deposit revealed "+ + "events: [%v]", + err, + ) + continue + } + + var batchErr error + for _, event := range events { + depositKey := spvChain.BuildDepositKey( + event.FundingTxHash, + event.FundingOutputIndex, + ) + + isReserved, err := spvChain.IsReservedDeposit(depositKey) + if err != nil { + reservationWiringLogger.Errorf( + "stale-deposit poll failed to check if deposit "+ + "[%v] is reserved: [%v]", + depositKey, + err, + ) + batchErr = err + break + } + if !isReserved { + continue + } + + pending[depositKey.String()] = depositKey + } + if batchErr != nil { + continue + } + + lastSeenBlock = currentBlock + now := uint32(time.Now().Unix()) + + for key, depositKey := range pending { + resolution, err := watcher.CheckStaleReservedDeposit( + depositKey, + now, + ) + if err != nil { + reservationWiringLogger.Errorf( + "stale-deposit poll failed to check deposit "+ + "[%v]: [%v]", + depositKey, + err, + ) + continue + } + + if resolution == StaleDepositResolutionDrop || + resolution == StaleDepositResolutionNotified { + delete(pending, key) + } + } + } + }() +} diff --git a/pkg/maintainer/spv/reservation_wiring_test.go b/pkg/maintainer/spv/reservation_wiring_test.go new file mode 100644 index 0000000000..0ab475e385 --- /dev/null +++ b/pkg/maintainer/spv/reservation_wiring_test.go @@ -0,0 +1,371 @@ +package spv + +import ( + "context" + "fmt" + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/subscription" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// TestResolveWalletPublicKeyHash covers the three branches of +// resolveWalletPublicKeyHash: a matching NewWalletRegistered event found, no +// matching event found, and a chain read error. +func TestResolveWalletPublicKeyHash(t *testing.T) { + walletID := [32]byte{0x01, 0x02, 0x03} + expectedPKH := [20]byte{0xAA, 0xBB, 0xCC} + + t.Run("found", func(t *testing.T) { + spvChain := newLocalChain() + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: walletID, + WalletPublicKeyHash: expectedPKH, + }) + + pkh, err := resolveWalletPublicKeyHash(spvChain, walletID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pkh != expectedPKH { + t.Errorf( + "unexpected public key hash\nexpected: %x\nactual: %x", + expectedPKH, + pkh, + ) + } + }) + + t.Run("not found", func(t *testing.T) { + spvChain := newLocalChain() + // No matching event registered for walletID; a different wallet's + // event exists to confirm the filter, not just an empty set, drives + // the not-found path. + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: [32]byte{0x99}, + WalletPublicKeyHash: expectedPKH, + }) + + _, err := resolveWalletPublicKeyHash(spvChain, walletID) + if err == nil { + t.Fatal("expected error for missing wallet registration event") + } + }) + + t.Run("chain error", func(t *testing.T) { + spvChain := newLocalChain() + spvChain.setPastNewWalletRegisteredEventsErr( + fmt.Errorf("rpc unavailable"), + ) + + _, err := resolveWalletPublicKeyHash(spvChain, walletID) + if err == nil { + t.Fatal("expected chain error to propagate") + } + }) + + t.Run("duplicate event delivery uses the latest match", func(t *testing.T) { + spvChain := newLocalChain() + staleePKH := [20]byte{0x11} + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: walletID, + WalletPublicKeyHash: staleePKH, + }) + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: walletID, + WalletPublicKeyHash: expectedPKH, + }) + + pkh, err := resolveWalletPublicKeyHash(spvChain, walletID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pkh != expectedPKH { + t.Errorf( + "expected the latest matching event to win\nexpected: %x\nactual: %x", + expectedPKH, + pkh, + ) + } + }) +} + +// TestCheckStaleReservedDeposit_Resolution covers the resolution outcomes of +// CheckStaleReservedDeposit used by the poller to decide pending-set retention: +// a deposit still reserved with unreached timeout must be kept, non-reserved +// deposits, deposits with live wallets, or settled actions must be dropped, +// and timed-out deposits must be notified and evicted. +func TestCheckStaleReservedDeposit_Resolution(t *testing.T) { + tests := map[string]struct { + isReserved bool + walletState tbtc.WalletState + actionState tbtc.ReservationActionState + timeoutAt uint32 + now uint32 + expectedResolution StaleDepositResolution + }{ + "not reserved": { + isReserved: false, + walletState: tbtc.StateMovingFunds, + actionState: tbtc.ReservationActionStatePending, + timeoutAt: 100, + now: 1000, + expectedResolution: StaleDepositResolutionDrop, + }, + "reserved, wallet live": { + isReserved: true, + walletState: tbtc.StateLive, + actionState: tbtc.ReservationActionStatePending, + timeoutAt: 100, + now: 1000, + expectedResolution: StaleDepositResolutionDrop, + }, + "reserved, action settled": { + isReserved: true, + walletState: tbtc.StateMovingFunds, + actionState: tbtc.ReservationActionStateSettled, + timeoutAt: 100, + now: 1000, + expectedResolution: StaleDepositResolutionDrop, + }, + "reserved, timeout not yet reached": { + isReserved: true, + walletState: tbtc.StateMovingFunds, + actionState: tbtc.ReservationActionStatePending, + timeoutAt: 5000, + now: 1000, + expectedResolution: StaleDepositResolutionKeep, + }, + "reserved, timeout passed and notified": { + isReserved: true, + walletState: tbtc.StateMovingFunds, + actionState: tbtc.ReservationActionStatePending, + timeoutAt: 100, + now: 1000, + expectedResolution: StaleDepositResolutionNotified, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + spvChain := newLocalChain() + depositKey := reservationDepositKey(0xCC01) + wallet := walletPKH() + spvChain.setReservedDeposit(depositKey, wallet, test.isReserved) + spvChain.setWallet(wallet, &tbtc.WalletChainData{ + State: test.walletState, + }) + spvChain.setReservation(depositKey, &tbtc.Reservation{ + RequestNonce: 1, + }) + spvChain.setReservationAction(depositKey, 1, &tbtc.ReservationAction{ + State: test.actionState, + TimeoutAt: test.timeoutAt, + }) + spvChain.setReservationParameters(&tbtc.ReservationParameters{ + ReservationActionTimeout: 3600, + }) + watcher := NewReservationStaleDepositWatcher(spvChain) + resolution, err := watcher.CheckStaleReservedDeposit(depositKey, test.now) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resolution != test.expectedResolution { + t.Errorf( + "unexpected resolution\nexpected: %v\nactual: %v", + test.expectedResolution, + resolution, + ) + } + }) + } +} + +type mockWalletMembersResolver struct { + resolveFn func(walletPublicKeyHash [20]byte) ([]uint32, error) +} + +func (m *mockWalletMembersResolver) ResolveWalletMembers(walletPublicKeyHash [20]byte) ([]uint32, error) { + return m.resolveFn(walletPublicKeyHash) +} + +type mockWalletClosedChain struct { + onWalletClosedHandler func(event *tbtc.WalletClosedEvent) +} + +func (m *mockWalletClosedChain) OnWalletClosed( + handler func(event *tbtc.WalletClosedEvent), +) subscription.EventSubscription { + m.onWalletClosedHandler = handler + return subscription.NewEventSubscription(func() {}) +} + +func TestWireReservationWatchers(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + walletClosedChain := &mockWalletClosedChain{} + spvChain := newLocalChain() + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + spvChain.setBlockCounter(blockCounter) + + resolver := &mockWalletMembersResolver{ + resolveFn: func(walletPublicKeyHash [20]byte) ([]uint32, error) { + return []uint32{1, 2, 3}, nil + }, + } + + if err := WireReservationWatchers(ctx, walletClosedChain, spvChain, resolver); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestWireReservationWatchers_NilParameters(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + walletClosedChain := &mockWalletClosedChain{} + spvChain := newLocalChain() + resolver := &mockWalletMembersResolver{ + resolveFn: func(walletPublicKeyHash [20]byte) ([]uint32, error) { + return []uint32{1}, nil + }, + } + + t.Run("nil wallet closed chain", func(t *testing.T) { + err := WireReservationWatchers(ctx, nil, spvChain, resolver) + if err == nil { + t.Fatal("expected error for nil wallet closed chain") + } + }) + + t.Run("nil spv chain", func(t *testing.T) { + err := WireReservationWatchers(ctx, walletClosedChain, nil, resolver) + if err == nil { + t.Fatal("expected error for nil spv chain") + } + }) + + t.Run("nil wallet members resolver", func(t *testing.T) { + err := WireReservationWatchers(ctx, walletClosedChain, spvChain, nil) + if err == nil { + t.Fatal("expected error for nil wallet members resolver") + } + }) +} + +// TestWireReservationWatchers_StartupCatchUpScan_TransientErrorsDoNotAbort +// proves Fix 1: during the stranding watcher's startup catch-up scan, a transient +// chain-read failure against one wallet (e.g. GetWallet returning an error) +// does not abort the entire startup. The scan continues to the next wallets, +// properly processing Closed and Terminated wallets while skipping Live ones. +func TestWireReservationWatchers_StartupCatchUpScan_TransientErrorsDoNotAbort(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + walletClosedChain := &mockWalletClosedChain{} + spvChain := newLocalChain() + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(5000) + spvChain.setBlockCounter(blockCounter) + + resolver := &mockWalletMembersResolver{ + resolveFn: func(walletPublicKeyHash [20]byte) ([]uint32, error) { + return []uint32{1, 2, 3}, nil + }, + } + + walletTransientError := walletPKHAt(0x01) + walletClosed := walletPKHAt(0x02) + walletLive := walletPKHAt(0x03) + walletTerminated := walletPKHAt(0x04) + + resKeyClosed := reservationKey(0xDD02) + resKeyLive := reservationKey(0xDD03) + resKeyTerminated := reservationKey(0xDD04) + + // Register all 4 wallets in NewWalletRegisteredEvents. + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: [32]byte{0x01}, + WalletPublicKeyHash: walletTransientError, + }) + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: [32]byte{0x02}, + WalletPublicKeyHash: walletClosed, + }) + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: [32]byte{0x03}, + WalletPublicKeyHash: walletLive, + }) + spvChain.addNewWalletRegisteredEvent(&tbtc.NewWalletRegisteredEvent{ + EcdsaWalletID: [32]byte{0x04}, + WalletPublicKeyHash: walletTerminated, + }) + + // walletTransientError is NOT added to spvChain.wallets, so GetWallet + // returns "no wallet for given PKH" simulating a transient RPC error. + + // walletClosed is Closed with an Active reservation. + spvChain.setWallet(walletClosed, &tbtc.WalletChainData{ + State: tbtc.StateClosed, + }) + spvChain.setWalletReservations(walletClosed, []*big.Int{resKeyClosed}) + spvChain.setReservation(resKeyClosed, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + + // walletLive is Live with an Active reservation (must be skipped). + spvChain.setWallet(walletLive, &tbtc.WalletChainData{ + State: tbtc.StateLive, + }) + spvChain.setWalletReservations(walletLive, []*big.Int{resKeyLive}) + spvChain.setReservation(resKeyLive, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + + // walletTerminated is Terminated with an Active reservation. + spvChain.setWallet(walletTerminated, &tbtc.WalletChainData{ + State: tbtc.StateTerminated, + }) + spvChain.setWalletReservations(walletTerminated, []*big.Int{resKeyTerminated}) + spvChain.setReservation(resKeyTerminated, &tbtc.Reservation{ + State: tbtc.ReservationStateActive, + }) + + // WireReservationWatchers must succeed without returning an error despite + // walletTransientError failing GetWallet. + err := WireReservationWatchers(ctx, walletClosedChain, spvChain, resolver) + if err != nil { + t.Fatalf("expected WireReservationWatchers to succeed despite transient wallet error: %v", err) + } + + // Verify that the stranded active reservations for walletClosed and + // walletTerminated were both notified, while walletLive was skipped. + notifiedKeys := spvChain.getSubmittedReservationStrandedKeys() + if len(notifiedKeys) != 2 { + t.Fatalf("expected 2 notified stranded keys, got %d: %v", len(notifiedKeys), notifiedKeys) + } + + foundClosed := false + foundTerminated := false + for _, k := range notifiedKeys { + if k.Cmp(resKeyClosed) == 0 { + foundClosed = true + } + if k.Cmp(resKeyTerminated) == 0 { + foundTerminated = true + } + if k.Cmp(resKeyLive) == 0 { + t.Errorf("live wallet reservation was unexpectedly notified as stranded") + } + } + + if !foundClosed { + t.Errorf("expected closed wallet reservation [%v] to be notified", resKeyClosed) + } + if !foundTerminated { + t.Errorf("expected terminated wallet reservation [%v] to be notified", resKeyTerminated) + } +} diff --git a/pkg/maintainer/spv/spv.go b/pkg/maintainer/spv/spv.go index 6191a2144d..dee9a22f2b 100644 --- a/pkg/maintainer/spv/spv.go +++ b/pkg/maintainer/spv/spv.go @@ -51,6 +51,23 @@ func Initialize( btcChain: btcChain, } + if config.Reservations.Enabled { + logger.Infof( + "SPV maintainer reservation proof submission is enabled; " + + "ensure the paired Tbtc.Reservations.Enabled flag is also " + + "enabled in the client config for end-to-end operation", + ) + // Reservation acceptance/re-anchor proofs run on a dedicated loop, + // not through the generic proofTypes map: SubmitReservationProof + // requires the (reservationKey, requestNonce) pair of the action + // generation being proven, which the generic + // unprovenTransactionsGetter/transactionProofSubmitter signatures + // (shared by deposit sweep, redemption, moving funds, and moved + // funds sweep, none of which need that pair) cannot carry. See + // reservation_proof_loop.go. + go maintainReservationProofs(ctx, config, spvChain, btcDiffChain, btcChain) + } + go spvMaintainer.startControlLoop(ctx) } diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index a58599f273..09e6b75868 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -34,9 +34,9 @@ type GroupSelectionChain interface { } // GroupSelectionResult represents a group selection result, i.e. operators -// selected to perform the DKG protocol. The result consists of two slices -// of equal length holding the chain.OperatorID and chain.Address for each -// selected operator. +// selected to perform the group key generation protocol. The result consists of +// two slices of equal length holding the chain.OperatorID and chain.Address for +// each selected operator. type GroupSelectionResult struct { OperatorsIDs chain.OperatorIDs OperatorsAddresses chain.Addresses @@ -292,6 +292,13 @@ type BridgeChain interface { fundingOutputIndex uint32, ) (*DepositChainRequest, bool, error) + // BuildDepositKey calculates a deposit key for the given funding + // transaction hash and output index. Mirrors tbtcpg.Chain's identical + // method - the reservation anchor wallet action needs it to derive the + // m1 reservation key (reservationKey == depositKey) without depending + // on the tbtcpg package. + BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int + // GetMovedFundsSweepRequest gets the on-chain moved funds sweep request for // the given moving funds transaction hash and output index. // The returned bool value indicates whether the request was found or not. @@ -427,6 +434,26 @@ type WalletProposalValidatorChain interface { }, ) error + // ValidateReservationAnchorProposal validates the given reservation + // anchor proposal against the chain. Returns an error if the proposal + // is not valid or nil otherwise. + ValidateReservationAnchorProposal( + walletPublicKeyHash [20]byte, + proposal *ReservationAnchorProposal, + depositExtraInfo struct { + *Deposit + FundingTx *bitcoin.Transaction + }, + ) error + + // ValidateReservationReanchorProposal validates the given reservation + // re-anchor proposal against the chain. Returns an error if the + // proposal is not valid or nil otherwise. + ValidateReservationReanchorProposal( + sourceWalletPublicKeyHash [20]byte, + proposal *ReservationReanchorProposal, + ) error + // ValidateRedemptionProposal validates the given redemption proposal // against the chain. Returns an error if the proposal is not valid or // nil otherwise. @@ -545,4 +572,336 @@ type Chain interface { InactivityClaimChain BridgeChain WalletProposalValidatorChain + ReservationChain +} + +// ReservationChain defines the subset of the TBTC chain interface that pertains +// specifically to UTXO reservation Bridge operations. The reservation state +// machine is implemented behind Bridge.fallback's delegatecall to the +// ReservationRouter contract; the binding is constructed against the Bridge +// address, so every read, write, and log subscription on this interface +// routes through the Bridge's storage rather than the router's empty +// standalone storage. +type ReservationChain interface { + // RequestReservationAcceptance requests a reservation acceptance action + // generation for the given reservation. The reservation must be in a + // state that allows acceptance; the operator-side guard is enforced at + // the chain layer. + RequestReservationAcceptance( + reservationKey *big.Int, + walletPublicKeyHash [20]byte, + ) error + + // RequestReservationReanchor requests a reservation re-anchor action + // generation for the given reservation, targeting the given wallet. + RequestReservationReanchor( + reservationKey *big.Int, + targetWalletPublicKeyHash [20]byte, + ) error + + // SubmitReservationProof submits an SPV proof for the given reservation + // action generation. proofType selects between Acceptance, Redemption, + // Reanchor, and Dissolution proofs; m1 invokes only Acceptance (1) and + // Reanchor (3). The call is restricted to the SPV maintainer registered + // against the Bridge. + SubmitReservationProof( + proofType uint8, + txInfo *BitcoinTxInfo, + proof *BitcoinTxProof, + mainUtxo *BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, + ) error + + // NotifyReservationActionTimeout notifies the Bridge that the timeout + // for the given reservation action generation has elapsed without the + // SPV proof being submitted. The walletMembersIDs carry the operator + // IDs of the wallet that was authorized for the action; they are used + // to slash the wallet in m2-era records (no-op in m1). + NotifyReservationActionTimeout( + reservationKey *big.Int, + walletMembersIDs []uint32, + ) error + + // NotifyStaleReservedDeposit notifies the Bridge that the given reserved + // deposit's wallet did not anchor it within the reservation-action + // timeout and should be released back to the default sweeping path. + NotifyStaleReservedDeposit(depositKey *big.Int) error + + // NotifyReservationStranded notifies the Bridge that the wallet + // custodying the given reservation has been closed or terminated and + // the anchor is therefore stranded. This is the m1 path that closes + // reservations whose wallet is no longer live. + NotifyReservationStranded(reservationKey *big.Int) error + + // GetReservation gets the on-chain reservation record for the given + // reservation key. Returns an error if the reservation was not found. + GetReservation(reservationKey *big.Int) (*Reservation, error) + + // GetReservationAction gets the on-chain action record for the given + // reservation key and request nonce. Returns an error if the action + // generation was not found. + GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, + ) (*ReservationAction, error) + + // ReservationParameters gets the current on-chain values of the Bridge + // reservation parameters. + ReservationParameters() (*ReservationParameters, error) + + // ReservationCaps returns the cap parameters that gate reservation + // acceptance: the maximum aggregate satoshi amount a single wallet may + // custody across all of its reservations, and the maximum satoshi + // amount any single reservation may anchor. + ReservationCaps() (maxReservationsAmountPerWallet uint64, reservationMaxSingleAmount uint64, err error) + + // WalletReservationsAmount returns the aggregate satoshi amount + // currently anchored by the given wallet across all of its + // reservations. + WalletReservationsAmount(walletPublicKeyHash [20]byte) (uint64, error) + + // WalletReservationsCount returns the number of reservations currently + // custodied by the given wallet. + WalletReservationsCount(walletPublicKeyHash [20]byte) (uint32, error) + + // WalletReservations returns the reservation keys for all reservations + // currently custodied by the given wallet. + WalletReservations(walletPublicKeyHash [20]byte) ([]*big.Int, error) + + // ReservedDepositWallet returns the wallet public key hash to which + // the given reserved deposit was revealed. Returns the zero hash if the + // deposit is not a reserved deposit. + ReservedDepositWallet(depositKey *big.Int) ([20]byte, error) + + // ActiveReservationsCount returns the current count of active + // reservations across all wallets and the cap on that count. + ActiveReservationsCount() (count uint32, maxActive uint32, err error) + + // IsReservedDeposit returns true if the given deposit was revealed + // with the reservation vault address and is therefore a reservation + // rather than a default deposit. + IsReservedDeposit(depositKey *big.Int) (bool, error) + + // OnReservationAcceptanceRequested registers a callback that is invoked + // when an on-chain ReservationAcceptanceRequested event is seen. + OnReservationAcceptanceRequested( + handler func(event *ReservationAcceptanceRequestedEvent), + ) subscription.EventSubscription + + // PastReservationAcceptanceRequestedEvents fetches past + // ReservationAcceptanceRequested events according to the provided + // filter or unfiltered if the filter is nil. Returned events are sorted + // by the block number in the ascending order, i.e. the latest event is + // at the end of the slice. + PastReservationAcceptanceRequestedEvents( + filter *ReservationAcceptanceRequestedEventFilter, + ) ([]*ReservationAcceptanceRequestedEvent, error) + + // OnReservationReanchorRequested registers a callback that is invoked + // when an on-chain ReservationReanchorRequested event is seen. + OnReservationReanchorRequested( + handler func(event *ReservationReanchorRequestedEvent), + ) subscription.EventSubscription + + // PastReservationReanchorRequestedEvents fetches past + // ReservationReanchorRequested events according to the provided filter + // or unfiltered if the filter is nil. + PastReservationReanchorRequestedEvents( + filter *ReservationReanchorRequestedEventFilter, + ) ([]*ReservationReanchorRequestedEvent, error) +} + +// BitcoinTxInfo represents the on-chain BitcoinTx.Info struct used by +// reservation proof submissions. +type BitcoinTxInfo struct { + Version [4]byte + InputVector []byte + OutputVector []byte + Locktime [4]byte +} + +// BitcoinTxProof represents the on-chain BitcoinTx.Proof struct used by +// reservation proof submissions. +type BitcoinTxProof struct { + MerkleProof []byte + TxIndexInBlock *big.Int + BitcoinHeaders []byte + CoinbasePreimage [32]byte + CoinbaseProof []byte +} + +// BitcoinTxUTXO represents the on-chain BitcoinTx.UTXO struct used by +// reservation proof submissions. +type BitcoinTxUTXO struct { + TxHash [32]byte + TxOutputIndex uint32 + TxOutputValue uint64 +} + +// ReservationAcceptanceRequestedEvent represents a reservation acceptance +// requested event. +type ReservationAcceptanceRequestedEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + WalletPublicKeyHash [20]byte + DepositAmount uint64 + TxMaxFee uint64 + TimeoutAt uint32 + BlockNumber uint64 +} + +// ReservationAcceptanceRequestedEventFilter is a component allowing to filter +// ReservationAcceptanceRequestedEvent. +type ReservationAcceptanceRequestedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + ReservationKey []*big.Int + WalletPublicKeyHash [][20]byte +} + +// ReservationAcceptedEvent represents a reservation accepted event. +type ReservationAcceptedEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + WalletPublicKeyHash [20]byte + Owner chain.Address + AnchorTxHash [32]byte + AnchorAmount uint64 + ExpiresAt uint32 + BlockNumber uint64 +} + +// ReservationAcceptedEventFilter is a component allowing to filter +// ReservationAcceptedEvent. +type ReservationAcceptedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + ReservationKey []*big.Int + WalletPublicKeyHash [][20]byte + Owner []chain.Address +} + +// ReservationReanchorRequestedEvent represents a reservation re-anchor +// requested event. +type ReservationReanchorRequestedEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + SourceWalletPublicKeyHash [20]byte + TargetWalletPublicKeyHash [20]byte + TxMaxFee uint64 + BlockNumber uint64 +} + +// ReservationReanchorRequestedEventFilter is a component allowing to filter +// ReservationReanchorRequestedEvent. +type ReservationReanchorRequestedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + ReservationKey []*big.Int + SourceWalletPublicKeyHash [][20]byte + TargetWalletPublicKeyHash [][20]byte +} + +// ReservationReanchoredEvent represents a reservation re-anchored event. +type ReservationReanchoredEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + NewWalletPublicKeyHash [20]byte + NewAnchorTxHash [32]byte + NewAnchorAmount uint64 + BlockNumber uint64 +} + +// ReservationReanchoredEventFilter is a component allowing to filter +// ReservationReanchoredEvent. +type ReservationReanchoredEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + ReservationKey []*big.Int + NewWalletPublicKeyHash [][20]byte +} + +// ReservationActionTimedOutEvent represents a reservation action timed out +// event. +type ReservationActionTimedOutEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + ActionType ReservationActionType + BlockNumber uint64 +} + +// ReservationActionTimedOutEventFilter is a component allowing to filter +// ReservationActionTimedOutEvent. +type ReservationActionTimedOutEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + ReservationKey []*big.Int +} + +// ReservationActionSupersededEvent represents a reservation action superseded +// event. +type ReservationActionSupersededEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + BlockNumber uint64 +} + +// ReservationLateSettledEvent represents a reservation late-settled event. +type ReservationLateSettledEvent struct { + ReservationKey *big.Int + RequestNonce uint64 + ActionType ReservationActionType + BlockNumber uint64 +} + +// ReservationRetryCreditMintedEvent represents a reservation retry credit +// minted event. +type ReservationRetryCreditMintedEvent struct { + ReservationKey *big.Int + BlockNumber uint64 +} + +// ReservedDepositMarkedStaleEvent represents a reserved deposit marked stale +// event. +type ReservedDepositMarkedStaleEvent struct { + DepositKey *big.Int + BlockNumber uint64 +} + +// ReservationStrandedEvent represents a reservation stranded event. +type ReservationStrandedEvent struct { + ReservationKey *big.Int + WalletPublicKeyHash [20]byte + Owner chain.Address + AnchorAmount uint64 + BlockNumber uint64 +} + +// ReservationParametersUpdatedEvent represents a reservation parameters +// updated event. +type ReservationParametersUpdatedEvent struct { + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationTermSeconds uint32 + ReservationDissolutionDelay uint32 + ReservationMaxTotalAmount uint64 + MaxReservationsPerWallet uint32 + ReservationActionTimeout uint32 + ReservationRenewalWindowSeconds uint32 + BlockNumber uint64 +} + +// ReservationVaultUpdatedEvent represents a reservation vault updated event. +type ReservationVaultUpdatedEvent struct { + ReservationVault chain.Address + BlockNumber uint64 +} + +// ReservationCapsUpdatedEvent represents a reservation caps updated event. +type ReservationCapsUpdatedEvent struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 + MaxActiveReservations uint32 + BlockNumber uint64 } diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 1dc799dc84..38b83f00bc 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -14,6 +14,8 @@ import ( "time" "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/crypto/sha3" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/chain/local_v1" @@ -24,7 +26,6 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/inactivity" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" - "golang.org/x/crypto/sha3" ) const ( @@ -102,6 +103,11 @@ type localChain struct { blockCounter chain.BlockCounter operatorPrivateKey *operator.PrivateKey + + reservation *Reservation + reservationAction *ReservationAction + validateReservationAnchorProposalErr error + validateReservationReanchorProposalErr error } func (lc *localChain) BlockCounter() (chain.BlockCounter, error) { @@ -802,6 +808,15 @@ func (lc *localChain) GetDepositRequest( return request, true, nil } +func (lc *localChain) BuildDepositKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) *big.Int { + depositKeyBytes := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + return new(big.Int).SetBytes(depositKeyBytes[:]) +} + func (lc *localChain) setDepositRequest( fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, @@ -1450,3 +1465,187 @@ func generateHandlerID() int { // Local chain implementation doesn't require secure randomness. return rand.Int() } + +// GetReservation returns the reservation previously installed via +// setReservation. Panics if never set, matching this fake chain's +// convention for exercising an unconfigured dependency. +func (lc *localChain) GetReservation( + reservationKey *big.Int, +) (*Reservation, error) { + if lc.reservation != nil { + return lc.reservation, nil + } + panic("unsupported") +} + +// setReservation installs the reservation GetReservation returns. +func (lc *localChain) setReservation(reservation *Reservation) { + lc.reservation = reservation +} + +// GetReservationAction returns the reservation action previously installed +// via setReservationAction. Panics if never set, matching this fake +// chain's convention for exercising an unconfigured dependency. +func (lc *localChain) GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, +) (*ReservationAction, error) { + if lc.reservationAction != nil { + return lc.reservationAction, nil + } + panic("unsupported") +} + +// setReservationAction installs the action GetReservationAction returns. +func (lc *localChain) setReservationAction(action *ReservationAction) { + lc.reservationAction = action +} + +func (lc *localChain) ReservationParameters() (*ReservationParameters, error) { + panic("unsupported") +} + +// ValidateReservationAnchorProposal returns the error previously installed +// via setValidateReservationAnchorProposalErr, or nil (accept) by default. +func (lc *localChain) ValidateReservationAnchorProposal( + walletPublicKeyHash [20]byte, + proposal *ReservationAnchorProposal, + depositExtraInfo struct { + *Deposit + FundingTx *bitcoin.Transaction + }, +) error { + return lc.validateReservationAnchorProposalErr +} + +// setValidateReservationAnchorProposalErr installs the error +// ValidateReservationAnchorProposal returns. +func (lc *localChain) setValidateReservationAnchorProposalErr(err error) { + lc.validateReservationAnchorProposalErr = err +} + +// ValidateReservationReanchorProposal returns the error previously +// installed via setValidateReservationReanchorProposalErr, or nil (accept) +// by default. +func (lc *localChain) ValidateReservationReanchorProposal( + sourceWalletPublicKeyHash [20]byte, + proposal *ReservationReanchorProposal, +) error { + return lc.validateReservationReanchorProposalErr +} + +// setValidateReservationReanchorProposalErr installs the error +// ValidateReservationReanchorProposal returns. +func (lc *localChain) setValidateReservationReanchorProposalErr(err error) { + lc.validateReservationReanchorProposalErr = err +} + +func (lc *localChain) RequestReservationAcceptance( + reservationKey *big.Int, + walletPublicKeyHash [20]byte, +) error { + panic("unsupported") +} + +func (lc *localChain) RequestReservationReanchor( + reservationKey *big.Int, + targetWalletPublicKeyHash [20]byte, +) error { + panic("unsupported") +} + +func (lc *localChain) SubmitReservationProof( + proofType uint8, + txInfo *BitcoinTxInfo, + proof *BitcoinTxProof, + mainUtxo *BitcoinTxUTXO, + reservationKey *big.Int, + requestNonce uint64, +) error { + panic("unsupported") +} + +func (lc *localChain) NotifyReservationActionTimeout( + reservationKey *big.Int, + walletMembersIDs []uint32, +) error { + panic("unsupported") +} + +func (lc *localChain) NotifyStaleReservedDeposit( + depositKey *big.Int, +) error { + panic("unsupported") +} + +func (lc *localChain) NotifyReservationStranded( + reservationKey *big.Int, +) error { + panic("unsupported") +} + +func (lc *localChain) ReservationCaps() ( + uint64, + uint64, + error, +) { + return 0, 0, fmt.Errorf("unsupported") +} + +func (lc *localChain) WalletReservationsAmount( + walletPublicKeyHash [20]byte, +) (uint64, error) { + return 0, fmt.Errorf("unsupported") +} + +func (lc *localChain) WalletReservationsCount( + walletPublicKeyHash [20]byte, +) (uint32, error) { + return 0, fmt.Errorf("unsupported") +} + +func (lc *localChain) WalletReservations( + walletPublicKeyHash [20]byte, +) ([]*big.Int, error) { + return nil, fmt.Errorf("unsupported") +} + +func (lc *localChain) ReservedDepositWallet( + depositKey *big.Int, +) ([20]byte, error) { + return [20]byte{}, fmt.Errorf("unsupported") +} + +func (lc *localChain) ActiveReservationsCount() (uint32, uint32, error) { + return 0, 0, fmt.Errorf("unsupported") +} + +func (lc *localChain) IsReservedDeposit( + depositKey *big.Int, +) (bool, error) { + return false, fmt.Errorf("unsupported") +} + +func (lc *localChain) OnReservationAcceptanceRequested( + handler func(event *ReservationAcceptanceRequestedEvent), +) subscription.EventSubscription { + return subscription.NewEventSubscription(func() {}) +} + +func (lc *localChain) PastReservationAcceptanceRequestedEvents( + filter *ReservationAcceptanceRequestedEventFilter, +) ([]*ReservationAcceptanceRequestedEvent, error) { + return nil, fmt.Errorf("unsupported") +} + +func (lc *localChain) OnReservationReanchorRequested( + handler func(event *ReservationReanchorRequestedEvent), +) subscription.EventSubscription { + return subscription.NewEventSubscription(func() {}) +} + +func (lc *localChain) PastReservationReanchorRequestedEvents( + filter *ReservationReanchorRequestedEventFilter, +) ([]*ReservationReanchorRequestedEvent, error) { + return nil, fmt.Errorf("unsupported") +} diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 2dd75e9614..1840b54042 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -10,17 +10,19 @@ import ( "strings" "time" - "github.com/keep-network/keep-core/pkg/internal/pb" "go.uber.org/zap" "golang.org/x/exp/slices" + "github.com/keep-network/keep-core/pkg/internal/pb" + + "golang.org/x/sync/semaphore" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/clientinfo" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/group" - "golang.org/x/sync/semaphore" ) const ( @@ -66,6 +68,13 @@ const ( // upgrade to a binary containing this constant before the activation block // is reached. DepositSweepEveryWindowActivationBlock = uint64(24559289) + // ReservationsActivationBlock is the Ethereum block height at which + // reservation actions (anchor, re-anchor) become available in the + // coordination checklist. + // + // NOTE: This value is a placeholder that MUST be set to the real mainnet + // rollout height before release and must stay ahead of the chain tip. + ReservationsActivationBlock = uint64(26500000) ) // errCoordinationExecutorBusy is an error returned when the coordination @@ -631,6 +640,27 @@ func (ce *coordinationExecutor) getActionsChecklist( } } + // Reservation actions (acceptance, re-anchor) checklist gate is deliberately + // config-independent and height-only so every operator computes an + // identical checklist once the network-wide activation block passes. + // If the checklist depended on each operator's local config flag, + // operators with different local settings would compute different checklists + // and fault each other's proposals via FaultLeaderMistake. Checklist + // agreement is achieved because the gate ignores local config and uses only + // globally-observable chain height. Config.Reservations.Enabled controls + // only whether THIS operator originates (leader-proposes) new reservation + // actions and whether its reservation watchers run - it does NOT prevent + // this operator from evaluating/countersigning another leader's reservation + // proposal as a follower once the activation height passes, regardless of + // this operator's own local flag setting. Frequency-gated like + // DepositSweep/MovingFunds below the activation block: reservation + // acceptance/re-anchor windows are not as time-critical as redemption. + if coordinationBlock >= ReservationsActivationBlock && + windowIndex%frequencyWindows == 0 { + actions = append(actions, ActionReservationAnchor) + actions = append(actions, ActionReservationReanchor) + } + // #nosec G404 (insecure random number source (rand)) // Drawing a decision about heartbeat does not require secure randomness. // Use first 8 bytes of the seed to initialize the RNG. diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index c597048fb0..4623485107 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -11,6 +11,8 @@ import ( "time" "github.com/go-test/deep" + "golang.org/x/exp/slices" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/chain/local_v1" @@ -20,7 +22,6 @@ import ( "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" - "golang.org/x/exp/slices" "github.com/keep-network/keep-core/internal/testutils" ) @@ -838,6 +839,88 @@ func TestCoordinationExecutor_GetActionsChecklist_PostActivation(t *testing.T) { } } +// TestCoordinationExecutor_GetActionsChecklist_Reservations verifies the +// reservation actions checklist gate depends solely on the activation +// block and the frequency window, never on a local per-operator +// configuration flag - see coordinationExecutor.getActionsChecklist's +// comment for why: a follower gating checklist validation on its own +// local flag would wrongly fault an honest leader whenever the two +// operators' local configs diverge. +func TestCoordinationExecutor_GetActionsChecklist_Reservations(t *testing.T) { + tests := map[string]struct { + coordinationBlock uint64 + windowIndex uint64 + expectedActions []WalletActionType + }{ + "below activation": { + coordinationBlock: ReservationsActivationBlock - 1, + windowIndex: 4, + expectedActions: []WalletActionType{ActionRedemption}, + }, + "at activation, non-4th window": { + coordinationBlock: ReservationsActivationBlock, + windowIndex: 5, + expectedActions: []WalletActionType{ActionRedemption}, + }, + "at activation, 4th window": { + coordinationBlock: ReservationsActivationBlock, + windowIndex: 4, + expectedActions: []WalletActionType{ActionRedemption, ActionReservationAnchor, ActionReservationReanchor}, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + executor := &coordinationExecutor{} + + // We don't care about the seed for this test, as it only affects + // the ActionHeartbeat which is not the focus here. + seed := [32]byte{} + + checklist := executor.getActionsChecklist( + test.windowIndex, + seed, + test.coordinationBlock, + ) + + // We only care about reservation actions. + var actualReservationActions []WalletActionType + for _, action := range checklist { + if action == ActionReservationAnchor || action == ActionReservationReanchor { + actualReservationActions = append(actualReservationActions, action) + } + } + + var expectedReservationActions []WalletActionType + for _, action := range test.expectedActions { + if action == ActionReservationAnchor || action == ActionReservationReanchor { + expectedReservationActions = append(expectedReservationActions, action) + } + } + + if diff := deep.Equal(actualReservationActions, expectedReservationActions); diff != nil { + t.Errorf("reservation actions mismatch: %v", diff) + } + }) + } +} + +func TestReservationsActivationBlock_SanityCheck(t *testing.T) { + // Reference Ethereum mainnet block height as of 2026-09-02 (~25,880,000). + // ReservationsActivationBlock must be set to a future block height ahead + // of chain tip before release. If this test fails, both the reference + // height and ReservationsActivationBlock must be updated. + const referenceMainnetBlockHeight = uint64(25880000) + + if ReservationsActivationBlock <= referenceMainnetBlockHeight { + t.Errorf( + "ReservationsActivationBlock [%d] must be ahead of the reference mainnet block height [%d]", + ReservationsActivationBlock, + referenceMainnetBlockHeight, + ) + } +} + // assertPostActivationSafety verifies the safety invariants that must hold // for every non-nil post-activation checklist: // - ActionRedemption is at index 0. @@ -893,11 +976,13 @@ func assertChecklistOrdering( t.Helper() actionPriority := map[WalletActionType]int{ - ActionRedemption: 0, - ActionDepositSweep: 1, - ActionMovedFundsSweep: 2, - ActionMovingFunds: 3, - ActionHeartbeat: 4, + ActionRedemption: 0, + ActionDepositSweep: 1, + ActionMovedFundsSweep: 2, + ActionMovingFunds: 3, + ActionReservationAnchor: 4, + ActionReservationReanchor: 5, + ActionHeartbeat: 6, } for i := 1; i < len(checklist); i++ { diff --git a/pkg/tbtc/gen/pb/message.pb.go b/pkg/tbtc/gen/pb/message.pb.go index 7496ad009d..10ffa49071 100644 --- a/pkg/tbtc/gen/pb/message.pb.go +++ b/pkg/tbtc/gen/pb/message.pb.go @@ -508,6 +508,148 @@ func (x *MovedFundsSweepProposal) GetSweepTxFee() []byte { return nil } +type ReservationAnchorProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositFundingTxHash []byte `protobuf:"bytes,1,opt,name=depositFundingTxHash,proto3" json:"depositFundingTxHash,omitempty"` + DepositFundingOutputIndex uint32 `protobuf:"varint,2,opt,name=depositFundingOutputIndex,proto3" json:"depositFundingOutputIndex,omitempty"` + RequestNonce uint64 `protobuf:"varint,3,opt,name=requestNonce,proto3" json:"requestNonce,omitempty"` + AnchorTxFee []byte `protobuf:"bytes,4,opt,name=anchorTxFee,proto3" json:"anchorTxFee,omitempty"` +} + +func (x *ReservationAnchorProposal) Reset() { + *x = ReservationAnchorProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReservationAnchorProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReservationAnchorProposal) ProtoMessage() {} + +func (x *ReservationAnchorProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[8] + 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 ReservationAnchorProposal.ProtoReflect.Descriptor instead. +func (*ReservationAnchorProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{8} +} + +func (x *ReservationAnchorProposal) GetDepositFundingTxHash() []byte { + if x != nil { + return x.DepositFundingTxHash + } + return nil +} + +func (x *ReservationAnchorProposal) GetDepositFundingOutputIndex() uint32 { + if x != nil { + return x.DepositFundingOutputIndex + } + return 0 +} + +func (x *ReservationAnchorProposal) GetRequestNonce() uint64 { + if x != nil { + return x.RequestNonce + } + return 0 +} + +func (x *ReservationAnchorProposal) GetAnchorTxFee() []byte { + if x != nil { + return x.AnchorTxFee + } + return nil +} + +type ReservationReanchorProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ReservationKey []byte `protobuf:"bytes,1,opt,name=reservationKey,proto3" json:"reservationKey,omitempty"` + RequestNonce uint64 `protobuf:"varint,2,opt,name=requestNonce,proto3" json:"requestNonce,omitempty"` + TargetWalletPublicKeyHash []byte `protobuf:"bytes,3,opt,name=targetWalletPublicKeyHash,proto3" json:"targetWalletPublicKeyHash,omitempty"` + ReanchorTxFee []byte `protobuf:"bytes,4,opt,name=reanchorTxFee,proto3" json:"reanchorTxFee,omitempty"` +} + +func (x *ReservationReanchorProposal) Reset() { + *x = ReservationReanchorProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReservationReanchorProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReservationReanchorProposal) ProtoMessage() {} + +func (x *ReservationReanchorProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[9] + 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 ReservationReanchorProposal.ProtoReflect.Descriptor instead. +func (*ReservationReanchorProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{9} +} + +func (x *ReservationReanchorProposal) GetReservationKey() []byte { + if x != nil { + return x.ReservationKey + } + return nil +} + +func (x *ReservationReanchorProposal) GetRequestNonce() uint64 { + if x != nil { + return x.RequestNonce + } + return 0 +} + +func (x *ReservationReanchorProposal) GetTargetWalletPublicKeyHash() []byte { + if x != nil { + return x.TargetWalletPublicKeyHash + } + return nil +} + +func (x *ReservationReanchorProposal) GetReanchorTxFee() []byte { + if x != nil { + return x.ReanchorTxFee + } + return nil +} + type DepositSweepProposal_DepositKey struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -520,7 +662,7 @@ type DepositSweepProposal_DepositKey struct { func (x *DepositSweepProposal_DepositKey) Reset() { *x = DepositSweepProposal_DepositKey{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[8] + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -533,7 +675,7 @@ func (x *DepositSweepProposal_DepositKey) String() string { func (*DepositSweepProposal_DepositKey) ProtoMessage() {} func (x *DepositSweepProposal_DepositKey) ProtoReflect() protoreflect.Message { - mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[8] + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -642,8 +784,34 @@ var file_pkg_tbtc_gen_pb_message_proto_rawDesc = []byte{ 0x46, 0x75, 0x6e, 0x64, 0x73, 0x54, 0x78, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x46, - 0x65, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x65, 0x65, 0x22, 0xd3, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x41, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, + 0x12, 0x32, 0x0a, 0x14, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x46, 0x75, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x14, + 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, + 0x48, 0x61, 0x73, 0x68, 0x12, 0x3c, 0x0a, 0x19, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x46, + 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x19, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4e, 0x6f, 0x6e, + 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, + 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x6e, 0x63, + 0x68, 0x6f, 0x72, 0x54, 0x78, 0x46, 0x65, 0x65, 0x22, 0xcd, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x73, + 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, + 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x65, + 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0e, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, + 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4e, + 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x19, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, + 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x19, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, + 0x73, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x54, 0x78, + 0x46, 0x65, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x61, 0x6e, 0x63, + 0x68, 0x6f, 0x72, 0x54, 0x78, 0x46, 0x65, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -658,7 +826,7 @@ func file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP() []byte { return file_pkg_tbtc_gen_pb_message_proto_rawDescData } -var file_pkg_tbtc_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_pkg_tbtc_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_pkg_tbtc_gen_pb_message_proto_goTypes = []interface{}{ (*SigningDoneMessage)(nil), // 0: tbtc.SigningDoneMessage (*CoordinationProposal)(nil), // 1: tbtc.CoordinationProposal @@ -668,16 +836,18 @@ var file_pkg_tbtc_gen_pb_message_proto_goTypes = []interface{}{ (*RedemptionProposal)(nil), // 5: tbtc.RedemptionProposal (*MovingFundsProposal)(nil), // 6: tbtc.MovingFundsProposal (*MovedFundsSweepProposal)(nil), // 7: tbtc.MovedFundsSweepProposal - (*DepositSweepProposal_DepositKey)(nil), // 8: tbtc.DepositSweepProposal.DepositKey + (*ReservationAnchorProposal)(nil), // 8: tbtc.ReservationAnchorProposal + (*ReservationReanchorProposal)(nil), // 9: tbtc.ReservationReanchorProposal + (*DepositSweepProposal_DepositKey)(nil), // 10: tbtc.DepositSweepProposal.DepositKey } var file_pkg_tbtc_gen_pb_message_proto_depIdxs = []int32{ - 1, // 0: tbtc.CoordinationMessage.proposal:type_name -> tbtc.CoordinationProposal - 8, // 1: tbtc.DepositSweepProposal.depositsKeys:type_name -> tbtc.DepositSweepProposal.DepositKey - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 1, // 0: tbtc.CoordinationMessage.proposal:type_name -> tbtc.CoordinationProposal + 10, // 1: tbtc.DepositSweepProposal.depositsKeys:type_name -> tbtc.DepositSweepProposal.DepositKey + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_pkg_tbtc_gen_pb_message_proto_init() } @@ -783,6 +953,30 @@ func file_pkg_tbtc_gen_pb_message_proto_init() { } } file_pkg_tbtc_gen_pb_message_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReservationAnchorProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReservationReanchorProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DepositSweepProposal_DepositKey); i { case 0: return &v.state @@ -801,7 +995,7 @@ func file_pkg_tbtc_gen_pb_message_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_pkg_tbtc_gen_pb_message_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/tbtc/gen/pb/message.proto b/pkg/tbtc/gen/pb/message.proto index e26d31ab46..46833241fe 100644 --- a/pkg/tbtc/gen/pb/message.proto +++ b/pkg/tbtc/gen/pb/message.proto @@ -53,3 +53,17 @@ message MovedFundsSweepProposal { uint32 movingFundsTxOutputIndex = 2; bytes sweepTxFee = 3; } + +message ReservationAnchorProposal { + bytes depositFundingTxHash = 1; + uint32 depositFundingOutputIndex = 2; + uint64 requestNonce = 3; + bytes anchorTxFee = 4; +} + +message ReservationReanchorProposal { + bytes reservationKey = 1; + uint64 requestNonce = 2; + bytes targetWalletPublicKeyHash = 3; + bytes reanchorTxFee = 4; +} diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 02b5195e45..3874e51ea6 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -229,12 +229,14 @@ func unmarshalCoordinationProposal(actionType uint32, payload []byte) ( } proposal, ok := map[WalletActionType]CoordinationProposal{ - ActionNoop: &NoopProposal{}, - ActionHeartbeat: &HeartbeatProposal{}, - ActionDepositSweep: &DepositSweepProposal{}, - ActionRedemption: &RedemptionProposal{}, - ActionMovingFunds: &MovingFundsProposal{}, - ActionMovedFundsSweep: &MovedFundsSweepProposal{}, + ActionNoop: &NoopProposal{}, + ActionHeartbeat: &HeartbeatProposal{}, + ActionDepositSweep: &DepositSweepProposal{}, + ActionRedemption: &RedemptionProposal{}, + ActionMovingFunds: &MovingFundsProposal{}, + ActionMovedFundsSweep: &MovedFundsSweepProposal{}, + ActionReservationAnchor: &ReservationAnchorProposal{}, + ActionReservationReanchor: &ReservationReanchorProposal{}, }[parsedActionType] if !ok { return nil, fmt.Errorf( @@ -490,3 +492,95 @@ func validateMemberIndex(protoIndex uint32) error { } return nil } + +func (rap *ReservationAnchorProposal) Marshal() ([]byte, error) { + return proto.Marshal( + &pb.ReservationAnchorProposal{ + DepositFundingTxHash: rap.DepositFundingTxHash[:], + DepositFundingOutputIndex: rap.DepositFundingOutputIndex, + RequestNonce: rap.RequestNonce, + AnchorTxFee: rap.AnchorTxFee.Bytes(), + }) +} + +func (rap *ReservationAnchorProposal) Unmarshal(data []byte) error { + pbMsg := pb.ReservationAnchorProposal{} + if err := proto.Unmarshal(data, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal ReservationAnchorProposal: [%v]", err) + } + + if len(pbMsg.AnchorTxFee) == 0 { + return fmt.Errorf("anchor transaction fee is required") + } + if len(pbMsg.AnchorTxFee) > 8 { + return fmt.Errorf( + "invalid anchor transaction fee byte length: [%v]", + len(pbMsg.AnchorTxFee), + ) + } + if pbMsg.RequestNonce == 0 { + return fmt.Errorf("request nonce is required") + } + if len(pbMsg.DepositFundingTxHash) != 32 { + return fmt.Errorf( + "invalid deposit funding tx hash length: [%v]", + len(pbMsg.DepositFundingTxHash), + ) + } + + copy(rap.DepositFundingTxHash[:], pbMsg.DepositFundingTxHash) + rap.DepositFundingOutputIndex = pbMsg.DepositFundingOutputIndex + rap.RequestNonce = pbMsg.RequestNonce + rap.AnchorTxFee = new(big.Int).SetBytes(pbMsg.AnchorTxFee) + + return nil +} + +func (rrp *ReservationReanchorProposal) Marshal() ([]byte, error) { + return proto.Marshal( + &pb.ReservationReanchorProposal{ + ReservationKey: rrp.ReservationKey.Bytes(), + RequestNonce: rrp.RequestNonce, + TargetWalletPublicKeyHash: append([]byte{}, rrp.TargetWalletPublicKeyHash[:]...), + ReanchorTxFee: rrp.ReanchorTxFee.Bytes(), + }) +} + +func (rrp *ReservationReanchorProposal) Unmarshal(data []byte) error { + pbMsg := pb.ReservationReanchorProposal{} + if err := proto.Unmarshal(data, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal ReservationReanchorProposal: [%v]", err) + } + + if len(pbMsg.ReservationKey) == 0 { + return fmt.Errorf("reservation key is required") + } + if pbMsg.RequestNonce == 0 { + return fmt.Errorf("request nonce is required") + } + if len(pbMsg.ReanchorTxFee) == 0 { + return fmt.Errorf("re-anchor transaction fee is required") + } + if len(pbMsg.ReanchorTxFee) > 8 { + return fmt.Errorf( + "invalid re-anchor transaction fee byte length: [%v]", + len(pbMsg.ReanchorTxFee), + ) + } + if len(pbMsg.TargetWalletPublicKeyHash) != 20 { + return fmt.Errorf( + "invalid target wallet public key hash length: [%v]", + len(pbMsg.TargetWalletPublicKeyHash), + ) + } + copy(rrp.TargetWalletPublicKeyHash[:], pbMsg.TargetWalletPublicKeyHash) + if rrp.TargetWalletPublicKeyHash == [20]byte{} { + return fmt.Errorf("target wallet public key hash is required") + } + + rrp.ReservationKey = new(big.Int).SetBytes(pbMsg.ReservationKey) + rrp.RequestNonce = pbMsg.RequestNonce + rrp.ReanchorTxFee = new(big.Int).SetBytes(pbMsg.ReanchorTxFee) + + return nil +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index fcf71ed5cb..a41913d2a0 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -11,6 +11,7 @@ import ( "github.com/keep-network/keep-core/pkg/clientinfo" "github.com/keep-network/keep-common/pkg/persistence" + "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" ) @@ -310,3 +311,20 @@ func (n *node) validateDKG( ) { n.dkgExecutor.executeDkgValidation(seed, submissionBlock, result, resultHash) } + +func (n *node) ResolveWalletMembers(walletPublicKeyHash [20]byte) ([]uint32, error) { + wallet, found := n.walletRegistry.getWalletByPublicKeyHash(walletPublicKeyHash) + if !found { + return nil, fmt.Errorf("wallet not found") + } + + operatorIDs := make([]uint32, len(wallet.signingGroupOperators)) + for i, operatorAddress := range wallet.signingGroupOperators { + operatorID, err := n.chain.GetOperatorID(operatorAddress) + if err != nil { + return nil, err + } + operatorIDs[i] = uint32(operatorID) + } + return operatorIDs, nil +} diff --git a/pkg/tbtc/node_coordination.go b/pkg/tbtc/node_coordination.go index 7c61587b79..fa1bdb2f27 100644 --- a/pkg/tbtc/node_coordination.go +++ b/pkg/tbtc/node_coordination.go @@ -309,6 +309,24 @@ func processCoordinationResult(node *node, result *coordinationResult) { expiryBlock, ) } + case ActionReservationAnchor: + if proposal, ok := result.proposal.(*ReservationAnchorProposal); ok { + node.handleReservationAnchorProposal( + result.wallet, + proposal, + startBlock, + expiryBlock, + ) + } + case ActionReservationReanchor: + if proposal, ok := result.proposal.(*ReservationReanchorProposal); ok { + node.handleReservationReanchorProposal( + result.wallet, + proposal, + startBlock, + expiryBlock, + ) + } default: logger.Errorf("no handler for coordination result [%s]", result) } diff --git a/pkg/tbtc/node_proposals.go b/pkg/tbtc/node_proposals.go index ede89206eb..3d11fec099 100644 --- a/pkg/tbtc/node_proposals.go +++ b/pkg/tbtc/node_proposals.go @@ -312,6 +312,136 @@ func (n *node) handleMovingFundsProposal( walletActionLogger.Infof("wallet action dispatched successfully") } +// handleReservationAnchorProposal handles an incoming reservation anchor proposal by +// orchestrating and dispatching an appropriate wallet action. +func (n *node) handleReservationAnchorProposal( + wallet wallet, + proposal *ReservationAnchorProposal, + startBlock uint64, + expiryBlock uint64, +) { + walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) + if err != nil { + logger.Errorf("cannot marshal wallet public key: [%v]", err) + return + } + + signingExecutor, ok, err := n.getSigningExecutor(wallet.publicKey) + if err != nil { + logger.Errorf("cannot get signing executor: [%v]", err) + return + } + if !ok { + logger.Infof( + "node does not control signers of wallet PKH [0x%x]; "+ + "ignoring the received reservation anchor proposal", + walletPublicKeyBytes, + ) + return + } + + logger.Infof( + "starting orchestration of the reservation anchor action for wallet [0x%x]; "+ + "20-byte public key hash of that wallet is [0x%x]", + walletPublicKeyBytes, + bitcoin.PublicKeyHash(wallet.publicKey), + ) + + walletActionLogger := logger.With( + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + zap.String("action", ActionReservationAnchor.String()), + zap.Uint64("startBlock", startBlock), + zap.Uint64("expiryBlock", expiryBlock), + ) + walletActionLogger.Infof("dispatching wallet action") + + action := newReservationAnchorAction( + walletActionLogger, + n.chain, + n.btcChain, + wallet, + signingExecutor, + proposal, + startBlock, + expiryBlock, + n.waitForBlockHeight, + n.transactionMonitor, + ) + + err = n.walletDispatcher.dispatch(action) + if err != nil { + walletActionLogger.Errorf("cannot dispatch wallet action: [%v]", err) + return + } + + walletActionLogger.Infof("wallet action dispatched successfully") +} + +// handleReservationReanchorProposal handles an incoming reservation re-anchor proposal by +// orchestrating and dispatching an appropriate wallet action. +func (n *node) handleReservationReanchorProposal( + wallet wallet, + proposal *ReservationReanchorProposal, + startBlock uint64, + expiryBlock uint64, +) { + walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) + if err != nil { + logger.Errorf("cannot marshal wallet public key: [%v]", err) + return + } + + signingExecutor, ok, err := n.getSigningExecutor(wallet.publicKey) + if err != nil { + logger.Errorf("cannot get signing executor: [%v]", err) + return + } + if !ok { + logger.Infof( + "node does not control signers of wallet PKH [0x%x]; "+ + "ignoring the received reservation re-anchor proposal", + walletPublicKeyBytes, + ) + return + } + + logger.Infof( + "starting orchestration of the reservation re-anchor action for wallet [0x%x]; "+ + "20-byte public key hash of that wallet is [0x%x]", + walletPublicKeyBytes, + bitcoin.PublicKeyHash(wallet.publicKey), + ) + + walletActionLogger := logger.With( + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + zap.String("action", ActionReservationReanchor.String()), + zap.Uint64("startBlock", startBlock), + zap.Uint64("expiryBlock", expiryBlock), + ) + walletActionLogger.Infof("dispatching wallet action") + + action := newReservationReanchorAction( + walletActionLogger, + n.chain, + n.btcChain, + wallet, + signingExecutor, + proposal, + startBlock, + expiryBlock, + n.waitForBlockHeight, + n.transactionMonitor, + ) + + err = n.walletDispatcher.dispatch(action) + if err != nil { + walletActionLogger.Errorf("cannot dispatch wallet action: [%v]", err) + return + } + + walletActionLogger.Infof("wallet action dispatched successfully") +} + // handleMovedFundsSweepProposal handles an incoming moved funds sweep proposal // by orchestrating and dispatching an appropriate wallet action. func (n *node) handleMovedFundsSweepProposal( diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index a756c69595..2a6820fb16 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -1029,6 +1029,71 @@ func TestProcessCoordinationResult_MovedFundsSweepRoutesToHandler(t *testing.T) } } +// TestProcessCoordinationResult_ReservationAnchorRoutesToHandler verifies that +func TestProcessCoordinationResult_ReservationAnchorRoutesToHandler(t *testing.T) { + n, signer := setupNodeForHandlerTests(t) + walletKey := walletKeyFor(t, signer) + + // Mark the wallet busy so dispatch is rejected before execute() runs. + func() { + n.walletDispatcher.actionsMutex.Lock() + defer n.walletDispatcher.actionsMutex.Unlock() + n.walletDispatcher.actions[walletKey] = ActionNoop + }() + + result := &coordinationResult{ + wallet: signer.wallet, + proposal: &ReservationAnchorProposal{}, + window: &coordinationWindow{coordinationBlock: 1}, + } + + processCoordinationResult(n, result) + + // Busy sentinel must still be there: dispatch was attempted (routing worked) + // but returned errWalletBusy without touching the map entry. + _, ok := func() (WalletActionType, bool) { + n.walletDispatcher.actionsMutex.Lock() + defer n.walletDispatcher.actionsMutex.Unlock() + v, exists := n.walletDispatcher.actions[walletKey] + return v, exists + }() + if !ok { + t.Error("expected walletDispatcher to retain the busy sentinel after ReservationAnchor routing") + } +} + +func TestProcessCoordinationResult_ReservationReanchorRoutesToHandler(t *testing.T) { + n, signer := setupNodeForHandlerTests(t) + walletKey := walletKeyFor(t, signer) + + // Mark the wallet busy so dispatch is rejected before execute() runs. + func() { + n.walletDispatcher.actionsMutex.Lock() + defer n.walletDispatcher.actionsMutex.Unlock() + n.walletDispatcher.actions[walletKey] = ActionNoop + }() + + result := &coordinationResult{ + wallet: signer.wallet, + proposal: &ReservationReanchorProposal{}, + window: &coordinationWindow{coordinationBlock: 1}, + } + + processCoordinationResult(n, result) + + // Busy sentinel must still be there: dispatch was attempted (routing worked) + // but returned errWalletBusy without touching the map entry. + _, ok := func() (WalletActionType, bool) { + n.walletDispatcher.actionsMutex.Lock() + defer n.walletDispatcher.actionsMutex.Unlock() + v, exists := n.walletDispatcher.actions[walletKey] + return v, exists + }() + if !ok { + t.Error("expected walletDispatcher to retain the busy sentinel after ReservationReanchor routing") + } +} + // setupNodeForClosureTests creates a node backed by a fast-block localChain // (1 ms per block) so that WaitForBlockConfirmations (32 blocks) completes in // ~32 ms instead of seconds. diff --git a/pkg/tbtc/reservation.go b/pkg/tbtc/reservation.go new file mode 100644 index 0000000000..f856864a0b --- /dev/null +++ b/pkg/tbtc/reservation.go @@ -0,0 +1,637 @@ +package tbtc + +import ( + "fmt" + "math/big" + "time" + + "go.uber.org/zap" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" +) + +const ( + reservationLookBackBlocks = uint64(216000) + + // reservationAnchorProposalValidityBlocks determines the reservation + // anchor proposal validity time expressed in blocks. + reservationAnchorProposalValidityBlocks = 600 + + // reservationReanchorProposalValidityBlocks determines the reservation + // re-anchor proposal validity time expressed in blocks. + reservationReanchorProposalValidityBlocks = 600 +) + +// ReservationState represents the state of an on-chain UTXO reservation. +type ReservationState uint8 + +const ( + // ReservationStateUnknown means the reservation is unknown to the Bridge. + ReservationStateUnknown ReservationState = iota + // ReservationStateActive means the reservation's anchor outpoint is under + // wallet custody with no action in flight. + ReservationStateActive + // ReservationStateActionPending means a redemption, re-anchor, or + // dissolution action is pending. The action details are held in the + // nonce-keyed ReservationAction record. + ReservationStateActionPending + // ReservationStateClosed means the reservation was closed by an in-kind + // redemption, dissolution, or late settlement. + ReservationStateClosed + // ReservationStateStranded means the custodying wallet was terminated + // while the anchor was outstanding and the anchor is no longer tracked. + ReservationStateStranded +) + +// Reservation represents an on-chain UTXO reservation record. A reservation +// is a deposit that was anchored by the wallet - spent in a 1-input-1-output +// transaction into a fresh wallet-controlled output with no refund path - +// instead of being swept into the wallet main UTXO. The anchor outpoint is +// custodied without ever commingling with the pooled supply and is +// redeemable in-kind by the reservation owner. +type Reservation struct { + // Owner is the reservation owner's address on the host chain. + Owner chain.Address + // MintedAmount is the gross amount in satoshi credited to the owner at + // acceptance time. + MintedAmount uint64 + // AcceptedAt is the UNIX timestamp the reservation was accepted at. + AcceptedAt uint32 + // WalletPublicKeyHash is the 20-byte public key hash of the wallet + // custodying the current anchor outpoint. + WalletPublicKeyHash [20]byte + // AnchorUtxo is the reservation's current anchor outpoint, i.e. the + // wallet-controlled output holding the reserved coins. + AnchorUtxo *bitcoin.UnspentTransactionOutput + // ExpiresAt is the UNIX timestamp the custody term expires at. + ExpiresAt uint32 + // State is the current state of the reservation. + State ReservationState + // RequestNonce is the current monotonic reservation action generation. + RequestNonce uint64 + // RetryCredit indicates the owner has a single-use fee-free redemption + // retry entitlement after a fee-paid redemption timed out. + RetryCredit bool + // DissolutionEligibleAt is the UNIX timestamp at which the current term + // becomes eligible for dissolution. + DissolutionEligibleAt uint32 +} + +// ReservationActionType represents the type of a reservation action +// generation. +type ReservationActionType uint8 + +const ( + ReservationActionTypeNone ReservationActionType = iota + ReservationActionTypeAcceptance + ReservationActionTypeRedemption + ReservationActionTypeReanchor + ReservationActionTypeDissolution +) + +// ReservationActionState represents the settlement state of a reservation +// action generation. +type ReservationActionState uint8 + +const ( + ReservationActionStateUnknown ReservationActionState = iota + ReservationActionStatePending + ReservationActionStateSettled + ReservationActionStateTimedOut + ReservationActionStateVetoed + ReservationActionStateSuperseded +) + +// ReservationAction represents one nonce-bound generation of a reservation +// action. All authorization data used to construct and settle the action is +// snapshotted when the generation is requested. +type ReservationAction struct { + // TargetWalletPublicKeyHash is the wallet an acceptance, re-anchor, or + // dissolution output must pay to. It is zero for redemptions. + TargetWalletPublicKeyHash [20]byte + // RequestedAt is the UNIX timestamp the action was requested at. + RequestedAt uint32 + // TimeoutAt is the UNIX timestamp after which the action may time out. + TimeoutAt uint32 + // TxMaxFee is the snapshotted maximum Bitcoin transaction fee in satoshi. + TxMaxFee uint64 + // ActionType is the type of this action generation. + ActionType ReservationActionType + // State is the settlement state of this action generation. + State ReservationActionState + // FeePaid indicates the generation was created through a fee-paying vault + // entry point. + FeePaid bool + // Redeemer is the address that can reclaim escrow after a redemption + // timeout. It is empty for other action types. + Redeemer chain.Address + // Amount is the satoshi amount associated with the action generation. + Amount uint64 + // RedeemerOutputScriptHash is the keccak256 hash of the length-prefixed + // output script authorized for a redemption. + RedeemerOutputScriptHash [32]byte + // ExpectedMainUtxoHash identifies the wallet main UTXO snapshotted for a + // dissolution. It is zero for other action types and no-main-UTXO wallets. + ExpectedMainUtxoHash [32]byte + // IsPartial indicates a redemption spends only Amount and must re-anchor + // the remaining reservation value back to the custodying wallet. + IsPartial bool +} + +// ReservationParameters represents the on-chain values of the Bridge +// reservation parameters. +type ReservationParameters struct { + // ReservationVault is the address of the reservation vault. Deposits + // revealed with this vault address are treated as UTXO reservations. + ReservationVault chain.Address + // ReservationMinAmount is the minimal anchor output amount in satoshi + // accepted for a reservation. + ReservationMinAmount uint64 + // ReservationTxMaxFee is the maximum transaction fee in satoshi for a + // single reservation lifecycle transaction. + ReservationTxMaxFee uint64 + // ReservationTermSeconds is the custody term length in seconds. + ReservationTermSeconds uint32 + // ReservationDissolutionDelay is the delay snapshotted after term expiry + // before a reservation becomes dissolvable. + ReservationDissolutionDelay uint32 + // ReservationMaxTotalAmount is the maximum total amount of all active + // reservations in satoshi. + ReservationMaxTotalAmount uint64 + // ReservationTotalAmount is the current total amount of all active + // reservations in satoshi. + ReservationTotalAmount uint64 + // MaxReservationsPerWallet is the maximum number of reservations a wallet + // may custody. + MaxReservationsPerWallet uint32 + // ReservationActionTimeout is the timeout for reservation actions in + // seconds. + ReservationActionTimeout uint32 + // ReservationRenewalWindowSeconds is the period before expiry during which + // a reservation can be renewed. + ReservationRenewalWindowSeconds uint32 +} + +// ReservationAnchorProposal represents a reservation anchor proposal issued +// by a wallet's coordination leader. +type ReservationAnchorProposal struct { + // DepositFundingTxHash is the funding transaction hash of the reserved + // deposit to anchor. + DepositFundingTxHash bitcoin.Hash + // DepositFundingOutputIndex is the funding output index of the reserved + // deposit to anchor. + DepositFundingOutputIndex uint32 + // RequestNonce is the acceptance authorization generation being executed. + RequestNonce uint64 + // AnchorTxFee is the proposed BTC fee for the anchor transaction. + AnchorTxFee *big.Int +} + +// ActionType returns the specific type of the walletAction being subject +// of this proposal. +func (rap *ReservationAnchorProposal) ActionType() WalletActionType { + return ActionReservationAnchor +} + +// ValidityBlocks returns the number of blocks for which the proposal is valid. +func (rap *ReservationAnchorProposal) ValidityBlocks() uint64 { + return reservationAnchorProposalValidityBlocks +} + +// ReservationReanchorProposal represents a reservation re-anchor proposal +// issued by a wallet's coordination leader, moving a reservation's anchor +// outpoint to another wallet (e.g. during wallet migration). +type ReservationReanchorProposal struct { + // ReservationKey is the key of the reservation to re-anchor. + ReservationKey *big.Int + // RequestNonce is the re-anchor authorization generation being executed. + RequestNonce uint64 + // TargetWalletPublicKeyHash is the 20-byte public key hash of the wallet + // receiving the anchor. + TargetWalletPublicKeyHash [20]byte + // ReanchorTxFee is the proposed BTC fee for the re-anchor transaction. + ReanchorTxFee *big.Int +} + +// ActionType returns the specific type of the walletAction being subject +// of this proposal. +func (rrp *ReservationReanchorProposal) ActionType() WalletActionType { + return ActionReservationReanchor +} + +// ValidityBlocks returns the number of blocks for which the proposal is valid. +func (rrp *ReservationReanchorProposal) ValidityBlocks() uint64 { + return reservationReanchorProposalValidityBlocks +} + +// Marshal/Unmarshal for ReservationReanchorProposal live in marshaling.go, +// alongside every other coordination proposal type's wire-format methods. + +// AssembleReservationAnchorTransaction constructs an unsigned reservation +// anchor transaction: a 1-input-1-output spend of the given reserved deposit +// into a fresh output controlled by the given wallet. The anchor mirrors the +// sweep's refund-disabling role without its consolidating role: the Bridge +// credits the reservation owner only against the SPV proof of this +// transaction. +func AssembleReservationAnchorTransaction( + bitcoinChain bitcoin.Chain, + deposit *Deposit, + walletPublicKeyHash [20]byte, + action *ReservationAction, + fee int64, +) (*bitcoin.TransactionBuilder, error) { + if deposit == nil { + return nil, fmt.Errorf("deposit is required") + } + if action == nil { + return nil, fmt.Errorf("reservation action is required") + } + if fee <= 0 { + return nil, fmt.Errorf("fee must be positive") + } + if uint64(fee) > action.TxMaxFee { + return nil, fmt.Errorf("fee exceeds the maximum allowed fee") + } + anchorValue := deposit.Utxo.Value - fee + if anchorValue <= 0 { + return nil, fmt.Errorf("transaction fee exceeds the deposit amount") + } + + builder := bitcoin.NewTransactionBuilder(bitcoinChain) + + depositScript, err := deposit.Script() + if err != nil { + return nil, fmt.Errorf("cannot get deposit script: [%v]", err) + } + + err = builder.AddScriptHashInput(deposit.Utxo, depositScript) + if err != nil { + return nil, fmt.Errorf("cannot add input pointing to deposit UTXO: [%v]", err) + } + + outputScript, err := bitcoin.PayToWitnessPublicKeyHash( + walletPublicKeyHash, + ) + if err != nil { + return nil, fmt.Errorf("cannot compute anchor script: [%v]", err) + } + + builder.AddOutput(&bitcoin.TransactionOutput{ + Value: anchorValue, + PublicKeyScript: outputScript, + }) + + return builder, nil +} + +// AssembleReservationReanchorTransaction constructs an unsigned reservation +// re-anchor transaction: a 1-input-1-output spend of the reservation's +// anchor outpoint into a fresh output controlled by the target wallet. Used +// during wallet migration so reservations never pin retiring wallets. +func AssembleReservationReanchorTransaction( + bitcoinChain bitcoin.Chain, + anchorUtxo *bitcoin.UnspentTransactionOutput, + targetWalletPublicKeyHash [20]byte, + action *ReservationAction, + fee int64, +) (*bitcoin.TransactionBuilder, error) { + if anchorUtxo == nil { + return nil, fmt.Errorf("anchor UTXO is required") + } + if action == nil { + return nil, fmt.Errorf("reservation action is required") + } + if fee <= 0 { + return nil, fmt.Errorf("fee must be positive") + } + if uint64(fee) > action.TxMaxFee { + return nil, fmt.Errorf("fee exceeds the maximum allowed fee") + } + + anchorValue := anchorUtxo.Value - fee + if anchorValue <= 0 { + return nil, fmt.Errorf("transaction fee exceeds the anchor value") + } + + builder := bitcoin.NewTransactionBuilder(bitcoinChain) + + err := builder.AddPublicKeyHashInput(anchorUtxo) + if err != nil { + return nil, fmt.Errorf( + "cannot add input pointing to anchor UTXO: [%v]", + err, + ) + } + + outputScript, err := bitcoin.PayToWitnessPublicKeyHash( + targetWalletPublicKeyHash, + ) + if err != nil { + return nil, fmt.Errorf("cannot compute anchor script: [%v]", err) + } + + builder.AddOutput(&bitcoin.TransactionOutput{ + Value: anchorValue, + PublicKeyScript: outputScript, + }) + + return builder, nil +} + +// reservationActionSigningTimeoutSafetyMarginBlocks is the duration, in +// blocks, that must remain before the proposal's expiry block for signing +// to be attempted. Mirrors redemptionSigningTimeoutSafetyMarginBlocks. +const reservationActionSigningTimeoutSafetyMarginBlocks = 300 + +// reservationActionBroadcastTimeout is the timeout applied while +// broadcasting a reservation anchor/re-anchor transaction. Mirrors +// redemptionBroadcastTimeout. +const reservationActionBroadcastTimeout = 15 * time.Minute + +// reservationActionBroadcastCheckDelay is the delay between broadcast +// attempts of a reservation anchor/re-anchor transaction. Mirrors +// redemptionBroadcastCheckDelay. +const reservationActionBroadcastCheckDelay = 1 * time.Minute + +// reservationAnchorAction is a walletAction implementation handling reservation +// anchor requests from the wallet coordinator. +type reservationAnchorAction struct { + logger *zap.SugaredLogger + chain Chain + btcChain bitcoin.Chain + custodyWallet wallet + transactionExecutor *walletTransactionExecutor + proposal *ReservationAnchorProposal + startBlock uint64 + expiryBlock uint64 +} + +func newReservationAnchorAction( + logger *zap.SugaredLogger, + chain Chain, + btcChain bitcoin.Chain, + custodyWallet wallet, + signingExecutor walletSigningExecutor, + proposal *ReservationAnchorProposal, + startBlock uint64, + expiryBlock uint64, + waitForBlockHeight waitForBlockFn, + transactionMonitor *transactionMonitor, +) *reservationAnchorAction { + transactionExecutor := newWalletTransactionExecutor( + btcChain, + custodyWallet, + signingExecutor, + waitForBlockHeight, + ) + transactionExecutor.setTransactionMonitor(transactionMonitor) + return &reservationAnchorAction{ + logger: logger, + chain: chain, + btcChain: btcChain, + custodyWallet: custodyWallet, + transactionExecutor: transactionExecutor, + proposal: proposal, + startBlock: startBlock, + expiryBlock: expiryBlock, + } +} + +func (raa *reservationAnchorAction) execute() error { + walletPublicKeyHash := bitcoin.PublicKeyHash(raa.custodyWallet.publicKey) + + fundingTx, err := raa.btcChain.GetTransaction(raa.proposal.DepositFundingTxHash) + if err != nil { + return fmt.Errorf("cannot fetch funding transaction: [%v]", err) + } + + // The proposal carries only the deposit's funding outpoint, not the + // block it was revealed at, so the DepositRevealed event lookup cannot + // be block-range narrowed the way the deposit sweep validation path + // narrows it via DepositsRevealBlocks. Narrow by wallet PKH instead and + // match the exact funding outpoint among the returned events. + events, err := raa.chain.PastDepositRevealedEvents(&DepositRevealedEventFilter{ + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + StartBlock: raa.startBlock - reservationLookBackBlocks, + }) + if err != nil { + return fmt.Errorf("cannot fetch deposit revealed events: [%v]", err) + } + + var matchingEvent *DepositRevealedEvent + for _, event := range events { + if event.FundingTxHash == raa.proposal.DepositFundingTxHash && + event.FundingOutputIndex == raa.proposal.DepositFundingOutputIndex { + matchingEvent = event + break + } + } + if matchingEvent == nil { + return fmt.Errorf("no matching DepositRevealed event for deposit") + } + + depositRequest, found, err := raa.chain.GetDepositRequest( + raa.proposal.DepositFundingTxHash, + raa.proposal.DepositFundingOutputIndex, + ) + if err != nil { + return fmt.Errorf("cannot fetch deposit request: [%v]", err) + } + if !found { + return fmt.Errorf("deposit request not found") + } + + deposit := matchingEvent.unpack(depositRequest.ExtraData) + + // m1 identity: the reservation key is the deposit key, mirroring the + // convention documented in pkg/maintainer/spv/reservation_stale_deposit_watch.go. + reservationKey := raa.chain.BuildDepositKey( + raa.proposal.DepositFundingTxHash, + raa.proposal.DepositFundingOutputIndex, + ) + + action, err := raa.chain.GetReservationAction(reservationKey, raa.proposal.RequestNonce) + if err != nil { + return fmt.Errorf("cannot get reservation action: [%v]", err) + } + if action.ActionType != ReservationActionTypeAcceptance || action.State != ReservationActionStatePending { + return fmt.Errorf("reservation action is not a pending acceptance") + } + + err = raa.chain.ValidateReservationAnchorProposal( + walletPublicKeyHash, + raa.proposal, + struct { + *Deposit + FundingTx *bitcoin.Transaction + }{Deposit: deposit, FundingTx: fundingTx}, + ) + if err != nil { + return fmt.Errorf("cannot validate reservation anchor proposal: [%v]", err) + } + + unsignedTx, err := AssembleReservationAnchorTransaction( + raa.btcChain, + deposit, + walletPublicKeyHash, + action, + raa.proposal.AnchorTxFee.Int64(), + ) + if err != nil { + return fmt.Errorf("cannot assemble reservation anchor transaction: [%v]", err) + } + + // Prevent unsigned underflow in signing deadline calculation. + if raa.expiryBlock < reservationActionSigningTimeoutSafetyMarginBlocks { + return fmt.Errorf("invalid proposal expiry block") + } + + signedTx, err := raa.transactionExecutor.signTransaction( + raa.logger, + unsignedTx, + raa.startBlock, + raa.expiryBlock-reservationActionSigningTimeoutSafetyMarginBlocks, + ) + if err != nil { + return fmt.Errorf("cannot sign reservation anchor transaction: [%v]", err) + } + + err = raa.transactionExecutor.broadcastTransaction( + raa.logger, + signedTx, + reservationActionBroadcastTimeout, + reservationActionBroadcastCheckDelay, + ) + if err != nil { + return fmt.Errorf("cannot broadcast reservation anchor transaction: [%v]", err) + } + + return nil +} + +func (raa *reservationAnchorAction) wallet() wallet { + return raa.custodyWallet +} + +func (raa *reservationAnchorAction) actionType() WalletActionType { + return ActionReservationAnchor +} + +// reservationReanchorAction is a walletAction implementation handling +// reservation re-anchor requests from the wallet coordinator. +type reservationReanchorAction struct { + logger *zap.SugaredLogger + chain Chain + btcChain bitcoin.Chain + custodyWallet wallet + transactionExecutor *walletTransactionExecutor + proposal *ReservationReanchorProposal + startBlock uint64 + expiryBlock uint64 +} + +func newReservationReanchorAction( + logger *zap.SugaredLogger, + chain Chain, + btcChain bitcoin.Chain, + custodyWallet wallet, + signingExecutor walletSigningExecutor, + proposal *ReservationReanchorProposal, + startBlock uint64, + expiryBlock uint64, + waitForBlockHeight waitForBlockFn, + transactionMonitor *transactionMonitor, +) *reservationReanchorAction { + transactionExecutor := newWalletTransactionExecutor( + btcChain, + custodyWallet, + signingExecutor, + waitForBlockHeight, + ) + transactionExecutor.setTransactionMonitor(transactionMonitor) + return &reservationReanchorAction{ + logger: logger, + chain: chain, + btcChain: btcChain, + custodyWallet: custodyWallet, + transactionExecutor: transactionExecutor, + proposal: proposal, + startBlock: startBlock, + expiryBlock: expiryBlock, + } +} + +func (rra *reservationReanchorAction) execute() error { + walletPublicKeyHash := bitcoin.PublicKeyHash(rra.custodyWallet.publicKey) + + reservation, err := rra.chain.GetReservation(rra.proposal.ReservationKey) + if err != nil { + return fmt.Errorf("cannot get reservation: [%v]", err) + } + + action, err := rra.chain.GetReservationAction(rra.proposal.ReservationKey, rra.proposal.RequestNonce) + if err != nil { + return fmt.Errorf("cannot get reservation action: [%v]", err) + } + if action.ActionType != ReservationActionTypeReanchor || action.State != ReservationActionStatePending { + return fmt.Errorf("reservation action is not a pending reanchor") + } + if action.TargetWalletPublicKeyHash != rra.proposal.TargetWalletPublicKeyHash { + return fmt.Errorf("reservation action targets a different wallet") + } + + err = rra.chain.ValidateReservationReanchorProposal( + walletPublicKeyHash, + rra.proposal, + ) + if err != nil { + return fmt.Errorf("cannot validate reservation reanchor proposal: [%v]", err) + } + + unsignedTx, err := AssembleReservationReanchorTransaction( + rra.btcChain, + reservation.AnchorUtxo, + rra.proposal.TargetWalletPublicKeyHash, + action, + rra.proposal.ReanchorTxFee.Int64(), + ) + if err != nil { + return fmt.Errorf("cannot assemble reservation reanchor transaction: [%v]", err) + } + + // Prevent unsigned underflow in signing deadline calculation. + if rra.expiryBlock < reservationActionSigningTimeoutSafetyMarginBlocks { + return fmt.Errorf("invalid proposal expiry block") + } + + signedTx, err := rra.transactionExecutor.signTransaction( + rra.logger, + unsignedTx, + rra.startBlock, + rra.expiryBlock-reservationActionSigningTimeoutSafetyMarginBlocks, + ) + if err != nil { + return fmt.Errorf("cannot sign reservation reanchor transaction: [%v]", err) + } + + err = rra.transactionExecutor.broadcastTransaction( + rra.logger, + signedTx, + reservationActionBroadcastTimeout, + reservationActionBroadcastCheckDelay, + ) + if err != nil { + return fmt.Errorf("cannot broadcast reservation reanchor transaction: [%v]", err) + } + + return nil +} + +func (rra *reservationReanchorAction) wallet() wallet { + return rra.custodyWallet +} + +func (rra *reservationReanchorAction) actionType() WalletActionType { + return ActionReservationReanchor +} diff --git a/pkg/tbtc/reservation_test.go b/pkg/tbtc/reservation_test.go new file mode 100644 index 0000000000..899d9a144c --- /dev/null +++ b/pkg/tbtc/reservation_test.go @@ -0,0 +1,696 @@ +package tbtc + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "math/big" + "reflect" + "testing" + "time" + + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc/gen/pb" +) + +func TestReservationActionTypes(t *testing.T) { + for value, expected := range map[uint8]WalletActionType{ + 6: ActionReservationAnchor, + 8: ActionReservationReanchor, + } { + parsed, err := ParseWalletActionType(value) + if err != nil { + t.Fatal(err) + } + if parsed != expected { + t.Errorf( + "unexpected action type for [%v]: expected [%v] got [%v]", + value, + expected, + parsed, + ) + } + } +} + +func TestReservationStateValues(t *testing.T) { + tests := map[ReservationState]uint8{ + ReservationStateUnknown: 0, + ReservationStateActive: 1, + ReservationStateActionPending: 2, + ReservationStateClosed: 3, + ReservationStateStranded: 4, + } + + for state, expected := range tests { + if actual := uint8(state); actual != expected { + t.Errorf( + "unexpected reservation state value\nexpected: [%v]\nactual: [%v]", + expected, + actual, + ) + } + } +} + +func TestReservationActionTypeValues(t *testing.T) { + tests := map[ReservationActionType]uint8{ + ReservationActionTypeNone: 0, + ReservationActionTypeAcceptance: 1, + ReservationActionTypeRedemption: 2, + ReservationActionTypeReanchor: 3, + ReservationActionTypeDissolution: 4, + } + + for actionType, expected := range tests { + if actual := uint8(actionType); actual != expected { + t.Errorf( + "unexpected reservation action type value\nexpected: [%v]\nactual: [%v]", + expected, + actual, + ) + } + } +} + +func TestReservationActionStateValues(t *testing.T) { + tests := map[ReservationActionState]uint8{ + ReservationActionStateUnknown: 0, + ReservationActionStatePending: 1, + ReservationActionStateSettled: 2, + ReservationActionStateTimedOut: 3, + ReservationActionStateVetoed: 4, + ReservationActionStateSuperseded: 5, + } + + for state, expected := range tests { + if actual := uint8(state); actual != expected { + t.Errorf( + "unexpected reservation action state value\nexpected: [%v]\nactual: [%v]", + expected, + actual, + ) + } + } +} + +func TestReservationProposals_MarshalingRoundtrip(t *testing.T) { + anchorProposal := &ReservationAnchorProposal{ + DepositFundingTxHash: bitcoin.Hash{0x01, 0x02}, + DepositFundingOutputIndex: 3, + RequestNonce: 1, + AnchorTxFee: big.NewInt(1500), + } + + reanchorProposal := &ReservationReanchorProposal{ + ReservationKey: big.NewInt(54321), + RequestNonce: 3, + TargetWalletPublicKeyHash: [20]byte{0xaa, 0xbb}, + ReanchorTxFee: big.NewInt(1700), + } + + roundtrip := func( + proposal CoordinationProposal, + fresh CoordinationProposal, + ) { + marshaled, err := proposal.Marshal() + if err != nil { + t.Fatal(err) + } + if err := fresh.Unmarshal(marshaled); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(proposal, fresh) { + t.Errorf( + "unexpected unmarshaled proposal: expected [%+v] got [%+v]", + proposal, + fresh, + ) + } + } + + roundtrip(anchorProposal, &ReservationAnchorProposal{}) + roundtrip(reanchorProposal, &ReservationReanchorProposal{}) +} + +func TestReservationProposals_UnmarshalRejectsMissingIntegers(t *testing.T) { + tests := map[string]struct { + actionType WalletActionType + payload []byte + expectedError string + }{ + "anchor empty object": { + actionType: ActionReservationAnchor, + payload: marshalPb(t, &pb.ReservationAnchorProposal{}), + expectedError: "cannot unmarshal proposal payload: [anchor transaction fee is required]", + }, + "anchor null payload": { + actionType: ActionReservationAnchor, + payload: nil, + expectedError: "cannot unmarshal proposal payload: [anchor transaction fee is required]", + }, + "anchor missing nonce": { + actionType: ActionReservationAnchor, + payload: marshalPb(t, &pb.ReservationAnchorProposal{ + AnchorTxFee: big.NewInt(1500).Bytes(), + }), + expectedError: "cannot unmarshal proposal payload: [request nonce is required]", + }, + "re-anchor null payload": { + actionType: ActionReservationReanchor, + payload: nil, + expectedError: "cannot unmarshal proposal payload: [reservation key is required]", + }, + "re-anchor missing nonce": { + actionType: ActionReservationReanchor, + payload: marshalPb(t, &pb.ReservationReanchorProposal{ + ReservationKey: big.NewInt(54321).Bytes(), + ReanchorTxFee: big.NewInt(1700).Bytes(), + }), + expectedError: "cannot unmarshal proposal payload: [request nonce is required]", + }, + "re-anchor missing fee": { + actionType: ActionReservationReanchor, + payload: marshalPb(t, &pb.ReservationReanchorProposal{ + ReservationKey: big.NewInt(54321).Bytes(), + RequestNonce: 3, + }), + expectedError: "cannot unmarshal proposal payload: [re-anchor transaction fee is required]", + }, + "anchor fee exceeds 8 bytes": { + actionType: ActionReservationAnchor, + payload: marshalPb(t, &pb.ReservationAnchorProposal{ + AnchorTxFee: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + RequestNonce: 1, + DepositFundingTxHash: make([]byte, 32), + DepositFundingOutputIndex: 0, + }), + expectedError: "cannot unmarshal proposal payload: [invalid anchor transaction fee byte length: [9]]", + }, + "re-anchor fee exceeds 8 bytes": { + actionType: ActionReservationReanchor, + payload: marshalPb(t, &pb.ReservationReanchorProposal{ + ReservationKey: big.NewInt(54321).Bytes(), + RequestNonce: 3, + ReanchorTxFee: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + TargetWalletPublicKeyHash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, + }), + expectedError: "cannot unmarshal proposal payload: [invalid re-anchor transaction fee byte length: [9]]", + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + _, err := unmarshalCoordinationProposal( + uint32(test.actionType), + test.payload, + ) + if err == nil || err.Error() != test.expectedError { + t.Errorf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + test.expectedError, + err, + ) + } + }) + } +} + +// marshalPb marshals a protobuf message for use as a test fixture payload. +func marshalPb(t *testing.T, msg proto.Message) []byte { + t.Helper() + data, err := proto.Marshal(msg) + if err != nil { + t.Fatal(err) + } + return data +} + +func signReservationTransaction( + t *testing.T, + builder *bitcoin.TransactionBuilder, + publicKey *ecdsa.PublicKey, + privateKeyValue *big.Int, +) *bitcoin.Transaction { + t.Helper() + + sigHashes, err := builder.ComputeSignatureHashes() + if err != nil { + t.Fatal(err) + } + + privateKey := &ecdsa.PrivateKey{ + PublicKey: *publicKey, + D: privateKeyValue, + } + signatures := make([]*bitcoin.SignatureContainer, len(sigHashes)) + for i, sigHash := range sigHashes { + r, s, err := ecdsa.Sign(rand.Reader, privateKey, sigHash.Bytes()) + if err != nil { + t.Fatal(err) + } + signatures[i] = &bitcoin.SignatureContainer{ + R: r, + S: s, + PublicKey: publicKey, + } + } + + transaction, err := builder.AddSignatures(signatures) + if err != nil { + t.Fatal(err) + } + + return transaction +} + +func TestAssembleReservationTransactions_InputValidation(t *testing.T) { + bitcoinChain := newLocalBitcoinChain() + walletPublicKeyHash := [20]byte{0x01} + + anchorUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{0x03}, + OutputIndex: 0, + }, + Value: 100000, + } + + assertError := func(err error, expected string) { + if err == nil || err.Error() != expected { + t.Errorf("expected error [%v], got [%v]", expected, err) + } + } + + var err error + _, err = AssembleReservationAnchorTransaction( + bitcoinChain, + nil, + walletPublicKeyHash, + nil, + 1500, + ) + assertError(err, "deposit is required") + + deposit := &Deposit{Utxo: anchorUtxo} + + _, err = AssembleReservationAnchorTransaction( + bitcoinChain, + deposit, + walletPublicKeyHash, + nil, + 1500, + ) + assertError(err, "reservation action is required") + + _, err = AssembleReservationAnchorTransaction( + bitcoinChain, + deposit, + walletPublicKeyHash, + &ReservationAction{TxMaxFee: 2000}, + 0, + ) + assertError(err, "fee must be positive") + + _, err = AssembleReservationAnchorTransaction( + bitcoinChain, + deposit, + walletPublicKeyHash, + &ReservationAction{TxMaxFee: 1000}, + 1500, + ) + assertError(err, "fee exceeds the maximum allowed fee") + + _, err = AssembleReservationReanchorTransaction( + bitcoinChain, + nil, + walletPublicKeyHash, + nil, + 1500, + ) + assertError(err, "anchor UTXO is required") + + _, err = AssembleReservationReanchorTransaction( + bitcoinChain, + anchorUtxo, + walletPublicKeyHash, + nil, + 1500, + ) + assertError(err, "reservation action is required") + + _, err = AssembleReservationReanchorTransaction( + bitcoinChain, + anchorUtxo, + walletPublicKeyHash, + &ReservationAction{TxMaxFee: 2000}, + 0, + ) + assertError(err, "fee must be positive") + + _, err = AssembleReservationReanchorTransaction( + bitcoinChain, + anchorUtxo, + walletPublicKeyHash, + &ReservationAction{TxMaxFee: 1000}, + 1500, + ) + assertError(err, "fee exceeds the maximum allowed fee") +} + +func TestAssembleReservationTransactions_FeeBoundaries(t *testing.T) { + bitcoinChain := newLocalBitcoinChain() + walletPublicKeyHash := [20]byte{0x01} + + // Anchor boundary + _, err := AssembleReservationAnchorTransaction( + bitcoinChain, + &Deposit{Utxo: &bitcoin.UnspentTransactionOutput{Value: 100000}}, + walletPublicKeyHash, + &ReservationAction{TxMaxFee: 200000}, + 100000, + ) + if err == nil || err.Error() != "transaction fee exceeds the deposit amount" { + t.Errorf("expected error [transaction fee exceeds the deposit amount], got [%v]", err) + } + + // Reanchor boundary + _, err = AssembleReservationReanchorTransaction( + bitcoinChain, + &bitcoin.UnspentTransactionOutput{Value: 100000}, + walletPublicKeyHash, + &ReservationAction{TxMaxFee: 200000}, + 100000, + ) + if err == nil || err.Error() != "transaction fee exceeds the anchor value" { + t.Errorf("expected error [transaction fee exceeds the anchor value], got [%v]", err) + } +} + +// reservationTestWallet returns a wallet with a real ECDSA public key so +// bitcoin.PublicKeyHash (called at the top of both execute() methods) +// doesn't panic on a nil key. +func reservationTestWallet(t *testing.T) wallet { + t.Helper() + + publicKeyBytes, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + return wallet{publicKey: mustUnmarshalPublicKey(t, publicKeyBytes)} +} + +func TestReservationAnchorAction_Execute(t *testing.T) { + const fundingOutputIndex = 0 + + custodyWallet := reservationTestWallet(t) + walletPublicKeyHash := bitcoin.PublicKeyHash(custodyWallet.publicKey) + + newAction := func( + chain Chain, + btcChain bitcoin.Chain, + fundingTxHash bitcoin.Hash, + ) *reservationAnchorAction { + return newReservationAnchorAction( + zap.NewNop().Sugar(), + chain, + btcChain, + custodyWallet, + nil, // signing executor unreached by these negative-path cases + &ReservationAnchorProposal{ + DepositFundingTxHash: fundingTxHash, + DepositFundingOutputIndex: fundingOutputIndex, + RequestNonce: 1, + AnchorTxFee: big.NewInt(1500), + }, + 300000, + 300000+600, + nil, + nil, + ) + } + + t.Run("no matching DepositRevealed event", func(t *testing.T) { + chain := Connect() + btcChain := newLocalBitcoinChain() + + fundingTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{Value: 100000}}, + } + if err := btcChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + fundingTxHash := fundingTx.Hash() + + // A DepositRevealed event exists for this wallet, but for a + // different funding outpoint - the matching loop must walk past + // it and still report no match, not silently accept it. + if err := chain.setPastDepositRevealedEvents( + &DepositRevealedEventFilter{ + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + StartBlock: 300000 - reservationLookBackBlocks, + }, + []*DepositRevealedEvent{{ + FundingTxHash: bitcoin.Hash{0x99}, + FundingOutputIndex: 0, + WalletPublicKeyHash: walletPublicKeyHash, + }}, + ); err != nil { + t.Fatal(err) + } + + err := newAction(chain, btcChain, fundingTxHash).execute() + if err == nil || err.Error() != "no matching DepositRevealed event for deposit" { + t.Errorf( + "unexpected error\nexpected: [no matching DepositRevealed event for deposit]\nactual: [%v]", + err, + ) + } + }) + + t.Run("deposit request not found", func(t *testing.T) { + chain := Connect() + btcChain := newLocalBitcoinChain() + + fundingTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{Value: 100000}}, + } + if err := btcChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + fundingTxHash := fundingTx.Hash() + + if err := chain.setPastDepositRevealedEvents( + &DepositRevealedEventFilter{ + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + StartBlock: 300000 - reservationLookBackBlocks, + }, + []*DepositRevealedEvent{{ + FundingTxHash: fundingTxHash, + FundingOutputIndex: fundingOutputIndex, + WalletPublicKeyHash: walletPublicKeyHash, + }}, + ); err != nil { + t.Fatal(err) + } + // Deliberately no setDepositRequest call: the Bridge has no + // request record for this funding outpoint. + + err := newAction(chain, btcChain, fundingTxHash).execute() + if err == nil || err.Error() != "deposit request not found" { + t.Errorf( + "unexpected error\nexpected: [deposit request not found]\nactual: [%v]", + err, + ) + } + }) + + t.Run("full happy path up to the signing boundary", func(t *testing.T) { + chain := Connect() + btcChain := newLocalBitcoinChain() + + depositForScript := &Deposit{ + Depositor: "0x0000000000000000000000000000000000000001", + WalletPublicKeyHash: walletPublicKeyHash, + } + depositScript, err := depositForScript.Script() + if err != nil { + t.Fatal(err) + } + scriptHash := sha256.Sum256(depositScript) + fundingOutputScript, err := bitcoin.PayToWitnessScriptHash(scriptHash) + if err != nil { + t.Fatal(err) + } + + fundingTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: fundingOutputScript, + }}, + } + if err := btcChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + fundingTxHash := fundingTx.Hash() + + if err := chain.setPastDepositRevealedEvents( + &DepositRevealedEventFilter{ + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + StartBlock: 300000 - reservationLookBackBlocks, + }, + []*DepositRevealedEvent{{ + FundingTxHash: fundingTxHash, + FundingOutputIndex: fundingOutputIndex, + WalletPublicKeyHash: walletPublicKeyHash, + Amount: 100000, + Depositor: "0x0000000000000000000000000000000000000001", + }}, + ); err != nil { + t.Fatal(err) + } + chain.setDepositRequest(fundingTxHash, fundingOutputIndex, &DepositChainRequest{ + Amount: 100000, + RevealedAt: time.Now(), + }) + chain.setReservationAction(&ReservationAction{ + ActionType: ReservationActionTypeAcceptance, + State: ReservationActionStatePending, + TxMaxFee: 2000, + }) + + action := newAction(chain, btcChain, fundingTxHash) + // Below reservationActionSigningTimeoutSafetyMarginBlocks (300): + // every real upstream step (event match, deposit request fetch, + // reservation key derivation, action load, on-chain validation, + // transaction assembly) must succeed before this guard is + // reached and rejects the proposal - reaching this exact error + // is the test's proof that all of it worked. + action.expiryBlock = 100 + + err = action.execute() + if err == nil || err.Error() != "invalid proposal expiry block" { + t.Errorf( + "unexpected error\nexpected: [invalid proposal expiry block]\nactual: [%v]", + err, + ) + } + }) +} + +func TestReservationReanchorAction_Execute(t *testing.T) { + custodyWallet := reservationTestWallet(t) + walletPublicKeyHash := bitcoin.PublicKeyHash(custodyWallet.publicKey) + + reservationKey := big.NewInt(777) + + newAction := func( + chain Chain, + btcChain bitcoin.Chain, + ) *reservationReanchorAction { + return newReservationReanchorAction( + zap.NewNop().Sugar(), + chain, + btcChain, + custodyWallet, + nil, // signing executor unreached by these negative-path cases + &ReservationReanchorProposal{ + ReservationKey: reservationKey, + RequestNonce: 1, + TargetWalletPublicKeyHash: walletPublicKeyHash, + ReanchorTxFee: big.NewInt(1500), + }, + 300000, + 100, // below reservationActionSigningTimeoutSafetyMarginBlocks + nil, + nil, + ) + } + + t.Run("full happy path up to the signing boundary", func(t *testing.T) { + chain := Connect() + btcChain := newLocalBitcoinChain() + + anchorOutputScript, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) + if err != nil { + t.Fatal(err) + } + priorAnchorTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{ + {Value: 10000}, + {Value: 100000, PublicKeyScript: anchorOutputScript}, + }, + } + if err := btcChain.BroadcastTransaction(priorAnchorTx); err != nil { + t.Fatal(err) + } + + chain.setReservation(&Reservation{ + WalletPublicKeyHash: walletPublicKeyHash, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: priorAnchorTx.Hash(), + OutputIndex: 1, + }, + Value: 100000, + }, + }) + chain.setReservationAction(&ReservationAction{ + ActionType: ReservationActionTypeReanchor, + State: ReservationActionStatePending, + TargetWalletPublicKeyHash: walletPublicKeyHash, + TxMaxFee: 2000, + }) + + // Every real upstream step (reservation load, action load, + // type/state check, target wallet match, on-chain validation, + // transaction assembly) must succeed before the expiry-block + // guard is reached and rejects the proposal - reaching this + // exact error is the test's proof that all of it worked. + err = newAction(chain, btcChain).execute() + if err == nil || err.Error() != "invalid proposal expiry block" { + t.Errorf( + "unexpected error\nexpected: [invalid proposal expiry block]\nactual: [%v]", + err, + ) + } + }) + + t.Run("target wallet mismatch is rejected before signing", func(t *testing.T) { + chain := Connect() + btcChain := newLocalBitcoinChain() + + chain.setReservation(&Reservation{ + WalletPublicKeyHash: walletPublicKeyHash, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{0x01}, + OutputIndex: 0, + }, + Value: 100000, + }, + }) + chain.setReservationAction(&ReservationAction{ + ActionType: ReservationActionTypeReanchor, + State: ReservationActionStatePending, + TargetWalletPublicKeyHash: [20]byte{0xff}, // does not match the proposal's target + TxMaxFee: 2000, + }) + + err := newAction(chain, btcChain).execute() + if err == nil || err.Error() != "reservation action targets a different wallet" { + t.Errorf( + "unexpected error\nexpected: [reservation action targets a different wallet]\nactual: [%v]", + err, + ) + } + }) +} diff --git a/pkg/tbtc/tbtc.go b/pkg/tbtc/tbtc.go index fa009348b9..7baa5f2995 100644 --- a/pkg/tbtc/tbtc.go +++ b/pkg/tbtc/tbtc.go @@ -12,6 +12,7 @@ import ( "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-common/pkg/persistence" + "github.com/keep-network/keep-core/pkg/clientinfo" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" @@ -96,11 +97,53 @@ type Config struct { PreParamsGenerationConcurrency int // Concurrency level for key-generation for tECDSA. KeyGenerationConcurrency int + // Reservations gates the m1 reservation feature (acceptance, re-anchor, + // stranding / stale / action-timeout watchers). When disabled the + // coordination layer constructs without any reservation plumbing, so + // non-reservation deployments stay side-effect free. + Reservations ReservationsConfig +} + +// ReservationsConfig holds the reservation-related tbtc.Config fields. It is +// a separate type so future reservation knobs (poll intervals, cap overrides) +// can be added without breaking the top-level Config layout. +// +// This flag controls BOTH reservation acceptance / re-anchor proposal +// GENERATION AND watcher wiring in the start process (the `start` +// command's coordination and watcher layers, config category Tbtc). The +// `maintainer` command runs as a separate process reading a +// disjoint config category (see config.MaintainerCategories) and has its +// own independent gate, spv.ReservationsConfig.Enabled, that controls SPV +// PROOF SUBMISSION for those same proposals. Neither command's config +// loading sees the other's category, so this flag cannot be derived from or +// validated against spv.ReservationsConfig.Enabled in code. An operator +// running both `start` and `maintainer` for the reservation feature to work +// end-to-end MUST enable both flags - normally the same [Tbtc.Reservations] +// / [Maintainer.Spv.Reservations] TOML sections in one shared config file. +type ReservationsConfig struct { + // Enabled toggles reservation acceptance / re-anchor proposal + // generation and reservation watcher wiring. Defaults to false so + // existing deployments opt in explicitly. + Enabled bool +} + +// WalletMembersResolver defines the interface for resolving wallet members. +type WalletMembersResolver interface { + ResolveWalletMembers(walletPublicKeyHash [20]byte) ([]uint32, error) } // Initialize kicks off the TBTC by initializing internal state, ensuring // preconditions like staking are met, and then kicking off the internal TBTC // implementation. Returns an error if this failed. +// +// Reservation watcher wiring (stranding / stale-deposit / action-timeout, +// see pkg/maintainer/spv.WireReservationWatchers) is not performed here: +// it lives in cmd/start.go, called directly against the same tbtc.Chain +// handle once Initialize returns successfully and gated on the same +// config.Reservations.Enabled flag. Threading it through Initialize via a +// callback type would only exist to dodge a tbtc -> spv import cycle that +// cmd/start.go (which already imports both packages) does not have. + func Initialize( ctx context.Context, chain Chain, @@ -114,7 +157,7 @@ func Initialize( clientInfo *clientinfo.Registry, perfMetrics *clientinfo.PerformanceMetrics, ethereumNetwork ethereum.Network, -) error { +) (WalletMembersResolver, error) { groupParameters := defaultGroupParameters(ethereumNetwork) if ethChain, ok := chain.(interface { @@ -122,7 +165,7 @@ func Initialize( }); ok { gp, err := ethChain.EcdsaWalletGroupParametersFromChain(ctx) if err != nil { - return fmt.Errorf( + return nil, fmt.Errorf( "cannot read TBTC group sizing from ECDSA validator: [%w]", err, ) @@ -152,12 +195,12 @@ func Initialize( config, ) if err != nil { - return fmt.Errorf("cannot set up TBTC node: [%v]", err) + return nil, fmt.Errorf("cannot set up TBTC node: [%v]", err) } err = node.runCoordinationLayer(ctx) if err != nil { - return fmt.Errorf("cannot run coordination layer: [%w]", err) + return nil, fmt.Errorf("cannot run coordination layer: [%w]", err) } deduplicator := newDeduplicator() @@ -174,7 +217,11 @@ func Initialize( ) if perfMetrics == nil { - perfMetrics = clientinfo.NewPerformanceMetrics(ctx, clientInfo) + perfMetrics = clientinfo.NewPerformanceMetrics( + ctx, + clientInfo, + config.Reservations.Enabled, + ) } node.setPerformanceMetrics(perfMetrics) @@ -212,7 +259,7 @@ func Initialize( ), ) if err != nil { - return fmt.Errorf( + return nil, fmt.Errorf( "could not set up sortition pool monitoring: [%v]", err, ) @@ -374,7 +421,7 @@ func Initialize( }() }) - return nil + return node, nil } // enoughPreParamsInPoolPolicy is a policy that enforces the sufficient size diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index b5d1edc311..6ef523b52a 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -13,13 +13,14 @@ import ( "golang.org/x/exp/slices" "github.com/ipfs/go-log/v2" + "go.uber.org/zap" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/clientinfo" "github.com/keep-network/keep-core/pkg/crypto/secp256k1" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" - "go.uber.org/zap" ) // WalletActionType represents actions types that can be performed by a wallet. @@ -32,6 +33,10 @@ const ( ActionRedemption ActionMovingFunds ActionMovedFundsSweep + ActionReservationAnchor + _ // reserved: formerly ActionReservedRedemption (wire value 7); client-side scaffolding removed, wire slot retained + ActionReservationReanchor + _ // reserved: formerly ActionReservationDissolution (wire value 9); client-side scaffolding removed, wire slot retained ) // ParseWalletActionType parses the given value into a WalletActionType. @@ -49,6 +54,10 @@ func ParseWalletActionType(value uint8) (WalletActionType, error) { return ActionMovingFunds, nil case 5: return ActionMovedFundsSweep, nil + case 6: + return ActionReservationAnchor, nil + case 8: + return ActionReservationReanchor, nil default: return 0, fmt.Errorf("unknown wallet action type [%v]", value) } @@ -68,6 +77,10 @@ func (wat WalletActionType) String() string { return "MovingFunds" case ActionMovedFundsSweep: return "MovedFundsSweep" + case ActionReservationAnchor: + return "ReservationAnchor" + case ActionReservationReanchor: + return "ReservationReanchor" default: panic("unknown wallet action type") } @@ -89,6 +102,10 @@ func (wat WalletActionType) MetricName() string { return "moving_funds" case ActionMovedFundsSweep: return "moved_funds_sweep" + case ActionReservationAnchor: + return "reservation_anchor" + case ActionReservationReanchor: + return "reservation_reanchor" default: panic("unknown wallet action type") } diff --git a/pkg/tbtc/wallet_test.go b/pkg/tbtc/wallet_test.go index 6dea580e52..eadc9d352a 100644 --- a/pkg/tbtc/wallet_test.go +++ b/pkg/tbtc/wallet_test.go @@ -53,9 +53,13 @@ func TestParseWalletActionType(t *testing.T) { value: 5, expectedAction: ActionMovedFundsSweep, }, + "reservation anchor": { + value: 6, + expectedAction: ActionReservationAnchor, + }, "unknown": { - value: 6, - expectedErr: fmt.Errorf("unknown wallet action type [6]"), + value: 10, + expectedErr: fmt.Errorf("unknown wallet action type [10]"), }, } @@ -82,6 +86,29 @@ func TestParseWalletActionType(t *testing.T) { } } +func TestWalletActionType_MetricName(t *testing.T) { + tests := map[WalletActionType]string{ + ActionNoop: "noop", + ActionHeartbeat: "heartbeat", + ActionDepositSweep: "deposit_sweep", + ActionRedemption: "redemption", + ActionMovingFunds: "moving_funds", + ActionMovedFundsSweep: "moved_funds_sweep", + ActionReservationAnchor: "reservation_anchor", + } + + for actionType, expected := range tests { + if actual := actionType.MetricName(); actual != expected { + t.Errorf( + "unexpected metric name for action type [%v]\nexpected: [%v]\nactual: [%v]", + actionType, + expected, + actual, + ) + } + } +} + func TestWalletDispatcher_Dispatch(t *testing.T) { walletDispatcher := newWalletDispatcher() diff --git a/pkg/tbtcpg/bitcoin_chain_test.go b/pkg/tbtcpg/bitcoin_chain_test.go index e5cd80cf67..7832bf7a0e 100644 --- a/pkg/tbtcpg/bitcoin_chain_test.go +++ b/pkg/tbtcpg/bitcoin_chain_test.go @@ -14,6 +14,7 @@ type LocalBitcoinChain struct { transactions map[bitcoin.Hash]*bitcoin.Transaction transactionsConfirmations map[bitcoin.Hash]uint satPerVByteFeeEstimation map[uint32]int64 + txHashesByPublicKeyHash map[[20]byte][]bitcoin.Hash } func NewLocalBitcoinChain() *LocalBitcoinChain { @@ -21,6 +22,7 @@ func NewLocalBitcoinChain() *LocalBitcoinChain { transactions: make(map[bitcoin.Hash]*bitcoin.Transaction), transactionsConfirmations: make(map[bitcoin.Hash]uint), satPerVByteFeeEstimation: make(map[uint32]int64), + txHashesByPublicKeyHash: make(map[[20]byte][]bitcoin.Hash), } } @@ -104,7 +106,23 @@ func (lbc *LocalBitcoinChain) GetTransactionsForPublicKeyHash( func (lbc *LocalBitcoinChain) GetTxHashesForPublicKeyHash( publicKeyHash [20]byte, ) ([]bitcoin.Hash, error) { - panic("unsupported") + lbc.mutex.Lock() + defer lbc.mutex.Unlock() + + return lbc.txHashesByPublicKeyHash[publicKeyHash], nil +} + +// SetTxHashesForPublicKeyHash wires the wallet's transaction history for +// GetTxHashesForPublicKeyHash, used by tbtc.DetermineWalletMainUtxo to +// locate the wallet's main UTXO among its past transactions. +func (lbc *LocalBitcoinChain) SetTxHashesForPublicKeyHash( + publicKeyHash [20]byte, + hashes []bitcoin.Hash, +) { + lbc.mutex.Lock() + defer lbc.mutex.Unlock() + + lbc.txHashesByPublicKeyHash[publicKeyHash] = hashes } func (lbc *LocalBitcoinChain) GetMempoolForPublicKeyHash( diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index af939852e5..80f3809b76 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -164,4 +164,100 @@ type Chain interface { // the deposit reveal before a deposit becomes eligible for // a processing. GetDepositMinAge() (uint32, error) + + // ValidateReservationAnchorProposal validates the given reservation + // anchor proposal against the chain. Returns an error if the proposal + // is not valid or nil otherwise. + ValidateReservationAnchorProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.ReservationAnchorProposal, + depositExtraInfo struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, + ) error + + // ValidateReservationReanchorProposal validates the given reservation + // re-anchor proposal against the chain. Returns an error if the + // proposal is not valid or nil otherwise. + ValidateReservationReanchorProposal( + sourceWalletPublicKeyHash [20]byte, + proposal *tbtc.ReservationReanchorProposal, + ) error + + // RequestReservationAcceptance requests a reservation acceptance action + // generation for the given reservation. The reservation must be in a + // state that allows acceptance; the operator-side guard is enforced at + // the chain layer. + RequestReservationAcceptance( + reservationKey *big.Int, + walletPublicKeyHash [20]byte, + ) error + + // RequestReservationReanchor requests a reservation re-anchor action + // generation for the given reservation, targeting the given wallet. + RequestReservationReanchor( + reservationKey *big.Int, + targetWalletPublicKeyHash [20]byte, + ) error + + // GetReservation gets the on-chain reservation record for the given + // reservation key. Returns an error if the reservation was not found. + GetReservation(reservationKey *big.Int) (*tbtc.Reservation, error) + + // GetReservationAction gets the on-chain action record for the given + // reservation key and request nonce. Returns an error if the action + // generation was not found. + GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, + ) (*tbtc.ReservationAction, error) + + // ReservationParameters gets the current on-chain values of the Bridge + // reservation parameters. + ReservationParameters() (*tbtc.ReservationParameters, error) + + // ReservationCaps returns the cap parameters that gate reservation + // acceptance: the maximum aggregate satoshi amount a single wallet may + // custody across all of its reservations, and the maximum satoshi + // amount any single reservation may anchor. + ReservationCaps() (maxReservationsAmountPerWallet uint64, reservationMaxSingleAmount uint64, err error) + + // WalletReservationsAmount returns the aggregate satoshi amount + // currently anchored by the given wallet across all of its + // reservations. + WalletReservationsAmount(walletPublicKeyHash [20]byte) (uint64, error) + + // WalletReservationsCount returns the number of reservations currently + // custodied by the given wallet. + WalletReservationsCount(walletPublicKeyHash [20]byte) (uint32, error) + + // WalletReservations returns the reservation keys for all reservations + // currently custodied by the given wallet. + WalletReservations(walletPublicKeyHash [20]byte) ([]*big.Int, error) + + // ActiveReservationsCount returns the current count of active + // reservations across all wallets and the cap on that count. + ActiveReservationsCount() (count uint32, maxActive uint32, err error) + + // IsReservedDeposit returns true if the given deposit was revealed + // with the reservation vault address and is therefore a reservation + // rather than a default deposit. + IsReservedDeposit(depositKey *big.Int) (bool, error) + + // PastReservationAcceptanceRequestedEvents fetches past + // ReservationAcceptanceRequested events according to the provided + // filter or unfiltered if the filter is nil. Returned events are sorted + // by the block number in the ascending order. + PastReservationAcceptanceRequestedEvents( + filter *tbtc.ReservationAcceptanceRequestedEventFilter, + ) ([]*tbtc.ReservationAcceptanceRequestedEvent, error) + + // PastReservationReanchorRequestedEvents fetches past + // ReservationReanchorRequested events according to the provided + // filter or unfiltered if the filter is nil. Returned events are + // sorted by the block number in the ascending order. + PastReservationReanchorRequestedEvents( + filter *tbtc.ReservationReanchorRequestedEventFilter, + ) ([]*tbtc.ReservationReanchorRequestedEvent, error) } diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index cdff0f01e3..69de27fa20 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -29,6 +29,13 @@ type movingFundsCommitmentSubmission struct { TargetWallets [][20]byte } +// reservationReanchorRequestSubmission captures a submitted reservation +// re-anchor request that tests can inspect for assertion. +type reservationReanchorRequestSubmission struct { + ReservationKey *big.Int + TargetWalletPublicKeyHash [20]byte +} + type LocalChain struct { mutex sync.Mutex @@ -57,6 +64,16 @@ type LocalChain struct { operatorIDs map[chain.Address]uint32 redemptionDelays map[[32]byte]time.Duration depositMinAge uint32 + + reservations map[string]*tbtc.Reservation + reservationActions map[string]*tbtc.ReservationAction + reservationParametersValue tbtc.ReservationParameters + reservationParametersSet bool + reservationProposalValidations map[[32]byte]bool + reservationReanchorRequestSubmissions []*reservationReanchorRequestSubmission + reservationWalletKeys map[[20]byte][]*big.Int + liveWalletsCountValue uint32 + liveWalletsCountSet bool } func NewLocalChain() *LocalChain { @@ -78,6 +95,12 @@ func NewLocalChain() *LocalChain { movedFundsSweepProposalValidations: make(map[[32]byte]bool), operatorIDs: make(map[chain.Address]uint32), redemptionDelays: make(map[[32]byte]time.Duration), + + reservations: make(map[string]*tbtc.Reservation), + reservationActions: make(map[string]*tbtc.ReservationAction), + reservationProposalValidations: make(map[[32]byte]bool), + reservationReanchorRequestSubmissions: make([]*reservationReanchorRequestSubmission, 0), + reservationWalletKeys: make(map[[20]byte][]*big.Int), } } @@ -1003,11 +1026,46 @@ func (lc *LocalChain) SetWalletParameters( } func (lc *LocalChain) GetLiveWalletsCount() (uint32, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if lc.liveWalletsCountSet { + return lc.liveWalletsCountValue, nil + } + + count := uint32(0) + for _, wallet := range lc.walletChainData { + if wallet != nil && wallet.State == tbtc.StateLive { + count++ + } + } + return count, nil +} + +// SetLiveWalletsCount stores an explicit live-wallets count for tests. +func (lc *LocalChain) SetLiveWalletsCount(count uint32) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.liveWalletsCountValue = count + lc.liveWalletsCountSet = true } func (lc *LocalChain) ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte { - panic("unsupported") + outputIndexBytes := make([]byte, 4) + binary.BigEndian.PutUint32(outputIndexBytes, mainUtxo.Outpoint.OutputIndex) + + valueBytes := make([]byte, 8) + binary.BigEndian.PutUint64(valueBytes, uint64(mainUtxo.Value)) + + return crypto.Keccak256Hash( + append( + append( + mainUtxo.Outpoint.TransactionHash[:], + outputIndexBytes..., + ), valueBytes..., + ), + ) } func (lc *LocalChain) ComputeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte { @@ -1284,3 +1342,369 @@ func (mbc *MockBlockCounter) SetCurrentBlock(block uint64) { func (mbc *MockBlockCounter) WatchBlocks(ctx context.Context) <-chan uint64 { panic("unsupported") } + +// ValidateReservationAnchorProposal is a stub matching the reservation +// additions on the production Chain interface. Full behavioral +// validation belongs to the reservation acceptance proposal builder. +func (lc *LocalChain) ValidateReservationAnchorProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.ReservationAnchorProposal, + depositExtraInfo struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, +) error { + panic("unsupported") +} + +// ValidateReservationReanchorProposal returns nil when no explicit +// validation result was registered, mirroring the production contract's +// happy path for tests that don't need to enforce specific validation +// outcomes. Tests that need to drive specific failure modes should +// populate this via SetReservationReanchorProposalValidationResult. +func (lc *LocalChain) ValidateReservationReanchorProposal( + sourceWalletPublicKeyHash [20]byte, + proposal *tbtc.ReservationReanchorProposal, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if proposal == nil { + return fmt.Errorf("proposal is required") + } + + key, err := buildReservationReanchorProposalValidationKey( + sourceWalletPublicKeyHash, + proposal, + ) + if err != nil { + return err + } + + if result, ok := lc.reservationProposalValidations[key]; ok { + if !result { + return fmt.Errorf("validation failed") + } + } + + return nil +} + +// SetReservationReanchorProposalValidationResult stores the validation +// outcome for the given (sourceWalletPublicKeyHash, proposal) tuple. +func (lc *LocalChain) SetReservationReanchorProposalValidationResult( + sourceWalletPublicKeyHash [20]byte, + proposal *tbtc.ReservationReanchorProposal, + result bool, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key, err := buildReservationReanchorProposalValidationKey( + sourceWalletPublicKeyHash, + proposal, + ) + if err != nil { + return err + } + + lc.reservationProposalValidations[key] = result + return nil +} + +func buildReservationReanchorProposalValidationKey( + sourceWalletPublicKeyHash [20]byte, + proposal *tbtc.ReservationReanchorProposal, +) ([32]byte, error) { + var buffer bytes.Buffer + + buffer.Write(sourceWalletPublicKeyHash[:]) + + if proposal != nil { + if proposal.ReservationKey != nil { + buffer.Write(proposal.ReservationKey.Bytes()) + } + for i := 0; i < 8; i++ { + buffer.Write([]byte{byte(proposal.RequestNonce >> (8 * i))}) + } + buffer.Write(proposal.TargetWalletPublicKeyHash[:]) + if proposal.ReanchorTxFee != nil { + buffer.Write(proposal.ReanchorTxFee.Bytes()) + } + } + + return sha256.Sum256(buffer.Bytes()), nil +} + +// RequestReservationAcceptance records a submitted reservation acceptance +// request for assertion in tests. +func (lc *LocalChain) RequestReservationAcceptance( + reservationKey *big.Int, + walletPublicKeyHash [20]byte, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + _ = walletPublicKeyHash + _ = reservationKey + return nil +} + +// RequestReservationReanchor records a submitted reservation re-anchor +// request for assertion in tests. +func (lc *LocalChain) RequestReservationReanchor( + reservationKey *big.Int, + targetWalletPublicKeyHash [20]byte, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservationReanchorRequestSubmissions = append( + lc.reservationReanchorRequestSubmissions, + &reservationReanchorRequestSubmission{ + ReservationKey: new(big.Int).Set(reservationKey), + TargetWalletPublicKeyHash: targetWalletPublicKeyHash, + }, + ) + return nil +} + +// GetReservation returns the configured reservation record for the given +// reservation key, or an error if not found. +func (lc *LocalChain) GetReservation( + reservationKey *big.Int, +) (*tbtc.Reservation, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if reservation, ok := lc.reservations[reservationKey.Text(16)]; ok { + return reservation, nil + } + return nil, fmt.Errorf("reservation not found") +} + +// SetReservation stores the given reservation record keyed by reservationKey. +func (lc *LocalChain) SetReservation( + reservationKey *big.Int, + reservation *tbtc.Reservation, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservations[reservationKey.Text(16)] = reservation +} + +// GetReservationAction returns the configured reservation action record. +func (lc *LocalChain) GetReservationAction( + reservationKey *big.Int, + requestNonce uint64, +) (*tbtc.ReservationAction, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key := buildReservationActionKey(reservationKey, requestNonce) + if action, ok := lc.reservationActions[key]; ok { + return action, nil + } + // Fall back to comparing by value + for k, a := range lc.reservationActions { + expected := buildReservationActionKey(reservationKey, requestNonce) + if k == expected { + return a, nil + } + } + return nil, fmt.Errorf("reservation action not found") +} + +// SetReservationAction stores the given reservation action record. +func (lc *LocalChain) SetReservationAction( + reservationKey *big.Int, + requestNonce uint64, + action *tbtc.ReservationAction, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key := buildReservationActionKey(reservationKey, requestNonce) + lc.reservationActions[key] = action +} + +func buildReservationActionKey( + reservationKey *big.Int, + requestNonce uint64, +) string { + if reservationKey == nil { + return fmt.Sprintf("nil/%d", requestNonce) + } + return fmt.Sprintf("%s/%d", reservationKey.String(), requestNonce) +} + +// ReservationParameters returns the configured reservation parameters. +func (lc *LocalChain) ReservationParameters() ( + *tbtc.ReservationParameters, + error, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + if !lc.reservationParametersSet { + return nil, fmt.Errorf("reservation parameters not set") + } + params := lc.reservationParametersValue + return ¶ms, nil +} + +// SetReservationParameters stores the given reservation parameters. +func (lc *LocalChain) SetReservationParameters(params tbtc.ReservationParameters) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.reservationParametersValue = params + lc.reservationParametersSet = true +} + +// ReservationCaps returns a static cap-pair useful for tests. +func (lc *LocalChain) ReservationCaps() ( + maxReservationsAmountPerWallet uint64, + reservationMaxSingleAmount uint64, + err error, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return 100000000, 10000000, nil +} + +// WalletReservationsAmount returns the sum of anchor values for the +// wallet's reservations. +func (lc *LocalChain) WalletReservationsAmount( + walletPublicKeyHash [20]byte, +) (uint64, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + var total uint64 + for _, reservationKey := range lc.reservationWalletKeys[walletPublicKeyHash] { + if r, ok := lc.reservations[reservationKey.Text(16)]; ok && r != nil && r.AnchorUtxo != nil { + total += uint64(r.AnchorUtxo.Value) + } + } + return total, nil +} + +// WalletReservationsCount returns the count of reservations for the wallet. +func (lc *LocalChain) WalletReservationsCount( + walletPublicKeyHash [20]byte, +) (uint32, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return uint32(len(lc.reservationWalletKeys[walletPublicKeyHash])), nil +} + +// WalletReservations returns the configured reservation keys for the wallet. +func (lc *LocalChain) WalletReservations( + walletPublicKeyHash [20]byte, +) ([]*big.Int, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + keys := lc.reservationWalletKeys[walletPublicKeyHash] + result := make([]*big.Int, len(keys)) + for i, k := range keys { + result[i] = new(big.Int).Set(k) + } + return result, nil +} + +// SetWalletReservations stores the reservation keys associated with the wallet. +func (lc *LocalChain) SetWalletReservations( + walletPublicKeyHash [20]byte, + reservationKeys []*big.Int, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + copy := make([]*big.Int, len(reservationKeys)) + for i, k := range reservationKeys { + copy[i] = new(big.Int).Set(k) + } + lc.reservationWalletKeys[walletPublicKeyHash] = copy +} + +// Reservations is a stub mirroring the Bridge view. Tests that need this +// data should populate it explicitly via custom extensions. + +// ActiveReservationsCount reports zero active reservations by default. +func (lc *LocalChain) ActiveReservationsCount() ( + count uint32, + maxActive uint32, + err error, +) { + return 0, 0, nil +} + +// IsReservedDeposit returns false by default. +func (lc *LocalChain) IsReservedDeposit( + depositKey *big.Int, +) (bool, error) { + return false, nil +} + +// PastReservationAcceptanceRequestedEvents returns no events by default. +func (lc *LocalChain) PastReservationAcceptanceRequestedEvents( + filter *tbtc.ReservationAcceptanceRequestedEventFilter, +) ([]*tbtc.ReservationAcceptanceRequestedEvent, error) { + return nil, nil +} + +// PastReservationReanchorRequestedEvents returns the recorded re-anchor +// request submissions that match the filter. +func (lc *LocalChain) PastReservationReanchorRequestedEvents( + filter *tbtc.ReservationReanchorRequestedEventFilter, +) ([]*tbtc.ReservationReanchorRequestedEvent, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + results := make([]*tbtc.ReservationReanchorRequestedEvent, 0) + for _, submission := range lc.reservationReanchorRequestSubmissions { + event := &tbtc.ReservationReanchorRequestedEvent{ + ReservationKey: new(big.Int).Set(submission.ReservationKey), + TargetWalletPublicKeyHash: submission.TargetWalletPublicKeyHash, + } + + if filter != nil { + if len(filter.TargetWalletPublicKeyHash) > 0 { + matched := false + for _, w := range filter.TargetWalletPublicKeyHash { + if w == submission.TargetWalletPublicKeyHash { + matched = true + break + } + } + if !matched { + continue + } + } + } + + results = append(results, event) + } + return results, nil +} + +// GetReservationReanchorRequestSubmissions returns the recorded +// reservation re-anchor request submissions for assertion. +func (lc *LocalChain) GetReservationReanchorRequestSubmissions() []*reservationReanchorRequestSubmission { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + copy := make([]*reservationReanchorRequestSubmission, len(lc.reservationReanchorRequestSubmissions)) + for i, s := range lc.reservationReanchorRequestSubmissions { + copy[i] = &reservationReanchorRequestSubmission{ + ReservationKey: new(big.Int).Set(s.ReservationKey), + TargetWalletPublicKeyHash: s.TargetWalletPublicKeyHash, + } + } + return copy +} diff --git a/pkg/tbtcpg/fee.go b/pkg/tbtcpg/fee.go index b1e2acc43e..7a0d36ad18 100644 --- a/pkg/tbtcpg/fee.go +++ b/pkg/tbtcpg/fee.go @@ -3,6 +3,8 @@ package tbtcpg import ( "errors" "fmt" + + "github.com/keep-network/keep-core/pkg/bitcoin" ) // ErrMaxFeeTooLow indicates that the Bridge maximum total fee is too low to @@ -104,3 +106,42 @@ func applyWalletTxFeeFloor( return totalFee, nil } + +// estimateReservationFixedSizeTxFee estimates the fee for a reservation +// transaction with a fixed virtual size. It mirrors the fee estimation +// logic in acceptance and re-anchor tasks, including fee flooring and +// max-fee clamping. exceedsMaxErrMsg is the caller-specific message used +// when the raw estimate already exceeds txMaxFee (acceptance and re-anchor +// use distinct action-named messages here, matching their pre-existing +// fixture expectations). +func estimateReservationFixedSizeTxFee( + btcChain bitcoin.Chain, + sizeEstimator *bitcoin.TransactionSizeEstimator, + txMaxFee uint64, + exceedsMaxErrMsg string, +) (int64, error) { + transactionSize, err := sizeEstimator.VirtualSize() + if err != nil { + return 0, fmt.Errorf( + "cannot estimate transaction virtual size: [%v]", + err, + ) + } + + feeEstimator := bitcoin.NewTransactionFeeEstimator(btcChain) + totalFee, err := feeEstimator.EstimateFee(transactionSize) + if err != nil { + return 0, fmt.Errorf("cannot estimate transaction fee: [%v]", err) + } + + if uint64(totalFee) > txMaxFee { + return 0, fmt.Errorf("%s", exceedsMaxErrMsg) + } + + totalFee, err = applyWalletTxFeeFloor(totalFee, transactionSize, txMaxFee) + if err != nil { + return 0, err + } + + return totalFee, nil +} diff --git a/pkg/tbtcpg/fee_test.go b/pkg/tbtcpg/fee_test.go index cdb522c53e..04f8d8801f 100644 --- a/pkg/tbtcpg/fee_test.go +++ b/pkg/tbtcpg/fee_test.go @@ -3,6 +3,8 @@ package tbtcpg import ( "strings" "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" ) func TestApplyWalletTxFeeFloor(t *testing.T) { @@ -91,3 +93,117 @@ func TestApplyWalletTxFeeFloor(t *testing.T) { }) } } + +func TestEstimateReservationFixedSizeTxFee(t *testing.T) { + sizeEstimator := bitcoin.NewTransactionSizeEstimator(). + AddScriptHashInputs(1, depositScriptByteSize, true). + AddPublicKeyHashOutputs(1, true) + + size, err := sizeEstimator.VirtualSize() + if err != nil { + t.Fatal(err) + } + + const ( + acceptanceErrMsg = "reservation acceptance estimated fee exceeds the maximum fee" + reanchorErrMsg = "reservation re-anchor estimated fee exceeds the maximum fee" + ) + + tests := map[string]struct { + estimateSatPerVByte int64 + txMaxFee uint64 + exceedsMaxErrMsg string + expectedFee int64 + expectErrorContains string + }{ + "low estimate is raised to the minimum floor": { + estimateSatPerVByte: 1, + txMaxFee: 100000, + exceedsMaxErrMsg: acceptanceErrMsg, + expectedFee: 5 * size, // max(5, ceil(1*1.25)=2)=5 sat/vByte * size + }, + "estimate above the floor is buffered by 25%": { + estimateSatPerVByte: 20, + txMaxFee: 100000, + exceedsMaxErrMsg: acceptanceErrMsg, + expectedFee: 25 * size, // ceil(20*1.25) = 25 sat/vByte * size + }, + "buffered estimate above the cap is bounded to the cap": { + estimateSatPerVByte: 20, + txMaxFee: uint64(22 * size), + exceedsMaxErrMsg: acceptanceErrMsg, + expectedFee: 22 * size, + }, + "raw estimate exactly equal to the cap is allowed and bounded": { + estimateSatPerVByte: 20, + txMaxFee: uint64(20 * size), + exceedsMaxErrMsg: acceptanceErrMsg, + expectedFee: 20 * size, + }, + "raw estimate 1 sat above the cap returns an error": { + estimateSatPerVByte: 20, + txMaxFee: uint64(20*size - 1), + exceedsMaxErrMsg: acceptanceErrMsg, + expectErrorContains: acceptanceErrMsg, + }, + "raw estimate above the cap returns acceptance error": { + estimateSatPerVByte: 30, + // The raw 30*size fee already exceeds the 10*size cap, so the raw-fee + // check must error before the minimum-floor logic runs. The returned + // error must match the exact exceedsMaxErrMsg passed by the caller. + txMaxFee: uint64(10 * size), + exceedsMaxErrMsg: acceptanceErrMsg, + expectErrorContains: acceptanceErrMsg, + }, + "raw estimate above the cap returns re-anchor error": { + estimateSatPerVByte: 30, + txMaxFee: uint64(10 * size), + exceedsMaxErrMsg: reanchorErrMsg, + expectErrorContains: reanchorErrMsg, + }, + "minimum floor above the cap returns an error": { + estimateSatPerVByte: 1, + // Cap sits below 5*size (the floor) but above the raw fee (1*size), + // so the minimum-fee check must error rather than lower the fee. + txMaxFee: uint64(3 * size), + exceedsMaxErrMsg: acceptanceErrMsg, + expectErrorContains: "minimum safe transaction fee", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + btcChain := NewLocalBitcoinChain() + btcChain.SetEstimateSatPerVByteFee(1, tc.estimateSatPerVByte) + + fee, err := estimateReservationFixedSizeTxFee( + btcChain, + sizeEstimator, + tc.txMaxFee, + tc.exceedsMaxErrMsg, + ) + + if tc.expectErrorContains != "" { + if err == nil { + t.Fatalf("expected an error, got fee [%d]", fee) + } + if !strings.Contains(err.Error(), tc.expectErrorContains) { + t.Fatalf( + "expected error containing [%s]; got [%v]", + tc.expectErrorContains, err, + ) + } + return + } + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + if fee != tc.expectedFee { + t.Errorf( + "unexpected fee\nexpected: [%d]\nactual: [%d]", + tc.expectedFee, fee, + ) + } + }) + } +} diff --git a/pkg/tbtcpg/internal/test/marshaling.go b/pkg/tbtcpg/internal/test/marshaling.go index 91c390df6e..cac577f048 100644 --- a/pkg/tbtcpg/internal/test/marshaling.go +++ b/pkg/tbtcpg/internal/test/marshaling.go @@ -5,10 +5,11 @@ import ( "encoding/json" "errors" "fmt" - "github.com/keep-network/keep-core/pkg/tbtcpg" "math/big" "time" + "github.com/keep-network/keep-core/pkg/tbtcpg" + "github.com/keep-network/keep-core/internal/hexutils" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tbtc" @@ -375,3 +376,188 @@ func hexToSlice(hexString string) []byte { return bytes } + +// UnmarshalJSON implements a custom JSON unmarshaling logic to produce a +// proper ReservationReanchorTestScenario. +func (rrts *ReservationReanchorTestScenario) UnmarshalJSON(data []byte) error { + type reservationDataJSON struct { + ReservationKey string + WalletPublicKeyHash string + AnchorTxHash string + AnchorTxOutputIndex uint32 + AnchorValue int64 + State string + RequestNonce uint64 + HasPendingAction bool + PendingActionState string + } + type reservationReanchorTestScenarioJSON struct { + Title string + + SourceWalletPublicKeyHash string + SourceWalletState string + SourceWalletMainUtxoHash string + SourceWalletMainUtxoValue int64 + SourceWalletMainUtxoTxHash string + SourceWalletMainUtxoTxIndex uint32 + + TargetWalletPublicKeyHash string + + LiveWalletsCount uint32 + + MovingFundsDustThreshold uint64 + ReservationTxMaxFee uint64 + EstimateSatPerVByteFee int64 + ReanchorTxFee int64 + + Reservations []reservationDataJSON + + ExpectedProposal *reservationReanchorProposalJSON + ExpectedErr string + } + + var unmarshaled reservationReanchorTestScenarioJSON + + if err := json.Unmarshal(data, &unmarshaled); err != nil { + return err + } + + rrts.Title = unmarshaled.Title + + if len(unmarshaled.SourceWalletPublicKeyHash) > 0 { + copy(rrts.SourceWalletPublicKeyHash[:], hexToSlice(unmarshaled.SourceWalletPublicKeyHash)) + } + rrts.SourceWalletState = parseWalletState(unmarshaled.SourceWalletState) + if len(unmarshaled.SourceWalletMainUtxoHash) > 0 { + copy(rrts.SourceWalletMainUtxoHashBytes[:], hexToSlice(unmarshaled.SourceWalletMainUtxoHash)) + } else { + rrts.SourceWalletMainUtxoHashBytes = [32]byte{} + } + rrts.SourceWalletMainUtxoValue = unmarshaled.SourceWalletMainUtxoValue + rrts.SourceWalletMainUtxoTxHash = unmarshaled.SourceWalletMainUtxoTxHash + rrts.SourceWalletMainUtxoTxIndex = unmarshaled.SourceWalletMainUtxoTxIndex + + if len(unmarshaled.TargetWalletPublicKeyHash) > 0 { + copy(rrts.TargetWalletPublicKeyHash[:], hexToSlice(unmarshaled.TargetWalletPublicKeyHash)) + } + + rrts.LiveWalletsCount = unmarshaled.LiveWalletsCount + + rrts.MovingFundsDustThreshold = unmarshaled.MovingFundsDustThreshold + rrts.ReservationTxMaxFee = unmarshaled.ReservationTxMaxFee + rrts.EstimateSatPerVByteFee = unmarshaled.EstimateSatPerVByteFee + rrts.ReanchorTxFee = unmarshaled.ReanchorTxFee + + rrts.Reservations = make([]*ReservationReanchorData, 0, len(unmarshaled.Reservations)) + for _, r := range unmarshaled.Reservations { + d := &ReservationReanchorData{} + + if len(r.ReservationKey) > 0 { + keyBytes := hexToSlice(r.ReservationKey) + d.ReservationKey = new(big.Int).SetBytes(keyBytes) + } + if len(r.WalletPublicKeyHash) > 0 { + copy(d.WalletPublicKeyHash[:], hexToSlice(r.WalletPublicKeyHash)) + } + d.AnchorTxHash = r.AnchorTxHash + d.AnchorTxOutputIndex = r.AnchorTxOutputIndex + d.AnchorValue = r.AnchorValue + d.State = parseReservationState(r.State) + d.RequestNonce = r.RequestNonce + d.HasPendingAction = r.HasPendingAction + d.PendingActionState = parseReservationActionState(r.PendingActionState) + + rrts.Reservations = append(rrts.Reservations, d) + } + + if unmarshaled.ExpectedProposal != nil { + prop, err := unmarshaled.ExpectedProposal.convert() + if err != nil { + return fmt.Errorf( + "failed to convert expected reservation re-anchor proposal: [%w]", + err, + ) + } + rrts.ExpectedProposal = prop + } + + if len(unmarshaled.ExpectedErr) > 0 { + rrts.ExpectedErr = errors.New(unmarshaled.ExpectedErr) + } + + return nil +} + +type reservationReanchorProposalJSON struct { + ReservationKey string + RequestNonce uint64 + TargetWalletPublicKeyHash string + ReanchorTxFee int64 +} + +func (rj *reservationReanchorProposalJSON) convert() (*tbtc.ReservationReanchorProposal, error) { + if rj == nil { + return nil, nil + } + + result := &tbtc.ReservationReanchorProposal{ + RequestNonce: rj.RequestNonce, + ReanchorTxFee: big.NewInt(rj.ReanchorTxFee), + } + if len(rj.ReservationKey) > 0 { + result.ReservationKey = new(big.Int).SetBytes(hexToSlice(rj.ReservationKey)) + } + if len(rj.TargetWalletPublicKeyHash) > 0 { + copy(result.TargetWalletPublicKeyHash[:], hexToSlice(rj.TargetWalletPublicKeyHash)) + } + return result, nil +} + +func parseWalletState(s string) tbtc.WalletState { + switch s { + case "Live": + return tbtc.StateLive + case "MovingFunds": + return tbtc.StateMovingFunds + case "Closing": + return tbtc.StateClosing + case "Closed": + return tbtc.StateClosed + case "Terminated": + return tbtc.StateTerminated + default: + return tbtc.StateUnknown + } +} + +func parseReservationState(s string) tbtc.ReservationState { + switch s { + case "Active": + return tbtc.ReservationStateActive + case "ActionPending": + return tbtc.ReservationStateActionPending + case "Closed": + return tbtc.ReservationStateClosed + case "Stranded": + return tbtc.ReservationStateStranded + default: + return tbtc.ReservationStateUnknown + } +} + +func parseReservationActionState(s string) tbtc.ReservationActionState { + switch s { + case "Pending": + return tbtc.ReservationActionStatePending + case "Settled": + return tbtc.ReservationActionStateSettled + case "TimedOut": + return tbtc.ReservationActionStateTimedOut + case "Vetoed": + return tbtc.ReservationActionStateVetoed + case "Superseded": + return tbtc.ReservationActionStateSuperseded + default: + return tbtc.ReservationActionStateUnknown + } +} diff --git a/pkg/tbtcpg/internal/test/reservation_acceptance.go b/pkg/tbtcpg/internal/test/reservation_acceptance.go new file mode 100644 index 0000000000..9ce2d2f537 --- /dev/null +++ b/pkg/tbtcpg/internal/test/reservation_acceptance.go @@ -0,0 +1,370 @@ +package test + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + "github.com/keep-network/keep-core/internal/hexutils" + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// reservationAcceptanceTestDataFilePrefix is the prefix shared by every +// reservation acceptance scenario file under testdata/. The loader walks +// the directory and matches files whose name starts with this prefix. +const reservationAcceptanceTestDataFilePrefix = "reservation_acceptance" + +// ReservedDepositScenario holds a single reserved deposit's data in a +// reservation acceptance test scenario, including unexported parsed copies +// populated by UnmarshalJSON for use by Materialize. +type ReservedDepositScenario struct { + FundingTxHash string + FundingOutputIndex uint32 + FundingTxConfirmations uint + FundingTxHex string + WalletPublicKeyHash string + Depositor string + BlindingFactor string + RefundPublicKeyHash string + RefundLocktime string + Amount uint64 + RevealBlock uint64 + Age int64 + SweptAt int64 + Vault string + + parsedFundingTxHash bitcoin.Hash + parsedFundingTx *bitcoin.Transaction +} + +// ReservationAcceptanceTestScenario represents one test scenario for the +// reservation acceptance proposal builder. It captures the on-chain state +// (chain parameters, reserved deposits, cap snapshot, wallet state) and the +// expected outcome (no proposal or a specific anchor proposal). +type ReservationAcceptanceTestScenario struct { + Title string + + ChainParameters struct { + AverageBlockTime time.Duration + CurrentBlock uint64 + DepositMinAge uint32 + } + + WalletPublicKeyHash [20]byte + + ReservationVault string + + WalletState string + + ReservationParameters struct { + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + } + + Caps struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 + } + + WalletCustody struct { + Count uint32 + Amount uint64 + } + + Global struct { + ActiveCount uint32 + MaxActive uint32 + } + + PendingReservedDeposits uint64 + + ReservedDeposits []*ReservedDepositScenario + + ExpectedAnchorProposal *tbtc.ReservationAnchorProposal + ExpectedErr error +} + +// reservationAnchorProposalScenario is the JSON-friendly representation of +// the expected anchor proposal. +type reservationAnchorProposalScenario struct { + DepositFundingTxHash string + DepositFundingOutputIndex uint32 + RequestNonce uint64 + AnchorTxFee int64 +} + +// convert builds a *tbtc.ReservationAnchorProposal from the scenario's +// JSON-friendly form. It returns nil when the scenario is nil. +func (ras *reservationAnchorProposalScenario) convert() *tbtc.ReservationAnchorProposal { + if ras == nil { + return nil + } + + fundingTxHash, err := bitcoin.NewHashFromString( + ras.DepositFundingTxHash, + bitcoin.ReversedByteOrder, + ) + if err != nil { + panic(fmt.Errorf( + "failed to parse anchor deposit funding tx hash: [%w]", + err, + )) + } + + return &tbtc.ReservationAnchorProposal{ + DepositFundingTxHash: fundingTxHash, + DepositFundingOutputIndex: ras.DepositFundingOutputIndex, + RequestNonce: ras.RequestNonce, + AnchorTxFee: big.NewInt(ras.AnchorTxFee), + } +} + +// LoadReservationAcceptanceTestScenario loads all scenarios related with +// reservation acceptance. The scenarios live in +// internal/test/testdata/reservation_acceptance_scenario_*.json. +func LoadReservationAcceptanceTestScenario() ( + []*ReservationAcceptanceTestScenario, + error, +) { + return loadTestScenarios[*ReservationAcceptanceTestScenario]( + reservationAcceptanceTestDataFilePrefix, + ) +} + +// UnmarshalJSON implements a custom JSON unmarshaling logic to produce a +// proper ReservationAcceptanceTestScenario. +func (rats *ReservationAcceptanceTestScenario) UnmarshalJSON( + data []byte, +) error { + type reservedDepositScenarioJSON struct { + FundingTxHash string + FundingOutputIndex uint32 + FundingTxConfirmations uint + FundingTxHex string + WalletPublicKeyHash string + Depositor string + BlindingFactor string + RefundPublicKeyHash string + RefundLocktime string + Amount uint64 + RevealBlock uint64 + Age int64 + SweptAt int64 + Vault string + } + + type scenario struct { + Title string + ChainParameters struct { + AverageBlockTime int64 + CurrentBlock uint64 + DepositMinAge uint32 + } + WalletPublicKeyHash string + ReservationVault string + Wallet struct { + State string + } + ReservationParameters struct { + ReservationMinAmount uint64 + ReservationTxMaxFee uint64 + ReservationMaxTotalAmount uint64 + ReservationTotalAmount uint64 + MaxReservationsPerWallet uint32 + } + Caps struct { + MaxReservationsAmountPerWallet uint64 + ReservationMaxSingleAmount uint64 + } + WalletCustody struct { + Count uint32 + Amount uint64 + } + Global struct { + ActiveCount uint32 + MaxActive uint32 + } + PendingReservedDeposits uint64 + ReservedDeposits []reservedDepositScenarioJSON + ExpectedAnchorProposal *reservationAnchorProposalScenario + ExpectedErr string + } + + bytesFromHex := func(str string) []byte { + value, err := hexutils.Decode(str) + if err != nil { + panic(err) + } + return value + } + + txFromHex := func(str string) *bitcoin.Transaction { + transaction := new(bitcoin.Transaction) + err := transaction.Deserialize(bytesFromHex(str)) + if err != nil { + panic(err) + } + return transaction + } + + var unmarshaled scenario + if err := json.Unmarshal(data, &unmarshaled); err != nil { + return err + } + + rats.Title = unmarshaled.Title + + rats.ChainParameters.AverageBlockTime = + time.Duration(unmarshaled.ChainParameters.AverageBlockTime) * time.Second + rats.ChainParameters.CurrentBlock = unmarshaled.ChainParameters.CurrentBlock + rats.ChainParameters.DepositMinAge = unmarshaled.ChainParameters.DepositMinAge + + if len(unmarshaled.WalletPublicKeyHash) > 0 { + walletBytes := hexToSlice(unmarshaled.WalletPublicKeyHash) + if len(walletBytes) != 20 { + return fmt.Errorf( + "wallet public key hash must be 20 bytes, got [%d]", + len(walletBytes), + ) + } + copy(rats.WalletPublicKeyHash[:], walletBytes) + } + + rats.ReservationVault = unmarshaled.ReservationVault + rats.WalletState = unmarshaled.Wallet.State + + rats.ReservationParameters.ReservationMinAmount = + unmarshaled.ReservationParameters.ReservationMinAmount + rats.ReservationParameters.ReservationTxMaxFee = + unmarshaled.ReservationParameters.ReservationTxMaxFee + rats.ReservationParameters.ReservationMaxTotalAmount = + unmarshaled.ReservationParameters.ReservationMaxTotalAmount + rats.ReservationParameters.ReservationTotalAmount = + unmarshaled.ReservationParameters.ReservationTotalAmount + rats.ReservationParameters.MaxReservationsPerWallet = + unmarshaled.ReservationParameters.MaxReservationsPerWallet + + rats.Caps.MaxReservationsAmountPerWallet = + unmarshaled.Caps.MaxReservationsAmountPerWallet + rats.Caps.ReservationMaxSingleAmount = + unmarshaled.Caps.ReservationMaxSingleAmount + + rats.WalletCustody.Count = unmarshaled.WalletCustody.Count + rats.WalletCustody.Amount = unmarshaled.WalletCustody.Amount + + rats.Global.ActiveCount = unmarshaled.Global.ActiveCount + rats.Global.MaxActive = unmarshaled.Global.MaxActive + + rats.PendingReservedDeposits = unmarshaled.PendingReservedDeposits + + now := time.Now() + + rats.ReservedDeposits = make([]*ReservedDepositScenario, 0) + for _, rd := range unmarshaled.ReservedDeposits { + fundingTxHash, err := bitcoin.NewHashFromString( + rd.FundingTxHash, + bitcoin.ReversedByteOrder, + ) + if err != nil { + return fmt.Errorf( + "failed to parse reserved deposit funding tx hash: [%w]", + err, + ) + } + + var fundingTx *bitcoin.Transaction + if len(rd.FundingTxHex) > 0 { + fundingTx = txFromHex(rd.FundingTxHex) + } + + rats.ReservedDeposits = append( + rats.ReservedDeposits, + &ReservedDepositScenario{ + FundingTxHash: rd.FundingTxHash, + FundingOutputIndex: rd.FundingOutputIndex, + FundingTxConfirmations: rd.FundingTxConfirmations, + FundingTxHex: rd.FundingTxHex, + WalletPublicKeyHash: rd.WalletPublicKeyHash, + Depositor: rd.Depositor, + BlindingFactor: rd.BlindingFactor, + RefundPublicKeyHash: rd.RefundPublicKeyHash, + RefundLocktime: rd.RefundLocktime, + Amount: rd.Amount, + RevealBlock: rd.RevealBlock, + Age: rd.Age, + SweptAt: rd.SweptAt, + Vault: rd.Vault, + parsedFundingTxHash: fundingTxHash, + parsedFundingTx: fundingTx, + }, + ) + } + + rats.ExpectedAnchorProposal = unmarshaled.ExpectedAnchorProposal.convert() + + if len(unmarshaled.ExpectedErr) > 0 { + rats.ExpectedErr = errors.New(unmarshaled.ExpectedErr) + } + + _ = now + return nil +} + +// ReservedDeposit is the materialized form of a reserved deposit scenario, +// populated by the test driver once the chain state is set up. +type ReservedDeposit struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + FundingTx *bitcoin.Transaction + WalletPublicKeyHash [20]byte + RevealBlock uint64 + RevealedAt time.Time + SweptAt time.Time + Amount uint64 + Vault *chain.Address +} + +// Materialize converts a scenario row into a fully-typed ReservedDeposit +// the test driver can wire into the local chain. +func (rds *ReservedDepositScenario) Materialize() (*ReservedDeposit, error) { + if rds == nil { + return nil, fmt.Errorf("nil scenario deposit") + } + + if rds.parsedFundingTxHash == (bitcoin.Hash{}) { + return nil, fmt.Errorf("scenario not yet unmarshaled") + } + + var walletHash [20]byte + if len(rds.WalletPublicKeyHash) > 0 { + copy(walletHash[:], hexToSlice(rds.WalletPublicKeyHash)) + } + + var vault *chain.Address + if len(rds.Vault) > 0 { + addr := chain.Address(rds.Vault) + vault = &addr + } + + age := time.Duration(rds.Age) * time.Second + revealedAt := time.Now().Add(-age) + + return &ReservedDeposit{ + FundingTxHash: rds.parsedFundingTxHash, + FundingOutputIndex: rds.FundingOutputIndex, + FundingTx: rds.parsedFundingTx, + WalletPublicKeyHash: walletHash, + RevealBlock: rds.RevealBlock, + RevealedAt: revealedAt, + SweptAt: time.Unix(rds.SweptAt, 0), + Amount: rds.Amount, + Vault: vault, + }, nil +} diff --git a/pkg/tbtcpg/internal/test/tbtcpgtest.go b/pkg/tbtcpg/internal/test/tbtcpgtest.go index b03cc40468..19d2ced166 100644 --- a/pkg/tbtcpg/internal/test/tbtcpgtest.go +++ b/pkg/tbtcpg/internal/test/tbtcpgtest.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/fs" + "math/big" "os" "path/filepath" "runtime" @@ -21,6 +22,7 @@ const ( findDepositsToSweepTestDataFilePrefix = "find_deposits" proposeDepositsSweepTestDataFilePrefix = "propose_sweep" findPendingRedemptionsTestDataFilePrefix = "find_pending_redemptions" + reservationReanchorTestDataFilePrefix = "reservation_reanchor" ) // Deposit holds the deposit data in the given test scenario. @@ -144,6 +146,58 @@ func LoadFindPendingRedemptionsTestScenario() ( ) } +// ReservationReanchorData holds the per-reservation data in a reservation +// re-anchor test scenario. +type ReservationReanchorData struct { + ReservationKey *big.Int + WalletPublicKeyHash [20]byte + AnchorTxHash string + AnchorTxOutputIndex uint32 + AnchorValue int64 + State tbtc.ReservationState + RequestNonce uint64 + HasPendingAction bool + PendingActionState tbtc.ReservationActionState +} + +// ReservationReanchorTestScenario represents a test scenario of preparing a +// reservation re-anchor proposal. +type ReservationReanchorTestScenario struct { + Title string + + SourceWalletPublicKeyHash [20]byte + SourceWalletState tbtc.WalletState + SourceWalletMainUtxoHashBytes [32]byte + SourceWalletMainUtxoValue int64 + SourceWalletMainUtxoTxHash string + SourceWalletMainUtxoTxIndex uint32 + + TargetWalletPublicKeyHash [20]byte + + LiveWalletsCount uint32 + + MovingFundsDustThreshold uint64 + ReservationTxMaxFee uint64 + EstimateSatPerVByteFee int64 + ReanchorTxFee int64 + + Reservations []*ReservationReanchorData + + ExpectedProposal *tbtc.ReservationReanchorProposal + ExpectedErr error +} + +// LoadReservationReanchorTestScenario loads all scenarios related to +// reservation re-anchor proposals. +func LoadReservationReanchorTestScenario() ( + []*ReservationReanchorTestScenario, + error, +) { + return loadTestScenarios[*ReservationReanchorTestScenario]( + reservationReanchorTestDataFilePrefix, + ) +} + func loadTestScenarios[T json.Unmarshaler](testDataFilePrefix string) ([]T, error) { filePaths, err := detectTestDataFiles(testDataFilePrefix) if err != nil { diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_0.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_0.json new file mode 100644 index 0000000000..40231251b1 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_0.json @@ -0,0 +1,57 @@ +{ + "Title": "happy path - one reserved deposit eligible for acceptance", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 0, + "Amount": 0 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039523", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": { + "DepositFundingTxHash": "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "DepositFundingOutputIndex": 0, + "RequestNonce": 1, + "AnchorTxFee": 710 + }, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_1.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_1.json new file mode 100644 index 0000000000..464de89f3a --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_1.json @@ -0,0 +1,52 @@ +{ + "Title": "cap rejection - wallet already at max active reservations count", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 1, + "Amount": 2000000 + }, + "Global": { + "ActiveCount": 100, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "b1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039524", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_2.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_2.json new file mode 100644 index 0000000000..0dc78d7711 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_2.json @@ -0,0 +1,52 @@ +{ + "Title": "below-min rejection - deposit amount below ReservationMinAmount", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 1000000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 0, + "Amount": 0 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "c1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039525", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 500000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_3.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_3.json new file mode 100644 index 0000000000..6da58123e9 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_3.json @@ -0,0 +1,52 @@ +{ + "Title": "stale-deposit rejection - wallet state is Closing, not Live", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Closing" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 0, + "Amount": 0 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "d1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039526", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_4.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_4.json new file mode 100644 index 0000000000..53d9a51bc9 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_4.json @@ -0,0 +1,52 @@ +{ + "Title": "cap rejection - wallet already at max reservations per wallet", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 3 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 3, + "Amount": 2000000 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "e1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039524", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_5.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_5.json new file mode 100644 index 0000000000..3f628af58b --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_5.json @@ -0,0 +1,52 @@ +{ + "Title": "cap rejection - deposit amount exceeds ReservationMaxSingleAmount", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 1000000 + }, + "WalletCustody": { + "Count": 1, + "Amount": 0 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "f1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039525", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_6.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_6.json new file mode 100644 index 0000000000..0692e7b9d2 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_6.json @@ -0,0 +1,52 @@ +{ + "Title": "cap rejection - accepting would exceed wallet aggregate amount cap", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 100000000, + "ReservationTotalAmount": 0, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 3000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 1, + "Amount": 2000000 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "11b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039526", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_7.json b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_7.json new file mode 100644 index 0000000000..be65953866 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_acceptance_scenario_7.json @@ -0,0 +1,52 @@ +{ + "Title": "cap rejection - accepting would exceed global reservation total cap", + "ChainParameters": { + "AverageBlockTime": 12, + "CurrentBlock": 300000, + "DepositMinAge": 3600 + }, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "ReservationVault": "0xReservationVaultAddress1234567890abcdef12345678", + "Wallet": { + "State": "Live" + }, + "ReservationParameters": { + "ReservationMinAmount": 100000, + "ReservationTxMaxFee": 5000, + "ReservationMaxTotalAmount": 3000000, + "ReservationTotalAmount": 2000000, + "MaxReservationsPerWallet": 5 + }, + "Caps": { + "MaxReservationsAmountPerWallet": 50000000, + "ReservationMaxSingleAmount": 5000000 + }, + "WalletCustody": { + "Count": 1, + "Amount": 2000000 + }, + "Global": { + "ActiveCount": 0, + "MaxActive": 100 + }, + "PendingReservedDeposits": 0, + "ReservedDeposits": [ + { + "FundingTxHash": "21b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f00", + "FundingOutputIndex": 0, + "FundingTxConfirmations": 6, + "WalletPublicKeyHash": "8db50eb52063ea9d98b3eac91489a90f738986f6", + "Depositor": "934b98637ca318a4d6e7ca6ffd1690b8e77df637", + "BlindingFactor": "f9f0c90d00039527", + "RefundPublicKeyHash": "e257eccafbc07c381642ce6e7e55120fb077fbed", + "RefundLocktime": "e0250162", + "Amount": 2000000, + "RevealBlock": 290000, + "Age": 7200, + "SweptAt": 0, + "Vault": "0xReservationVaultAddress1234567890abcdef12345678" + } + ], + "ExpectedAnchorProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_0.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_0.json new file mode 100644 index 0000000000..46056b83ed --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_0.json @@ -0,0 +1,34 @@ +{ + "Title": "wallet-migration trigger: emits re-anchor proposal for first active reservation", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "1111111111111111111111111111111111111111111111111111111111111111", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "2222222222222222222222222222222222222222222222222222222222222222", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000aaaa01", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "3333333333333333333333333333333333333333333333333333333333333333", + "AnchorTxOutputIndex": 1, + "AnchorValue": 100000, + "State": "Active", + "RequestNonce": 0, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ], + "ExpectedProposal": { + "ReservationKey": "0xaaaa01", + "RequestNonce": 1, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "ReanchorTxFee": 550 + } +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_1.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_1.json new file mode 100644 index 0000000000..913a7ec21b --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_1.json @@ -0,0 +1,34 @@ +{ + "Title": "below-dust trigger: Live wallet without main UTXO re-anchors", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "Live", + "SourceWalletMainUtxoHash": "0000000000000000000000000000000000000000000000000000000000000000", + "SourceWalletMainUtxoValue": 0, + "SourceWalletMainUtxoTxHash": "0000000000000000000000000000000000000000000000000000000000000000", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000bbbb02", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "6666666666666666666666666666666666666666666666666666666666666666", + "AnchorTxOutputIndex": 1, + "AnchorValue": 200000, + "State": "Active", + "RequestNonce": 5, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ], + "ExpectedProposal": { + "ReservationKey": "0xbbbb02", + "RequestNonce": 6, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "ReanchorTxFee": 550 + } +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_2.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_2.json new file mode 100644 index 0000000000..94d2fd837e --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_2.json @@ -0,0 +1,28 @@ +{ + "Title": "cap rejection: estimated re-anchor fee exceeds ReservationTxMaxFee", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "7777777777777777777777777777777777777777777777777777777777777777", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "8888888888888888888888888888888888888888888888888888888888888888", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100, + "EstimateSatPerVByteFee": 25, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000cccc03", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "9999999999999999999999999999999999999999999999999999999999999999", + "AnchorTxOutputIndex": 1, + "AnchorValue": 500000, + "State": "Active", + "RequestNonce": 0, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ] +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_3.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_3.json new file mode 100644 index 0000000000..f78f5252cb --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_3.json @@ -0,0 +1,28 @@ +{ + "Title": "action-already-pending: pending action suppresses re-anchor, no proposal emitted", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000dddd04", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "AnchorTxOutputIndex": 1, + "AnchorValue": 200000, + "State": "Active", + "RequestNonce": 4, + "HasPendingAction": true, + "PendingActionState": "Pending" + } + ] +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_4.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_4.json new file mode 100644 index 0000000000..39d47f88d7 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_4.json @@ -0,0 +1,28 @@ +{ + "Title": "cap rejection: minimum safe re-anchor fee exceeds ReservationTxMaxFee", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 500, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000eeee05", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "AnchorTxOutputIndex": 1, + "AnchorValue": 500000, + "State": "Active", + "RequestNonce": 0, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ] +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_5.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_5.json new file mode 100644 index 0000000000..97f2d3aa1c --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_5.json @@ -0,0 +1,30 @@ +{ + "Title": "no-op: no live wallets available for re-anchor target", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "1111111111111111111111111111111111111111111111111111111111111111", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "2222222222222222222222222222222222222222222222222222222222222222", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "", + "LiveWalletsCount": 0, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000ffff06", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "3333333333333333333333333333333333333333333333333333333333333333", + "AnchorTxOutputIndex": 1, + "AnchorValue": 500000, + "State": "Active", + "RequestNonce": 0, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ], + "ExpectedProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_6.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_6.json new file mode 100644 index 0000000000..2b02ea0d68 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_6.json @@ -0,0 +1,18 @@ +{ + "Title": "no-op: wallet has no reservations to re-anchor", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "4444444444444444444444444444444444444444444444444444444444444444", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "5555555555555555555555555555555555555555555555555555555555555555", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [], + "ExpectedProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_7.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_7.json new file mode 100644 index 0000000000..808358b1b1 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_7.json @@ -0,0 +1,30 @@ +{ + "Title": "skip: reservation not in Active state is excluded from re-anchor", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "6666666666666666666666666666666666666666666666666666666666666666", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "7777777777777777777777777777777777777777777777777777777777777777", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000111107", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "8888888888888888888888888888888888888888888888888888888888888888", + "AnchorTxOutputIndex": 1, + "AnchorValue": 500000, + "State": "Stranded", + "RequestNonce": 0, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ], + "ExpectedProposal": null, + "ExpectedErr": "" +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_8.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_8.json new file mode 100644 index 0000000000..efa8535f27 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_8.json @@ -0,0 +1,34 @@ +{ + "Title": "findTargetWallet: source wallet is excluded from targets", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "MovingFunds", + "SourceWalletMainUtxoHash": "1111111111111111111111111111111111111111111111111111111111111111", + "SourceWalletMainUtxoValue": 500000, + "SourceWalletMainUtxoTxHash": "2222222222222222222222222222222222222222222222222222222222222222", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [ + { + "ReservationKey": "0000000000000000000000000000000000000000000000000000000000aaaa01", + "WalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "AnchorTxHash": "3333333333333333333333333333333333333333333333333333333333333333", + "AnchorTxOutputIndex": 1, + "AnchorValue": 100000, + "State": "Active", + "RequestNonce": 0, + "HasPendingAction": false, + "PendingActionState": "Unknown" + } + ], + "ExpectedProposal": { + "ReservationKey": "0xaaaa01", + "RequestNonce": 1, + "TargetWalletPublicKeyHash": "0x92a6ec889a8fa34f731e639edede4c75e184307c", + "ReanchorTxFee": 550 + } +} diff --git a/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_9.json b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_9.json new file mode 100644 index 0000000000..ac3f5f4020 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/reservation_reanchor_scenario_9.json @@ -0,0 +1,17 @@ +{ + "Title": "eligibility gate: Live wallet above dust threshold, no proposal", + "SourceWalletPublicKeyHash": "0xffb3f7538bfa98a511495dd96027cfbd57baf2fa", + "SourceWalletState": "Live", + "SourceWalletMainUtxoHash": "1111111111111111111111111111111111111111111111111111111111111111", + "SourceWalletMainUtxoValue": 2000000, + "SourceWalletMainUtxoTxHash": "2222222222222222222222222222222222222222222222222222222222222222", + "SourceWalletMainUtxoTxIndex": 0, + "TargetWalletPublicKeyHash": "0x0000000000000000000000000000000000000000", + "LiveWalletsCount": 4, + "MovingFundsDustThreshold": 1000000, + "ReservationTxMaxFee": 100000, + "EstimateSatPerVByteFee": 1, + "ReanchorTxFee": 0, + "Reservations": [], + "ExpectedProposal": null +} diff --git a/pkg/tbtcpg/reservation_acceptance.go b/pkg/tbtcpg/reservation_acceptance.go new file mode 100644 index 0000000000..62818009f1 --- /dev/null +++ b/pkg/tbtcpg/reservation_acceptance.go @@ -0,0 +1,627 @@ +package tbtcpg + +import ( + "context" + "fmt" + "math/big" + "sort" + "strings" + "time" + + "github.com/ipfs/go-log/v2" + "go.uber.org/zap" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// ReservationAcceptanceLookBackBlocks is the look-back period in blocks used +// when searching for reservation candidate deposits. It mirrors the deposit +// sweep look-back window: 30 days at 12 seconds per block. +const ReservationAcceptanceLookBackBlocks = uint64(216000) + +// ReservationAcceptanceTask is a task that may produce a reservation +// acceptance (anchor) proposal. It scans the chain for reserved deposits +// revealed to the operator's wallet, validates the wallet's eligibility +// against the active reservation caps, and emits a proposal whose resulting +// transaction is a 1-input-1-output anchor that disables the deposit's +// refund path. +type ReservationAcceptanceTask struct { + chain Chain + btcChain bitcoin.Chain +} + +// NewReservationAcceptanceTask constructs a ReservationAcceptanceTask. +func NewReservationAcceptanceTask( + chain Chain, + btcChain bitcoin.Chain, +) *ReservationAcceptanceTask { + return &ReservationAcceptanceTask{ + chain: chain, + btcChain: btcChain, + } +} + +// Run inspects the chain for an acceptance candidate reserved deposit and, +// if one passes the eligibility gate, returns the resulting anchor proposal. +// The task is a no-op (proposal == nil, shouldExecute == false) when no +// candidate exists. +func (rat *ReservationAcceptanceTask) Run(request *tbtc.CoordinationProposalRequest) ( + tbtc.CoordinationProposal, + bool, + error, +) { + walletPublicKeyHash := request.WalletPublicKeyHash + + taskLogger := logger.With( + zap.String("task", rat.ActionType().String()), + zap.String("walletPKH", fmt.Sprintf("0x%x", walletPublicKeyHash)), + ) + + candidate, err := rat.findReservationAcceptanceCandidate( + taskLogger, + walletPublicKeyHash, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot find reservation acceptance candidate: [%w]", + err, + ) + } + if candidate == nil { + taskLogger.Info("no reservation acceptance candidate") + return nil, false, nil + } + + proposal, shouldExecute, err := rat.proposeReservationAcceptance( + taskLogger, + walletPublicKeyHash, + candidate, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot prepare reservation acceptance proposal: [%w]", + err, + ) + } + + return proposal, shouldExecute, nil +} + +// ActionType returns the wallet action type this task proposes. +func (rat *ReservationAcceptanceTask) ActionType() tbtc.WalletActionType { + return tbtc.ActionReservationAnchor +} + +// reservationAcceptanceCandidate is the bundle a candidate reserved deposit +// for acceptance carries through the proposal builder. It captures the +// deposit's reveal context, the derived request nonce, plus the on-chain cap +// snapshot taken at scan time. +type reservationAcceptanceCandidate struct { + Deposit *tbtc.Deposit + FundingTx *bitcoin.Transaction + ReservationParameters *tbtc.ReservationParameters + TxMaxFee uint64 + RequestNonce uint64 +} + +// findReservationAcceptanceCandidate returns the first reserved deposit +// that the operator's wallet may accept, or nil when none qualifies. +func (rat *ReservationAcceptanceTask) findReservationAcceptanceCandidate( + taskLogger log.StandardLogger, + walletPublicKeyHash [20]byte, +) (*reservationAcceptanceCandidate, error) { + if walletPublicKeyHash == [20]byte{} { + return nil, fmt.Errorf("wallet public key hash is required") + } + + reservationParameters, err := rat.chain.ReservationParameters() + if err != nil { + return nil, fmt.Errorf( + "failed to get reservation parameters: [%w]", + err, + ) + } + reservationVault := reservationParameters.ReservationVault + if reservationVault == "" { + taskLogger.Info("reservation vault not configured") + return nil, nil + } + + wallet, err := rat.chain.GetWallet(walletPublicKeyHash) + if err != nil { + taskLogger.Errorf( + "failed to load wallet chain data: [%v]", + err, + ) + return nil, nil + } + if wallet.State != tbtc.StateLive { + taskLogger.Infof( + "wallet is not live (state=%v); cannot accept reservation", + wallet.State, + ) + return nil, nil + } + + blockCounter, err := rat.chain.BlockCounter() + if err != nil { + return nil, fmt.Errorf("failed to get block counter: [%w]", err) + } + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return nil, fmt.Errorf( + "failed to get current block: [%w]", + err, + ) + } + + maxReservationsAmountPerWallet, reservationMaxSingleAmount, err := + rat.chain.ReservationCaps() + if err != nil { + return nil, fmt.Errorf( + "failed to get reservation caps: [%w]", + err, + ) + } + + walletReservationsCount, err := rat.chain.WalletReservationsCount( + walletPublicKeyHash, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to get wallet reservations count: [%w]", + err, + ) + } + + walletReservationsAmount, err := rat.chain.WalletReservationsAmount( + walletPublicKeyHash, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to get wallet reservations amount: [%w]", + err, + ) + } + + activeReservationsCount, maxActiveReservations, err := + rat.chain.ActiveReservationsCount() + if err != nil { + return nil, fmt.Errorf( + "failed to get active reservations count: [%w]", + err, + ) + } + + depositMinAgeSeconds, err := rat.chain.GetDepositMinAge() + if err != nil { + return nil, fmt.Errorf( + "failed to get deposit minimum age: [%w]", + err, + ) + } + depositMinAge := time.Duration(depositMinAgeSeconds) * time.Second + + filterStartBlock := uint64(0) + if currentBlock > ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - ReservationAcceptanceLookBackBlocks + } + filter := &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + } + + depositRevealedEvents, err := rat.chain.PastDepositRevealedEvents(filter) + if err != nil { + return nil, fmt.Errorf( + "failed to get past deposit revealed events: [%w]", + err, + ) + } + + // Take the oldest first. + sort.SliceStable(depositRevealedEvents, func(i, j int) bool { + return depositRevealedEvents[i].BlockNumber < depositRevealedEvents[j].BlockNumber + }) + + now := time.Now() + + for _, event := range depositRevealedEvents { + if !depositTargetsReservationVault(event.Vault, reservationVault) { + continue + } + + depositKey := rat.chain.BuildDepositKey( + event.FundingTxHash, + event.FundingOutputIndex, + ) + + depositRequest, foundRequest, err := rat.chain.GetDepositRequest( + event.FundingTxHash, + event.FundingOutputIndex, + ) + if err != nil { + taskLogger.Errorf( + "failed to get deposit request for [%v]: [%v]", + depositKey, + err, + ) + continue + } + if !foundRequest { + taskLogger.Warnf( + "no deposit request for reserved deposit [%v]", + depositKey, + ) + continue + } + + if depositRequest.Amount < reservationParameters.ReservationMinAmount { + taskLogger.Infof( + "reserved deposit [%v] amount [%d] below minimum [%d]; skipping", + depositKey, + depositRequest.Amount, + reservationParameters.ReservationMinAmount, + ) + continue + } + + matureAt := depositRequest.RevealedAt.Add(depositMinAge) + if !now.After(matureAt) { + taskLogger.Infof( + "reserved deposit [%v] is not old enough: now=%v, matureAt=%v", + depositKey, + now, matureAt, + ) + continue + } + + if depositRequest.SweptAt.Unix() != 0 { + taskLogger.Debugf( + "reserved deposit [%v] is already swept", + depositKey, + ) + continue + } + + if !checkReservationAcceptanceEligibility( + taskLogger, + depositRequest, + walletReservationsCount, + walletReservationsAmount, + activeReservationsCount, + maxActiveReservations, + maxReservationsAmountPerWallet, + reservationMaxSingleAmount, + reservationParameters, + ) { + taskLogger.Infof("not eligible: [%v]", depositKey) + continue + } + + fundingTx, err := rat.btcChain.GetTransaction(event.FundingTxHash) + if err != nil { + taskLogger.Errorf( + "failed to get funding tx for reserved deposit [%v]: [%v]", + depositKey, + err, + ) + continue + } + + confirmations, err := rat.btcChain.GetTransactionConfirmations( + context.Background(), + event.FundingTxHash, + ) + if err != nil { + taskLogger.Errorf( + "failed to get funding tx confirmations for [%v]: [%v]", + depositKey, + err, + ) + continue + } + if confirmations < tbtc.DepositSweepRequiredFundingTxConfirmations { + taskLogger.Debugf( + "reserved deposit [%v] funding tx confirmations [%d/%d] below required", + depositKey, + confirmations, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + continue + } + + // Third fix (a): check for existing acceptance requested events + // and fail closed on error. + acceptanceEvents, err := rat.chain.PastReservationAcceptanceRequestedEvents( + &tbtc.ReservationAcceptanceRequestedEventFilter{ + ReservationKey: []*big.Int{depositKey}, + }, + ) + if err != nil { + taskLogger.Errorf( + "failed to get past reservation acceptance requested events for [%v]: [%v]", + depositKey, + err, + ) + continue + } + if len(acceptanceEvents) > 0 { + taskLogger.Infof( + "reservation [%v] already has acceptance requested event(s), skipping", + depositKey, + ) + continue + } + + // Second & Third fix: check reservation state and derive RequestNonce. + var requestNonce uint64 = 1 + reservation, err := rat.chain.GetReservation(depositKey) + if err != nil { + taskLogger.Debugf( + "cannot get reservation [%v] (assuming not yet created): [%v]", + depositKey, + err, + ) + } else if reservation != nil { + if reservation.State == tbtc.ReservationStateActive || + reservation.State == tbtc.ReservationStateActionPending || + reservation.State == tbtc.ReservationStateClosed || + reservation.State == tbtc.ReservationStateStranded { + taskLogger.Infof( + "reservation [%v] in non-eligible state [%v], skipping", + depositKey, + reservation.State, + ) + continue + } + + if hasPendingAction(depositKey, reservation, rat.chain, taskLogger) { + taskLogger.Infof( + "reservation [%v] has pending action, skipping", + depositKey, + ) + continue + } + + requestNonce = reservation.RequestNonce + 1 + } + + taskLogger.Infof( + "selected reserved deposit [%v] for acceptance", + depositKey, + ) + + return &reservationAcceptanceCandidate{ + Deposit: &tbtc.Deposit{ + Utxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: event.FundingTxHash, + OutputIndex: event.FundingOutputIndex, + }, + Value: int64(depositRequest.Amount), + }, + Depositor: depositRequest.Depositor, + BlindingFactor: event.BlindingFactor, + WalletPublicKeyHash: event.WalletPublicKeyHash, + RefundPublicKeyHash: event.RefundPublicKeyHash, + RefundLocktime: event.RefundLocktime, + Vault: depositRequest.Vault, + ExtraData: depositRequest.ExtraData, + }, + FundingTx: fundingTx, + ReservationParameters: reservationParameters, + TxMaxFee: reservationParameters.ReservationTxMaxFee, + RequestNonce: requestNonce, + }, nil + } + + return nil, nil +} + +func checkReservationAcceptanceEligibility( + taskLogger log.StandardLogger, + depositRequest *tbtc.DepositChainRequest, + walletReservationsCount uint32, + walletReservationsAmount uint64, + activeReservationsCount uint32, + maxActiveReservations uint32, + maxReservationsAmountPerWallet uint64, + reservationMaxSingleAmount uint64, + reservationParameters *tbtc.ReservationParameters, +) bool { + if reservationParameters.MaxReservationsPerWallet > 0 && + walletReservationsCount >= reservationParameters.MaxReservationsPerWallet { + taskLogger.Infof( + "wallet reservations count [%d] already at max [%d]", + walletReservationsCount, + reservationParameters.MaxReservationsPerWallet, + ) + return false + } + + if maxActiveReservations > 0 && + activeReservationsCount >= maxActiveReservations { + taskLogger.Infof( + "active reservations count [%d] already at max [%d]", + activeReservationsCount, + maxActiveReservations, + ) + return false + } + + if reservationMaxSingleAmount > 0 && + depositRequest.Amount > reservationMaxSingleAmount { + taskLogger.Infof( + "deposit amount [%d] exceeds reservation single cap [%d]", + depositRequest.Amount, + reservationMaxSingleAmount, + ) + return false + } + + newWalletTotal := walletReservationsAmount + depositRequest.Amount + if maxReservationsAmountPerWallet > 0 && + newWalletTotal > maxReservationsAmountPerWallet { + taskLogger.Infof( + "accepting would push wallet past aggregate cap "+ + "[current=%d, deposit=%d, cap=%d]", + walletReservationsAmount, + depositRequest.Amount, + maxReservationsAmountPerWallet, + ) + return false + } + + if reservationParameters.ReservationMaxTotalAmount > 0 { + newTotal := reservationParameters.ReservationTotalAmount + + depositRequest.Amount + if newTotal > reservationParameters.ReservationMaxTotalAmount { + taskLogger.Infof( + "global reservation total would exceed cap "+ + "[current=%d, deposit=%d, cap=%d]", + reservationParameters.ReservationTotalAmount, + depositRequest.Amount, + reservationParameters.ReservationMaxTotalAmount, + ) + return false + } + } + + return true +} + +func (rat *ReservationAcceptanceTask) proposeReservationAcceptance( + taskLogger log.StandardLogger, + walletPublicKeyHash [20]byte, + candidate *reservationAcceptanceCandidate, +) (*tbtc.ReservationAnchorProposal, bool, error) { + if candidate == nil || candidate.Deposit == nil { + return nil, false, fmt.Errorf("candidate is required") + } + + taskLogger.Infof("preparing a reservation acceptance proposal") + + anchorFee, err := estimateReservationAcceptanceFee( + rat.btcChain, + candidate.TxMaxFee, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot estimate reservation acceptance transaction fee: [%v]", + err, + ) + } + + anchorValue := candidate.Deposit.Utxo.Value - anchorFee + if anchorValue <= 0 { + return nil, false, fmt.Errorf( + "deposit value [%d] does not cover anchor fee [%d]", + candidate.Deposit.Utxo.Value, + anchorFee, + ) + } + + if candidate.ReservationParameters != nil && + uint64(anchorValue) < candidate.ReservationParameters.ReservationMinAmount { + return nil, false, nil + } + + taskLogger.Infof("anchor transaction fee: [%d]", anchorFee) + + reservationKey := rat.chain.BuildDepositKey( + candidate.Deposit.Utxo.Outpoint.TransactionHash, + candidate.Deposit.Utxo.Outpoint.OutputIndex, + ) + + feeBoundAction := &tbtc.ReservationAction{ + TxMaxFee: candidate.TxMaxFee, + } + + if _, err := tbtc.AssembleReservationAnchorTransaction( + rat.btcChain, + candidate.Deposit, + walletPublicKeyHash, + feeBoundAction, + anchorFee, + ); err != nil { + return nil, false, fmt.Errorf( + "cannot assemble reservation anchor transaction: [%v]", + err, + ) + } + + proposal := &tbtc.ReservationAnchorProposal{ + DepositFundingTxHash: candidate.Deposit.Utxo.Outpoint.TransactionHash, + DepositFundingOutputIndex: candidate.Deposit.Utxo.Outpoint.OutputIndex, + RequestNonce: candidate.RequestNonce, + AnchorTxFee: big.NewInt(anchorFee), + } + + taskLogger.Infof("validating the reservation anchor proposal") + + if err := rat.chain.ValidateReservationAnchorProposal( + walletPublicKeyHash, + proposal, + struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }{ + Deposit: candidate.Deposit, + FundingTx: candidate.FundingTx, + }, + ); err != nil { + return nil, false, fmt.Errorf( + "failed to verify reservation anchor proposal: %v", + err, + ) + } + + // Fifth fix: Note that RequestReservationAcceptance is called as a + // side effect of proposal generation itself (before coordination has + // agreed to anything), which is a known, accepted deviation from the + // read-only-during-generation pattern (also present in MovingFundsTask's + // SubmitMovingFundsCommitment). The guards against re-requesting + // acceptance for existing or pending reservations are the primary + // mitigation for spurious repeat writes. + if err := rat.chain.RequestReservationAcceptance( + reservationKey, + walletPublicKeyHash, + ); err != nil { + return nil, false, fmt.Errorf("cannot request reservation acceptance: [%v]", err) + } + + return proposal, true, nil +} + +func estimateReservationAcceptanceFee( + btcChain bitcoin.Chain, + txMaxFee uint64, +) (int64, error) { + sizeEstimator := bitcoin.NewTransactionSizeEstimator(). + AddScriptHashInputs(1, depositScriptByteSize, true). + AddPublicKeyHashOutputs(1, true) + + return estimateReservationFixedSizeTxFee( + btcChain, + sizeEstimator, + txMaxFee, + "reservation acceptance estimated fee exceeds the maximum fee", + ) +} + +func depositTargetsReservationVault( + depositVault *chain.Address, + reservationVault chain.Address, +) bool { + if depositVault == nil { + return false + } + return strings.EqualFold( + string(*depositVault), + string(reservationVault), + ) +} diff --git a/pkg/tbtcpg/reservation_acceptance_test.go b/pkg/tbtcpg/reservation_acceptance_test.go new file mode 100644 index 0000000000..1a40bbc5c5 --- /dev/null +++ b/pkg/tbtcpg/reservation_acceptance_test.go @@ -0,0 +1,1575 @@ +package tbtcpg_test + +import ( + "fmt" + "math/big" + "testing" + "time" + + "github.com/ipfs/go-log/v2" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/tbtc" + "github.com/keep-network/keep-core/pkg/tbtcpg" + "github.com/keep-network/keep-core/pkg/tbtcpg/internal/test" +) + +// reservationAcceptanceLocalChain is a test-only mock of tbtcpg.Chain that +// embeds the production LocalChain and adds reservation-specific behavior. +// It exists as a separate type so this test file does not need to edit the +// shared chain_test.go fixture used by sibling builders. +type reservationAcceptanceLocalChain struct { + *tbtcpg.LocalChain + + reservationParameters *tbtc.ReservationParameters + maxPerWalletAmount uint64 + maxSingleAmount uint64 + walletReservationsAmount uint64 + walletReservationsCount uint32 + activeCount uint32 + maxActive uint32 + pendingReserved uint64 + reservedDeposits map[string]bool + validateErr error + getWalletErr error + acceptanceEvents []*tbtc.ReservationAcceptanceRequestedEvent + acceptanceEventsErr error +} + +func newReservationAcceptanceLocalChain() *reservationAcceptanceLocalChain { + lc := tbtcpg.NewLocalChain() + return &reservationAcceptanceLocalChain{ + LocalChain: lc, + reservedDeposits: make(map[string]bool), + } +} + +// PastDepositRevealedEvents overrides the embedded LocalChain +// implementation to return an empty slice (rather than an error) when no +// events are registered for the filter. A real chain returns an empty +// event list when no deposits match; the in-memory mock's panic-stub +// "no events for given filter" error is a fixture bug that this override +// papers over without touching shared test infrastructure. +func (ralc *reservationAcceptanceLocalChain) PastDepositRevealedEvents( + filter *tbtc.DepositRevealedEventFilter, +) ([]*tbtc.DepositRevealedEvent, error) { + events, err := ralc.LocalChain.PastDepositRevealedEvents(filter) + if err != nil { + return []*tbtc.DepositRevealedEvent{}, nil + } + return events, nil +} + +func (ralc *reservationAcceptanceLocalChain) ReservationParameters() ( + *tbtc.ReservationParameters, + error, +) { + return ralc.reservationParameters, nil +} + +func (ralc *reservationAcceptanceLocalChain) ReservationCaps() ( + uint64, + uint64, + error, +) { + return ralc.maxPerWalletAmount, ralc.maxSingleAmount, nil +} + +func (ralc *reservationAcceptanceLocalChain) WalletReservationsAmount( + walletPublicKeyHash [20]byte, +) (uint64, error) { + return ralc.walletReservationsAmount, nil +} + +func (ralc *reservationAcceptanceLocalChain) WalletReservationsCount( + walletPublicKeyHash [20]byte, +) (uint32, error) { + return ralc.walletReservationsCount, nil +} + +func (ralc *reservationAcceptanceLocalChain) ActiveReservationsCount() ( + uint32, + uint32, + error, +) { + return ralc.activeCount, ralc.maxActive, nil +} + +func (ralc *reservationAcceptanceLocalChain) PendingReservedDeposits() ( + uint64, + error, +) { + return ralc.pendingReserved, nil +} + +func (ralc *reservationAcceptanceLocalChain) IsReservedDeposit( + depositKey *big.Int, +) (bool, error) { + if depositKey == nil { + return false, nil + } + return ralc.reservedDeposits[depositKey.Text(16)], nil +} + +func (ralc *reservationAcceptanceLocalChain) GetWallet( + walletPublicKeyHash [20]byte, +) (*tbtc.WalletChainData, error) { + if ralc.getWalletErr != nil { + return nil, ralc.getWalletErr + } + return ralc.LocalChain.GetWallet(walletPublicKeyHash) +} + +func (ralc *reservationAcceptanceLocalChain) ValidateReservationAnchorProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.ReservationAnchorProposal, + depositExtraInfo struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, +) error { + return ralc.validateErr +} + +func (ralc *reservationAcceptanceLocalChain) PastReservationAcceptanceRequestedEvents( + filter *tbtc.ReservationAcceptanceRequestedEventFilter, +) ([]*tbtc.ReservationAcceptanceRequestedEvent, error) { + if ralc.acceptanceEventsErr != nil { + return nil, ralc.acceptanceEventsErr + } + var results []*tbtc.ReservationAcceptanceRequestedEvent + for _, event := range ralc.acceptanceEvents { + if filter != nil && len(filter.ReservationKey) > 0 { + match := false + for _, k := range filter.ReservationKey { + if k != nil && event.ReservationKey != nil && k.Cmp(event.ReservationKey) == 0 { + match = true + break + } + } + if !match { + continue + } + } + results = append(results, event) + } + return results, nil +} + +func (ralc *reservationAcceptanceLocalChain) AddPastReservationAcceptanceRequestedEvent( + event *tbtc.ReservationAcceptanceRequestedEvent, +) { + ralc.acceptanceEvents = append(ralc.acceptanceEvents, event) +} + +// scenarioReservationAcceptanceChain wires a scenario's on-chain state +// into the test mock chain. +func scenarioReservationAcceptanceChain( + t *testing.T, + scenario *test.ReservationAcceptanceTestScenario, +) *reservationAcceptanceLocalChain { + t.Helper() + + ralc := newReservationAcceptanceLocalChain() + + var reservationVault chain.Address + if len(scenario.ReservationVault) > 0 { + reservationVault = chain.Address(scenario.ReservationVault) + } + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: reservationVault, + ReservationMinAmount: scenario.ReservationParameters.ReservationMinAmount, + ReservationTxMaxFee: scenario.ReservationParameters.ReservationTxMaxFee, + ReservationMaxTotalAmount: scenario.ReservationParameters.ReservationMaxTotalAmount, + ReservationTotalAmount: scenario.ReservationParameters.ReservationTotalAmount, + MaxReservationsPerWallet: scenario.ReservationParameters.MaxReservationsPerWallet, + } + + ralc.maxPerWalletAmount = scenario.Caps.MaxReservationsAmountPerWallet + ralc.maxSingleAmount = scenario.Caps.ReservationMaxSingleAmount + ralc.walletReservationsAmount = scenario.WalletCustody.Amount + ralc.walletReservationsCount = scenario.WalletCustody.Count + ralc.activeCount = scenario.Global.ActiveCount + ralc.maxActive = scenario.Global.MaxActive + ralc.pendingReserved = scenario.PendingReservedDeposits + + ralc.SetDepositMinAge(scenario.ChainParameters.DepositMinAge) + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(scenario.ChainParameters.CurrentBlock) + ralc.SetBlockCounter(blockCounter) + + // Map a WalletState string back to the tbtc constant. + var walletState tbtc.WalletState + switch scenario.WalletState { + case "Live": + walletState = tbtc.StateLive + case "Closing": + walletState = tbtc.StateClosing + case "Closed": + walletState = tbtc.StateClosed + case "Terminated": + walletState = tbtc.StateTerminated + default: + walletState = tbtc.StateLive + } + + ralc.SetWallet( + scenario.WalletPublicKeyHash, + &tbtc.WalletChainData{State: walletState}, + ) + + return ralc +} + +// registerReservedDeposits wires the scenario's reserved deposits into the +// mock chain as deposit requests and past DepositRevealedEvents. It also +// marks them as reserved via IsReservedDeposit. Bitcoin transaction +// registrations live on the btcChain mock. +func registerReservedDeposits( + t *testing.T, + scenario *test.ReservationAcceptanceTestScenario, + ralc *reservationAcceptanceLocalChain, + btcChain *tbtcpg.LocalBitcoinChain, +) { + t.Helper() + + // Configure the fee oracle rate. proposeReservationAcceptance now + // estimates the anchor fee dynamically (see estimateReservationAcceptanceFee); + // 1 sat/vByte hits the applyWalletTxFeeFloor minimum, matching the + // convention used by the sibling reservation re-anchor test fixtures. + btcChain.SetEstimateSatPerVByteFee(1, 1) + + filterStartBlock := uint64(0) + if scenario.ChainParameters.CurrentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = scenario.ChainParameters.CurrentBlock - + tbtcpg.ReservationAcceptanceLookBackBlocks + } + + for _, rd := range scenario.ReservedDeposits { + materialized, err := rd.Materialize() + if err != nil { + t.Fatalf( + "failed to materialize reserved deposit scenario row: [%v]", + err, + ) + } + + ralc.SetDepositRequest( + materialized.FundingTxHash, + materialized.FundingOutputIndex, + &tbtc.DepositChainRequest{ + Depositor: chain.Address(rd.Depositor), + Amount: rd.Amount, + RevealedAt: materialized.RevealedAt, + SweptAt: materialized.SweptAt, + Vault: materialized.Vault, + }, + ) + + if materialized.FundingTx != nil { + btcChain.SetTransaction( + materialized.FundingTxHash, + materialized.FundingTx, + ) + } else { + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction( + materialized.FundingTxHash, + dummyTx, + ) + } + btcChain.SetTransactionConfirmations( + materialized.FundingTxHash, + rd.FundingTxConfirmations, + ) + + currentBlock := scenario.ChainParameters.CurrentBlock + err = ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{materialized.WalletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: materialized.RevealBlock, + WalletPublicKeyHash: materialized.WalletPublicKeyHash, + FundingTxHash: materialized.FundingTxHash, + FundingOutputIndex: materialized.FundingOutputIndex, + Vault: materialized.Vault, + }, + ) + if err != nil { + t.Fatalf( + "failed to register past deposit revealed event: [%v]", + err, + ) + } + + depositKey := ralc.BuildDepositKey( + materialized.FundingTxHash, + materialized.FundingOutputIndex, + ) + ralc.reservedDeposits[depositKey.Text(16)] = true + } +} + +// expectedAnchorsEqual compares two proposal objects field-by-field. +// deep.Equal cannot be used for this: by default it does not descend into +// unexported fields, and *big.Int's representation is entirely unexported, +// so it silently reports "no difference" for any two distinct AnchorTxFee +// values. AnchorTxFee therefore needs an explicit .Cmp(). +func expectedAnchorsEqual( + expected, actual *tbtc.ReservationAnchorProposal, +) bool { + if expected == nil && actual == nil { + return true + } + if expected == nil || actual == nil { + return false + } + + if expected.DepositFundingTxHash != actual.DepositFundingTxHash { + return false + } + if expected.DepositFundingOutputIndex != actual.DepositFundingOutputIndex { + return false + } + if expected.RequestNonce != actual.RequestNonce { + return false + } + if expected.AnchorTxFee == nil || actual.AnchorTxFee == nil { + return expected.AnchorTxFee == actual.AnchorTxFee + } + return expected.AnchorTxFee.Cmp(actual.AnchorTxFee) == 0 +} + +func TestReservationAcceptanceLookBackBlocks(t *testing.T) { + expectedValue := uint64(216000) + + if tbtcpg.ReservationAcceptanceLookBackBlocks != expectedValue { + t.Errorf( + "unexpected ReservationAcceptanceLookBackBlocks\n"+ + "expected: %d\n"+ + "actual: %d", + expectedValue, + tbtcpg.ReservationAcceptanceLookBackBlocks, + ) + } +} + +func TestReservationAcceptanceTask_ActionType(t *testing.T) { + task := tbtcpg.NewReservationAcceptanceTask( + newReservationAcceptanceLocalChain(), + tbtcpg.NewLocalBitcoinChain(), + ) + if task.ActionType() != tbtc.ActionReservationAnchor { + t.Errorf( + "unexpected action type\n"+ + "expected: %v\n"+ + "actual: %v", + tbtc.ActionReservationAnchor, + task.ActionType(), + ) + } +} + +func TestReservationAcceptanceTask_Run(t *testing.T) { + if err := log.SetLogLevel("*", "DEBUG"); err != nil { + t.Fatal(err) + } + + scenarios, err := test.LoadReservationAcceptanceTestScenario() + if err != nil { + t.Fatal(err) + } + + for _, scenario := range scenarios { + t.Run(scenario.Title, func(t *testing.T) { + ralc := scenarioReservationAcceptanceChain(t, scenario) + btcChain := tbtcpg.NewLocalBitcoinChain() + registerReservedDeposits(t, scenario, ralc, btcChain) + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: scenario.WalletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + if scenario.ExpectedErr == nil { + t.Fatalf("unexpected error: [%v]", err) + } + if scenario.ExpectedErr.Error() != err.Error() { + t.Fatalf( + "unexpected error message\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + scenario.ExpectedErr, + err, + ) + } + return + } + if scenario.ExpectedErr != nil { + t.Fatalf("expected error [%v], got nil", scenario.ExpectedErr) + } + + expectedProposal := scenario.ExpectedAnchorProposal + + if expectedProposal == nil { + if shouldExecute { + t.Errorf( + "unexpected proposal returned when none expected", + ) + } + if proposal != nil { + t.Errorf( + "expected nil proposal, got [%+v]", + proposal, + ) + } + return + } + + if !shouldExecute { + t.Errorf("expected shouldExecute=true, got false") + } + if proposal == nil { + t.Fatal("expected proposal, got nil") + } + + actualProposal, ok := proposal.(*tbtc.ReservationAnchorProposal) + if !ok { + t.Fatalf("expected *ReservationAnchorProposal, got %T", proposal) + } + + if !expectedAnchorsEqual(expectedProposal, actualProposal) { + t.Errorf( + "invalid anchor proposal\nexpected: %+v\nactual: %+v", + expectedProposal, + actualProposal, + ) + } + }) + } +} + +// TestReservationAcceptanceTask_NoCandidates verifies that the task is a +// no-op when the chain has no reserved deposits. +func TestReservationAcceptanceTask_NoCandidates(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + } + ralc.maxPerWalletAmount = 1000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + if shouldExecute { + t.Errorf("expected shouldExecute=false, got true") + } + if proposal != nil { + t.Errorf("expected nil proposal, got [%+v]", proposal) + } +} + +// TestReservationAcceptanceTask_BoundedLookback verifies that the bounded +// look-back window is applied when the current block exceeds it. +func TestReservationAcceptanceTask_BoundedLookback(t *testing.T) { + currentBlock := uint64(400000) + expectedStartBlock := currentBlock - + tbtcpg.ReservationAcceptanceLookBackBlocks + + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + // Event below the look-back start block must NOT be returned. + oldFundingTxHash := hashFromString( + "1111111111111111111111111111111111111111111111111111111111111111", + ) + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: 0, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 1, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: oldFundingTxHash, + FundingOutputIndex: 0, + }, + ); err != nil { + t.Fatal(err) + } + + // Event at the look-back start block must be returned. Mark it as + // reserved and provide a deposit request. + fundingTxHash := hashFromString( + "2222222222222222222222222222222222222222222222222222222222222222", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + depositKey := ralc.BuildDepositKey(fundingTxHash, 0) + ralc.reservedDeposits[depositKey.Text(16)] = true + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: expectedStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: expectedStartBlock, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + if !shouldExecute { + t.Fatalf("expected shouldExecute=true, got false") + } + if proposal == nil { + t.Fatalf("expected proposal, got nil") + } + actualProposal, ok := proposal.(*tbtc.ReservationAnchorProposal) + if !ok { + t.Fatalf("expected *ReservationAnchorProposal, got %T", proposal) + } + if actualProposal.DepositFundingTxHash != fundingTxHash { + t.Errorf( + "unexpected deposit funding tx hash\n"+ + "expected: %s\n"+ + "actual: %s", + fundingTxHash.Hex(bitcoin.ReversedByteOrder), + actualProposal.DepositFundingTxHash.Hex( + bitcoin.ReversedByteOrder, + ), + ) + } +} + +// TestReservationAcceptanceTask_DepositNotReserved confirms that a deposit +// that does not target the reservation vault is filtered out. +func TestReservationAcceptanceTask_DepositNotReserved(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "3333333333333333333333333333333333333333333333333333333333333333", + ) + btcChain.SetTransaction(fundingTxHash, &bitcoin.Transaction{}) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + // Set event vault away from the configured reservation vault so it + // actually exercises "not reserved". + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xOtherVaultAddress1234567890abcdef12345678901234", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + if shouldExecute { + t.Errorf("expected shouldExecute=false, got true") + } + if proposal != nil { + t.Errorf("expected no proposal for non-reserved deposit, got %v", proposal) + } +} + +// TestReservationAcceptanceTask_GetWalletError exercises the GetWallet +// error passthrough inside findReservationAcceptanceCandidate: a +// reserved deposit candidate is discovered and matches the reservation +// vault, but the candidate wallet's chain data fails to load. +func TestReservationAcceptanceTask_GetWalletError(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + // No SetWallet call: GetWallet fails for the candidate wallet, and + // getWalletErr forces the exact error to assert against. + ralc.getWalletErr = fmt.Errorf("boom") + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "4444444444444444444444444444444444444444444444444444444444444444", + ) + btcChain.SetTransaction(fundingTxHash, &bitcoin.Transaction{}) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + depositKey := ralc.BuildDepositKey(fundingTxHash, 0) + ralc.reservedDeposits[depositKey.Text(16)] = true + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 1, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + + proposal, shouldExecute, err := task.Run(&tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if shouldExecute { + t.Errorf("expected shouldExecute=false, got true") + } + if proposal != nil { + t.Errorf("expected no proposal, got %v", proposal) + } +} + +// TestReservationAcceptanceTask_Stateless_Maturity verifies the stateless +// observable contract across two consecutive Run calls on the same task instance: +// an immature candidate is skipped on the first run, but when time advances and +// the candidate matures, the second run on the same task instance proposes it +// without any cache-state interference. +func TestReservationAcceptanceTask_Stateless_Maturity(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "5555555555555555555555555555555555555555555555555555555555555555", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + // Candidate revealed only 10 minutes ago (depositMinAge is 1 hour). + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + Amount: 2000000, + RevealedAt: time.Now().Add(-10 * time.Minute), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + // First run: deposit is immature, should not be proposed. + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("first run error: [%v]", err) + } + if shouldExecute || proposal != nil { + t.Fatalf("expected no proposal on first run for immature deposit") + } + + // Advance deposit age (simulating passage of time to 2 hours ago). + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + // Second run on the same task instance: deposit is now mature and proposed. + proposal, shouldExecute, err = task.Run(request) + if err != nil { + t.Fatalf("second run error: [%v]", err) + } + if !shouldExecute || proposal == nil { + t.Fatalf("expected proposal on second run after deposit matured") + } + + actualProposal, ok := proposal.(*tbtc.ReservationAnchorProposal) + if !ok { + t.Fatalf("expected *ReservationAnchorProposal, got %T", proposal) + } + if actualProposal.DepositFundingTxHash != fundingTxHash { + t.Errorf( + "unexpected deposit funding tx hash\nexpected: %s\nactual: %s", + fundingTxHash.Hex(bitcoin.ReversedByteOrder), + actualProposal.DepositFundingTxHash.Hex(bitcoin.ReversedByteOrder), + ) + } +} + +// TestReservationAcceptanceTask_Stateless_NoReRequest verifies that once a +// reservation has an existing acceptance requested event, subsequent Run calls +// on the same task instance do not produce a duplicate acceptance proposal. +func TestReservationAcceptanceTask_Stateless_NoReRequest(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "6666666666666666666666666666666666666666666666666666666666666666", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + // First run: deposit is eligible and proposed. + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("first run error: [%v]", err) + } + if !shouldExecute || proposal == nil { + t.Fatalf("expected proposal on first run") + } + + // Simulate on-chain record: mark reservation as having an acceptance + // requested event. + depositKey := ralc.BuildDepositKey(fundingTxHash, 0) + ralc.AddPastReservationAcceptanceRequestedEvent(&tbtc.ReservationAcceptanceRequestedEvent{ + ReservationKey: depositKey, + RequestNonce: 1, + WalletPublicKeyHash: walletPublicKeyHash, + }) + + // Second run on the same task instance: must not produce a second proposal. + proposal, shouldExecute, err = task.Run(request) + if err != nil { + t.Fatalf("second run error: [%v]", err) + } + if shouldExecute || proposal != nil { + t.Fatalf("expected no proposal on second run due to existing acceptance event") + } +} + +// TestReservationAcceptanceTask_Stateless_PastEventsError verifies that an +// RPC failure querying past acceptance requested events fails closed (skips candidate). +func TestReservationAcceptanceTask_Stateless_PastEventsError(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "7777777777777777777777777777777777777777777777777777777777777777", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + // Force an error on PastReservationAcceptanceRequestedEvents. + ralc.acceptanceEventsErr = fmt.Errorf("rpc failure") + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("unexpected task error: [%v]", err) + } + if shouldExecute || proposal != nil { + t.Fatalf("expected candidate to be skipped when past events check fails closed") + } +} + +// TestReservationAcceptanceTask_Stateless_NonEligibleReservationState verifies that +// a reservation whose on-chain state is Active, ActionPending, Closed, or Stranded +// is skipped from acceptance proposals. +func TestReservationAcceptanceTask_Stateless_NonEligibleReservationState(t *testing.T) { + nonEligibleStates := []tbtc.ReservationState{ + tbtc.ReservationStateActive, + tbtc.ReservationStateActionPending, + tbtc.ReservationStateClosed, + tbtc.ReservationStateStranded, + } + + for _, state := range nonEligibleStates { + t.Run(fmt.Sprintf("state_%v", state), func(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "8888888888888888888888888888888888888888888888888888888888888888", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + depositKey := ralc.BuildDepositKey(fundingTxHash, 0) + ralc.SetReservation(depositKey, &tbtc.Reservation{ + State: state, + RequestNonce: 1, + }) + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("unexpected task error: [%v]", err) + } + if shouldExecute || proposal != nil { + t.Fatalf("expected candidate with state %v to be skipped", state) + } + }) + } +} + +// TestReservationAcceptanceTask_Stateless_DynamicMinAmount verifies that the +// minimum-amount filter is retryable: a deposit below minimum on Run 1 is skipped, +// but when governance lowers the minimum amount, Run 2 on the same task instance +// proposes it. +func TestReservationAcceptanceTask_Stateless_DynamicMinAmount(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + // Initial min amount is 5,000,000. + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 5000000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 50000000 + ralc.maxSingleAmount = 50000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "9999999999999999999999999999999999999999999999999999999999999999", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + // Deposit amount is 2,000,000 (below initial 5,000,000 min). + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + // First run: deposit amount is below min, skipped. + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("first run error: [%v]", err) + } + if shouldExecute || proposal != nil { + t.Fatalf("expected no proposal when deposit is below min amount") + } + + // Governance lowers min amount to 1,000,000. + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + + // Second run on the same task instance: deposit is now above min and proposed. + proposal, shouldExecute, err = task.Run(request) + if err != nil { + t.Fatalf("second run error: [%v]", err) + } + if !shouldExecute || proposal == nil { + t.Fatalf("expected proposal on second run after min amount lowered") + } +} + +// TestReservationAcceptanceTask_Stateless_RequestNonceIncremented verifies that +// when an existing reservation record has RequestNonce = N, the generated proposal +// uses RequestNonce = N + 1. +func TestReservationAcceptanceTask_Stateless_RequestNonceIncremented(t *testing.T) { + ralc := newReservationAcceptanceLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + walletPublicKeyHash := hexToByte20( + "8db50eb52063ea9d98b3eac91489a90f738986f6", + ) + + ralc.reservationParameters = &tbtc.ReservationParameters{ + ReservationVault: chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + ), + ReservationMinAmount: 1000, + ReservationTxMaxFee: 5000, + MaxReservationsPerWallet: 5, + } + ralc.maxPerWalletAmount = 5000000 + ralc.maxSingleAmount = 5000000 + ralc.maxActive = 100 + + ralc.SetDepositMinAge(3600) + ralc.SetWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{State: tbtc.StateLive}, + ) + + currentBlock := uint64(300000) + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + ralc.SetBlockCounter(blockCounter) + + fundingTxHash := hashFromString( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + dummyTx := &bitcoin.Transaction{ + Outputs: []*bitcoin.TransactionOutput{{ + Value: 0, + PublicKeyScript: append([]byte{0x00, 0x20}, make([]byte, 32)...), + }}, + } + btcChain.SetTransaction(fundingTxHash, dummyTx) + btcChain.SetEstimateSatPerVByteFee(1, 1) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + ralc.SetDepositRequest( + fundingTxHash, + 0, + &tbtc.DepositChainRequest{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + Amount: 2000000, + RevealedAt: time.Now().Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ) + + filterStartBlock := uint64(0) + if currentBlock > tbtcpg.ReservationAcceptanceLookBackBlocks { + filterStartBlock = currentBlock - tbtcpg.ReservationAcceptanceLookBackBlocks + } + + if err := ralc.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + EndBlock: ¤tBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 290000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 0, + Vault: &[]chain.Address{chain.Address( + "0xReservationVaultAddress1234567890abcdef12345678", + )}[0], + }, + ); err != nil { + t.Fatal(err) + } + + depositKey := ralc.BuildDepositKey(fundingTxHash, 0) + // Set an existing reservation with StateUnknown (not active) and RequestNonce = 2. + ralc.SetReservation(depositKey, &tbtc.Reservation{ + State: tbtc.ReservationStateUnknown, + RequestNonce: 2, + }) + // Set previous action nonce 2 to TimedOut so hasPendingAction returns false. + ralc.SetReservationAction(depositKey, 2, &tbtc.ReservationAction{ + State: tbtc.ReservationActionStateTimedOut, + }) + + task := tbtcpg.NewReservationAcceptanceTask(ralc, btcChain) + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + proposal, shouldExecute, err := task.Run(request) + if err != nil { + t.Fatalf("task error: [%v]", err) + } + if !shouldExecute || proposal == nil { + t.Fatalf("expected proposal") + } + + actualProposal, ok := proposal.(*tbtc.ReservationAnchorProposal) + if !ok { + t.Fatalf("expected *ReservationAnchorProposal, got %T", proposal) + } + if actualProposal.RequestNonce != 3 { + t.Errorf( + "unexpected RequestNonce\nexpected: 3\nactual: %d", + actualProposal.RequestNonce, + ) + } +} diff --git a/pkg/tbtcpg/reservation_reanchor.go b/pkg/tbtcpg/reservation_reanchor.go new file mode 100644 index 0000000000..2ee6959dbd --- /dev/null +++ b/pkg/tbtcpg/reservation_reanchor.go @@ -0,0 +1,472 @@ +package tbtcpg + +import ( + "fmt" + "math/big" + + "github.com/ipfs/go-log/v2" + "go.uber.org/zap" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// ReservationReanchorLookBackBlocks is the look-back period in blocks used +// when searching for submitted reservation-related events. It is equal to +// 30 days assuming 12 seconds per block. +const ReservationReanchorLookBackBlocks = uint64(216000) + +// ReservationReanchorTask is a task that may produce a reservation re-anchor +// proposal. The wallet enters this task when the source wallet has begun a +// move to a new wallet (state StateMovingFunds) or when the source wallet's +// main UTXO has dropped below the moving funds dust threshold (below-dust +// re-anchor). For every reservation currently custodied by the wallet, the +// task picks a destination wallet and assembles a 1-input-1-output re-anchor +// transaction moving the anchor outpoint into that destination wallet. +type ReservationReanchorTask struct { + chain Chain + btcChain bitcoin.Chain +} + +// NewReservationReanchorTask returns a new ReservationReanchorTask bound to +// the given tbtc and Bitcoin chains. +func NewReservationReanchorTask( + chain Chain, + btcChain bitcoin.Chain, +) *ReservationReanchorTask { + return &ReservationReanchorTask{ + chain: chain, + btcChain: btcChain, + } +} + +// ActionType returns the type of wallet action this task produces. +func (rrt *ReservationReanchorTask) ActionType() tbtc.WalletActionType { + return tbtc.ActionReservationReanchor +} + +// Run evaluates whether the given wallet needs to re-anchor any of its +// reservations and returns a single ReservationReanchorProposal for the first +// reservation found to be re-anchorable. A wallet is a candidate for re-anchor +// when either: +// - it has entered the StateMovingFunds state (the wallet is migrating and +// reservations must be released to a live wallet), or +// - its main UTXO has dropped below the moving funds dust threshold (a +// re-anchor frees value from the wallet's main UTXO pool into a fresh +// reservation anchor held by another live wallet). +// +// Returns (nil, false, nil) when no reservation is eligible; callers should +// treat that as a benign no-op for the coordination window. +func (rrt *ReservationReanchorTask) Run( + request *tbtc.CoordinationProposalRequest, +) ( + tbtc.CoordinationProposal, + bool, + error, +) { + walletPublicKeyHash := request.WalletPublicKeyHash + + taskLogger := logger.With( + zap.String("task", rrt.ActionType().String()), + zap.String("walletPKH", fmt.Sprintf("0x%x", walletPublicKeyHash)), + ) + + walletChainData, err := rrt.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return nil, false, fmt.Errorf( + "cannot get wallet chain data: [%w]", + err, + ) + } + + migrating := walletChainData.State == tbtc.StateMovingFunds + if !migrating { + // Check the below-dust trigger. Re-anchor also unlocks wallet value + // when the main UTXO has fallen below the moving funds dust + // threshold, so wallets in StateLive may still need to re-anchor + // before they enter the moving funds flow. + needs, err := rrt.isBelowMovingFundsDustThreshold( + taskLogger, + walletPublicKeyHash, + ) + if err != nil { + return nil, false, err + } + + if !needs { + taskLogger.Info("wallet is not eligible for reservation re-anchor") + return nil, false, nil + } + } + + reservationKeys, err := rrt.chain.WalletReservations(walletPublicKeyHash) + if err != nil { + return nil, false, fmt.Errorf( + "cannot list wallet reservations: [%w]", + err, + ) + } + + if len(reservationKeys) == 0 { + taskLogger.Info("wallet has no reservations to re-anchor") + return nil, false, nil + } + + liveWalletsCount, err := rrt.chain.GetLiveWalletsCount() + if err != nil { + return nil, false, fmt.Errorf( + "cannot get live wallets count: [%w]", + err, + ) + } + + if liveWalletsCount == 0 { + taskLogger.Info("no live wallets available for re-anchor target") + return nil, false, nil + } + + targetWalletPublicKeyHash, err := rrt.findTargetWallet( + taskLogger, + walletPublicKeyHash, + ) + if err != nil { + taskLogger.Errorf( + "cannot pick re-anchor target wallet: [%v]", + err, + ) + return nil, false, nil + } + + for _, reservationKey := range reservationKeys { + reservation, err := rrt.chain.GetReservation(reservationKey) + if err != nil { + taskLogger.Errorf( + "cannot get reservation [0x%x]: [%v]", + reservationKey, + err, + ) + continue + } + + // Filter out reservations that are not in the Active state. + // Note: Checking Active state already covers pending actions, + // because a reservation with a pending action is in ActionPending state. + if reservation.State != tbtc.ReservationStateActive { + taskLogger.Infof( + "reservation [0x%x] not in Active state (state=%v), skipping", + reservationKey, + reservation.State, + ) + continue + } + + proposal, err := rrt.ProposeReservationReanchor( + taskLogger, + walletPublicKeyHash, + reservationKey, + reservation.RequestNonce+1, + targetWalletPublicKeyHash, + 0, + ) + if err != nil { + taskLogger.Errorf( + "cannot prepare reservation re-anchor proposal: [%v]", + err, + ) + continue + } + + return proposal, true, nil + } + + taskLogger.Info("no reservations eligible for re-anchor") + return nil, false, nil +} + +// ProposeReservationReanchor assembles a single reservation re-anchor proposal +// for the given reservation, targeting the given wallet. The supplied fee may +// be 0 to trigger on-chain-driven fee estimation; the caller is responsible +// for providing a RequestNonce that is exactly current_request_nonce + 1 on +// the reservation's view (the action generation being authorized). +func (rrt *ReservationReanchorTask) ProposeReservationReanchor( + taskLogger log.StandardLogger, + sourceWalletPublicKeyHash [20]byte, + reservationKey *big.Int, + requestNonce uint64, + targetWalletPublicKeyHash [20]byte, + fee int64, +) (*tbtc.ReservationReanchorProposal, error) { + if reservationKey == nil { + return nil, fmt.Errorf("reservation key is required") + } + if requestNonce == 0 { + return nil, fmt.Errorf("request nonce must be > 0") + } + if targetWalletPublicKeyHash == [20]byte{} { + return nil, fmt.Errorf("target wallet public key hash is required") + } + + taskLogger.Infof( + "preparing a reservation re-anchor proposal for reservation [0x%x]", + reservationKey, + ) + + reservation, err := rrt.chain.GetReservation(reservationKey) + if err != nil { + return nil, fmt.Errorf( + "cannot get reservation [0x%x]: [%w]", + reservationKey, + err, + ) + } + + // convertReservationFromAbiType (the Go-side chain adapter) always + // allocates a non-nil AnchorUtxo, populated with zero hash/value when + // no anchor exists on-chain, so a bare nil check can never fire against + // the production chain. Detect the unset case by value instead. + if reservation.AnchorUtxo == nil || + reservation.AnchorUtxo.Value == 0 || + reservation.AnchorUtxo.Outpoint == nil || + reservation.AnchorUtxo.Outpoint.TransactionHash == (bitcoin.Hash{}) { + return nil, fmt.Errorf( + "reservation [0x%x] has no anchor UTXO", + reservationKey, + ) + } + + // Estimate fee if it's missing. The Bridge caps each reservation + // lifecycle transaction with its own ReservationTxMaxFee, not the + // moving-funds TxMaxTotalFee, so we use the reservation parameters + // directly. + if fee <= 0 { + taskLogger.Infof("estimating reservation re-anchor transaction fee") + + params, err := rrt.chain.ReservationParameters() + if err != nil { + return nil, fmt.Errorf( + "cannot get reservation parameters: [%w]", + err, + ) + } + + fee, err = estimateReservationReanchorFee( + rrt.btcChain, + params.ReservationTxMaxFee, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot estimate reservation re-anchor transaction fee: [%w]", + err, + ) + } + } + + taskLogger.Infof("reservation re-anchor transaction fee: [%d]", fee) + + proposal := &tbtc.ReservationReanchorProposal{ + ReservationKey: new(big.Int).Set(reservationKey), + RequestNonce: requestNonce, + TargetWalletPublicKeyHash: targetWalletPublicKeyHash, + ReanchorTxFee: big.NewInt(fee), + } + + if err := rrt.chain.ValidateReservationReanchorProposal( + sourceWalletPublicKeyHash, + proposal, + ); err != nil { + return nil, fmt.Errorf( + "failed to verify reservation re-anchor proposal: [%w]", + err, + ) + } + // The re-anchor request generation must be authorized on-chain. + // Note: Calling RequestReservationReanchor during proposal generation is an + // accepted deviation from the read-only-during-generation pattern, with + // precedent in MovingFundsTask's SubmitMovingFundsCommitment. + if err := rrt.chain.RequestReservationReanchor( + reservationKey, + targetWalletPublicKeyHash, + ); err != nil { + return nil, fmt.Errorf("cannot request reservation re-anchor: [%v]", err) + } + + return proposal, nil +} + +// findTargetWallet picks a live destination wallet from the on-chain wallet +// registry, mirroring the moving funds target selection. The new wallet must +// be in StateLive and must not be the source wallet itself. The registration +// scan is bounded to ReservationReanchorLookBackBlocks (mirroring the other +// look-back scans in this package) rather than the full chain history: a +// live wallet must have registered recently, and an unbounded eth_getLogs +// scan on every re-anchor attempt does not. +func (rrt *ReservationReanchorTask) findTargetWallet( + taskLogger log.StandardLogger, + sourceWalletPublicKeyHash [20]byte, +) ([20]byte, error) { + blockCounter, err := rrt.chain.BlockCounter() + if err != nil { + return [20]byte{}, fmt.Errorf("failed to get block counter: [%v]", err) + } + + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return [20]byte{}, fmt.Errorf("failed to get current block: [%v]", err) + } + + startBlock := uint64(0) + if currentBlock > ReservationReanchorLookBackBlocks { + startBlock = currentBlock - ReservationReanchorLookBackBlocks + } + + events, err := rrt.chain.PastNewWalletRegisteredEvents( + &tbtc.NewWalletRegisteredEventFilter{StartBlock: startBlock}, + ) + if err != nil { + return [20]byte{}, fmt.Errorf( + "failed to get past new wallet registered events: [%v]", + err, + ) + } + + for i := len(events) - 1; i >= 0; i-- { + walletPubKeyHash := events[i].WalletPublicKeyHash + if walletPubKeyHash == sourceWalletPublicKeyHash { + continue + } + + wallet, err := rrt.chain.GetWallet(walletPubKeyHash) + if err != nil { + taskLogger.Errorf( + "failed to get wallet data for wallet with PKH [0x%x]: [%v]", + walletPubKeyHash, + err, + ) + continue + } + + if wallet.State == tbtc.StateLive { + return walletPubKeyHash, nil + } + } + + return [20]byte{}, fmt.Errorf("no live wallet available for re-anchor target") +} + +// isBelowMovingFundsDustThreshold returns true when the wallet's main UTXO +// value is below the moving funds dust threshold. The threshold is sourced +// from the on-chain MovingFundsParameters. A wallet without a main UTXO is +// considered to have fallen below the threshold. +func (rrt *ReservationReanchorTask) isBelowMovingFundsDustThreshold( + taskLogger log.StandardLogger, + walletPublicKeyHash [20]byte, +) (bool, error) { + params, err := rrt.chain.GetMovingFundsParameters() + if err != nil { + return false, fmt.Errorf( + "cannot get moving funds parameters: [%w]", + err, + ) + } + + walletChainData, err := rrt.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return false, fmt.Errorf( + "cannot get wallet chain data: [%w]", + err, + ) + } + + if walletChainData.MainUtxoHash == [32]byte{} { + // No main UTXO on-chain, the wallet has fully depleted its pool and + // must release any reservation anchors. + taskLogger.Info("wallet has no main UTXO; below dust threshold") + return true, nil + } + + walletMainUtxo, err := tbtc.DetermineWalletMainUtxo( + walletPublicKeyHash, + rrt.chain, + rrt.btcChain, + ) + if err != nil { + return false, fmt.Errorf( + "cannot determine wallet main UTXO: [%w]", + err, + ) + } + + if walletMainUtxo == nil { + taskLogger.Info("wallet has no resolvable main UTXO; below dust threshold") + return true, nil + } + + below := walletMainUtxo.Value < int64(params.DustThreshold) + if below { + taskLogger.Infof( + "wallet main UTXO value [%d] below moving funds dust threshold [%d]", + walletMainUtxo.Value, + params.DustThreshold, + ) + } + return below, nil +} + +// hasPendingAction reports whether the on-chain reservation action +// generation at the reservation's current request nonce (if any) is in a +// pending state. This guards against duplicate re-anchor requests: the +// Bridge rejects a new request while the previous generation is still in +// flight. The caller supplies the reservation record it already fetched +// (see Run) rather than this function re-reading it. +func hasPendingAction( + reservationKey *big.Int, + reservation *tbtc.Reservation, + chain Chain, + taskLogger log.StandardLogger, +) bool { + if reservation.RequestNonce == 0 { + return false + } + + action, err := chain.GetReservationAction( + reservationKey, + reservation.RequestNonce, + ) + if err != nil { + // Fail safe: a lookup error is indistinguishable from "still + // pending" here, and treating it as not-pending would let the + // caller send a duplicate re-anchor request that the Bridge + // rejects while a real pending generation is in flight. Skip this + // reservation for the current coordination window instead; the + // next window retries. + taskLogger.Errorf( + "cannot get reservation action for [0x%x] nonce [%d]: [%v]", + reservationKey, + reservation.RequestNonce, + err, + ) + return true + } + + return action.State == tbtc.ReservationActionStatePending +} + +// estimateReservationReanchorFee estimates the fee for a reservation +// re-anchor transaction. The transaction has one P2WPKH input (the +// reservation anchor) and one P2WPKH output (the new anchor under the +// target wallet), so its virtual size is fixed for any single re-anchor. +func estimateReservationReanchorFee( + btcChain bitcoin.Chain, + txMaxFee uint64, +) (int64, error) { + sizeEstimator := bitcoin.NewTransactionSizeEstimator(). + AddPublicKeyHashInputs(1, true). + AddPublicKeyHashOutputs(1, true) + + return estimateReservationFixedSizeTxFee( + btcChain, + sizeEstimator, + txMaxFee, + "reservation re-anchor estimated fee exceeds the maximum fee", + ) +} diff --git a/pkg/tbtcpg/reservation_reanchor_test.go b/pkg/tbtcpg/reservation_reanchor_test.go new file mode 100644 index 0000000000..9c1a0b03ae --- /dev/null +++ b/pkg/tbtcpg/reservation_reanchor_test.go @@ -0,0 +1,558 @@ +package tbtcpg_test + +import ( + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" + "github.com/keep-network/keep-core/pkg/tbtcpg" + pkgtest "github.com/keep-network/keep-core/pkg/tbtcpg/internal/test" +) + +func TestReservationReanchorTask_Run(t *testing.T) { + scenarios, err := pkgtest.LoadReservationReanchorTestScenario() + if err != nil { + t.Fatal(err) + } + + for _, scenario := range scenarios { + t.Run(scenario.Title, func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + // findTargetWallet now bounds its wallet-registration scan to + // ReservationReanchorLookBackBlocks; a small current block keeps + // the computed StartBlock at 0, matching the filter used below. + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + tbtcChain.SetBlockCounter(blockCounter) + + mainUtxoHash := scenario.SourceWalletMainUtxoHashBytes + if scenario.SourceWalletMainUtxoTxHash != "" && + scenario.SourceWalletMainUtxoTxHash != "0000000000000000000000000000000000000000000000000000000000000000" { + walletScript, err := bitcoin.PayToWitnessPublicKeyHash( + scenario.SourceWalletPublicKeyHash, + ) + if err != nil { + t.Fatal(err) + } + + mainUtxoTx := &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: scenario.SourceWalletMainUtxoValue, + PublicKeyScript: walletScript, + }}, + } + // DetermineWalletMainUtxo builds the candidate outpoint from + // transaction.Hash() (the tx's own computed hash), not from + // whatever key it happens to be stored under - both must + // agree, so derive the storage key from the same call. + mainUtxoTxHash := mainUtxoTx.Hash() + btcChain.SetTransaction(mainUtxoTxHash, mainUtxoTx) + btcChain.SetTxHashesForPublicKeyHash( + scenario.SourceWalletPublicKeyHash, + []bitcoin.Hash{mainUtxoTxHash}, + ) + + mainUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: mainUtxoTxHash, + OutputIndex: scenario.SourceWalletMainUtxoTxIndex, + }, + Value: scenario.SourceWalletMainUtxoValue, + } + mainUtxoHash = tbtcChain.ComputeMainUtxoHash(mainUtxo) + } + + tbtcChain.SetWallet( + scenario.SourceWalletPublicKeyHash, + &tbtc.WalletChainData{ + State: scenario.SourceWalletState, + MainUtxoHash: mainUtxoHash, + }, + ) + + tbtcChain.SetMovingFundsParameters( + 1000000, + scenario.MovingFundsDustThreshold, + 0, + 0, + nil, + 0, + 0, + 0, + 0, + nil, + 0, + ) + + reservationKeys := make([]*big.Int, 0, len(scenario.Reservations)) + for _, r := range scenario.Reservations { + reservationKeys = append(reservationKeys, r.ReservationKey) + + anchorTxHash, err := bitcoin.NewHashFromString( + r.AnchorTxHash, + bitcoin.ReversedByteOrder, + ) + if err != nil { + t.Fatal(err) + } + + btcChain.SetTransaction(anchorTxHash, &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: r.AnchorValue, + PublicKeyScript: []byte{}, + }}, + }) + + reservationState := r.State + if r.HasPendingAction || r.PendingActionState == tbtc.ReservationActionStatePending { + // On-chain, a reservation with a pending action is in ActionPending state. + reservationState = tbtc.ReservationStateActionPending + } + + tbtcChain.SetReservation(r.ReservationKey, &tbtc.Reservation{ + WalletPublicKeyHash: r.WalletPublicKeyHash, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash, + OutputIndex: r.AnchorTxOutputIndex, + }, + Value: r.AnchorValue, + }, + State: reservationState, + RequestNonce: r.RequestNonce, + }) + + // Always install an action record when RequestNonce > 0 so + // hasPendingAction's real GetReservationAction lookup + // succeeds and evaluates State directly, matching what a + // real chain would have: RequestNonce only ever advances + // alongside a real action record. HasPendingAction=false + // scenarios use r.PendingActionState (a terminal state, not + // Pending) to model an already-settled prior generation. + if r.RequestNonce > 0 { + tbtcChain.SetReservationAction( + r.ReservationKey, + r.RequestNonce, + &tbtc.ReservationAction{ + ActionType: tbtc.ReservationActionTypeReanchor, + State: r.PendingActionState, + }, + ) + } + } + tbtcChain.SetWalletReservations( + scenario.SourceWalletPublicKeyHash, + reservationKeys, + ) + + tbtcChain.SetReservationParameters(tbtc.ReservationParameters{ + ReservationTxMaxFee: scenario.ReservationTxMaxFee, + }) + + btcChain.SetEstimateSatPerVByteFee(1, scenario.EstimateSatPerVByteFee) + + if scenario.TargetWalletPublicKeyHash != [20]byte{} { + err := tbtcChain.AddPastNewWalletRegisteredEvent( + &tbtc.NewWalletRegisteredEventFilter{StartBlock: 0}, + &tbtc.NewWalletRegisteredEvent{ + WalletPublicKeyHash: scenario.TargetWalletPublicKeyHash, + }, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.SetWallet( + scenario.TargetWalletPublicKeyHash, + &tbtc.WalletChainData{ + State: tbtc.StateLive, + }, + ) + } + + tbtcChain.SetLiveWalletsCount(scenario.LiveWalletsCount) + + task := tbtcpg.NewReservationReanchorTask(tbtcChain, btcChain) + + if scenario.ExpectedProposal != nil { + err := tbtcChain.SetReservationReanchorProposalValidationResult( + scenario.SourceWalletPublicKeyHash, + scenario.ExpectedProposal, + true, + ) + if err != nil { + t.Fatal(err) + } + } + + proposal, _, err := task.Run( + &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: scenario.SourceWalletPublicKeyHash, + }, + ) + + expectedErrStr := "" + if scenario.ExpectedErr != nil { + expectedErrStr = scenario.ExpectedErr.Error() + } + actualErrStr := "" + if err != nil { + actualErrStr = err.Error() + } + if expectedErrStr != actualErrStr { + t.Errorf( + "unexpected error\nexpected: %v\nactual: %v", + scenario.ExpectedErr, + err, + ) + } + + actualProposal, _ := proposal.(*tbtc.ReservationReanchorProposal) + + if !reanchorProposalsEqual(scenario.ExpectedProposal, actualProposal) { + t.Errorf( + "invalid reservation re-anchor proposal\n"+ + "expected: %+v\n"+ + "actual: %+v", + scenario.ExpectedProposal, + actualProposal, + ) + } + }) + } +} + +// reanchorProposalsEqual compares two proposals field-by-field. deep.Equal +// cannot be used here (or anywhere else the package compares a +// *tbtc.ReservationReanchorProposal/*tbtc.ReservationAnchorProposal): by +// default it does not descend into unexported fields, and *big.Int's +// representation is entirely unexported, so deep.Equal silently reports "no +// difference" for any two distinct *big.Int values. ReservationKey and +// ReanchorTxFee are both *big.Int, so they need an explicit .Cmp(). +func reanchorProposalsEqual( + expected, actual *tbtc.ReservationReanchorProposal, +) bool { + if expected == nil && actual == nil { + return true + } + if expected == nil || actual == nil { + return false + } + if (expected.ReservationKey == nil) != (actual.ReservationKey == nil) { + return false + } + if expected.ReservationKey != nil && + expected.ReservationKey.Cmp(actual.ReservationKey) != 0 { + return false + } + if expected.RequestNonce != actual.RequestNonce { + return false + } + if expected.TargetWalletPublicKeyHash != actual.TargetWalletPublicKeyHash { + return false + } + if (expected.ReanchorTxFee == nil) != (actual.ReanchorTxFee == nil) { + return false + } + if expected.ReanchorTxFee != nil && + expected.ReanchorTxFee.Cmp(actual.ReanchorTxFee) != 0 { + return false + } + return true +} + +func TestReservationReanchorTask_TargetWalletExclusion_SharedTask(t *testing.T) { + walletA := hexToByte20("1111111111111111111111111111111111111111") + walletB := hexToByte20("2222222222222222222222222222222222222222") + walletC := hexToByte20("3333333333333333333333333333333333333333") + + tbtcChain := tbtcpg.NewLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + tbtcChain.SetBlockCounter(blockCounter) + + // Register walletC at block 100, then walletB at block 200 (walletB is newest). + err := tbtcChain.AddPastNewWalletRegisteredEvent( + &tbtc.NewWalletRegisteredEventFilter{StartBlock: 0}, + &tbtc.NewWalletRegisteredEvent{WalletPublicKeyHash: walletC}, + ) + if err != nil { + t.Fatal(err) + } + err = tbtcChain.AddPastNewWalletRegisteredEvent( + &tbtc.NewWalletRegisteredEventFilter{StartBlock: 0}, + &tbtc.NewWalletRegisteredEvent{WalletPublicKeyHash: walletB}, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.SetWallet(walletA, &tbtc.WalletChainData{State: tbtc.StateMovingFunds}) + tbtcChain.SetWallet(walletB, &tbtc.WalletChainData{State: tbtc.StateLive}) + tbtcChain.SetWallet(walletC, &tbtc.WalletChainData{State: tbtc.StateLive}) + tbtcChain.SetLiveWalletsCount(2) + + tbtcChain.SetMovingFundsParameters( + 1000000, + 1000000, + 0, + 0, + nil, + 0, + 0, + 0, + 0, + nil, + 0, + ) + tbtcChain.SetReservationParameters(tbtc.ReservationParameters{ + ReservationTxMaxFee: 100000, + }) + btcChain.SetEstimateSatPerVByteFee(1, 1) + + // Setup reservation for Wallet A. + resAKey := big.NewInt(1001) + anchorTxHashA, _ := bitcoin.NewHashFromString( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + bitcoin.ReversedByteOrder, + ) + btcChain.SetTransaction(anchorTxHashA, &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: []byte{}, + }}, + }) + tbtcChain.SetReservation(resAKey, &tbtc.Reservation{ + WalletPublicKeyHash: walletA, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHashA, + OutputIndex: 1, + }, + Value: 100000, + }, + State: tbtc.ReservationStateActive, + RequestNonce: 0, + }) + tbtcChain.SetWalletReservations(walletA, []*big.Int{resAKey}) + + // Setup reservation for Wallet B. + resBKey := big.NewInt(2001) + anchorTxHashB, _ := bitcoin.NewHashFromString( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + bitcoin.ReversedByteOrder, + ) + btcChain.SetTransaction(anchorTxHashB, &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 200000, + PublicKeyScript: []byte{}, + }}, + }) + tbtcChain.SetReservation(resBKey, &tbtc.Reservation{ + WalletPublicKeyHash: walletB, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHashB, + OutputIndex: 1, + }, + Value: 200000, + }, + State: tbtc.ReservationStateActive, + RequestNonce: 0, + }) + tbtcChain.SetWalletReservations(walletB, []*big.Int{resBKey}) + + // Single task instance used for both runs. + task := tbtcpg.NewReservationReanchorTask(tbtcChain, btcChain) + + // First run for wallet A: should select wallet B as target. + propA, okA, err := task.Run(&tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletA, + }) + if err != nil { + t.Fatalf("unexpected error for wallet A: %v", err) + } + if !okA || propA == nil { + t.Fatalf("expected proposal for wallet A, got ok=%v, prop=%v", okA, propA) + } + proposalA, ok := propA.(*tbtc.ReservationReanchorProposal) + if !ok { + t.Fatalf("unexpected proposal type: %T", propA) + } + if proposalA.TargetWalletPublicKeyHash != walletB { + t.Errorf( + "wallet A expected target walletB [%x], got [%x]", + walletB, + proposalA.TargetWalletPublicKeyHash, + ) + } + + // Second run with the SAME task instance for wallet B (now in StateMovingFunds): + // Must NOT select wallet B (itself), even though wallet B was cached in the previous run. + // Must select wallet C instead. + tbtcChain.SetWallet(walletB, &tbtc.WalletChainData{State: tbtc.StateMovingFunds}) + propB, okB, err := task.Run(&tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletB, + }) + if err != nil { + t.Fatalf("unexpected error for wallet B: %v", err) + } + if !okB || propB == nil { + t.Fatalf("expected proposal for wallet B, got ok=%v, prop=%v", okB, propB) + } + proposalB, ok := propB.(*tbtc.ReservationReanchorProposal) + if !ok { + t.Fatalf("unexpected proposal type: %T", propB) + } + if proposalB.TargetWalletPublicKeyHash == walletB { + t.Errorf("wallet B selected itself as target wallet: [%x]", walletB) + } + if proposalB.TargetWalletPublicKeyHash != walletC { + t.Errorf( + "wallet B expected target walletC [%x], got [%x]", + walletC, + proposalB.TargetWalletPublicKeyHash, + ) + } + + // Third run: if wallet C is not live, wallet B must not produce any proposal + // (must not fall back to selecting itself). + tbtcChain.SetWallet(walletC, &tbtc.WalletChainData{State: tbtc.StateMovingFunds}) + propB2, okB2, err := task.Run(&tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletB, + }) + if err != nil { + t.Fatalf("unexpected error on third run: %v", err) + } + if okB2 || propB2 != nil { + t.Errorf("expected no proposal when no other live wallet exists, got prop=%v", propB2) + } +} + +func TestReservationReanchorTask_Run_SkipNonActiveReservations(t *testing.T) { + walletA := hexToByte20("1111111111111111111111111111111111111111") + walletB := hexToByte20("2222222222222222222222222222222222222222") + + tbtcChain := tbtcpg.NewLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(1000) + tbtcChain.SetBlockCounter(blockCounter) + + err := tbtcChain.AddPastNewWalletRegisteredEvent( + &tbtc.NewWalletRegisteredEventFilter{StartBlock: 0}, + &tbtc.NewWalletRegisteredEvent{WalletPublicKeyHash: walletB}, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.SetWallet(walletA, &tbtc.WalletChainData{State: tbtc.StateMovingFunds}) + tbtcChain.SetWallet(walletB, &tbtc.WalletChainData{State: tbtc.StateLive}) + tbtcChain.SetLiveWalletsCount(1) + + tbtcChain.SetMovingFundsParameters( + 1000000, + 1000000, + 0, + 0, + nil, + 0, + 0, + 0, + 0, + nil, + 0, + ) + tbtcChain.SetReservationParameters(tbtc.ReservationParameters{ + ReservationTxMaxFee: 100000, + }) + btcChain.SetEstimateSatPerVByteFee(1, 1) + + // Setup 2 reservations for Wallet A: + // res1 is in ReservationStateActionPending (should be skipped) + // res2 is in ReservationStateActive (should be proposed) + res1Key := big.NewInt(101) + anchorTxHash1, _ := bitcoin.NewHashFromString( + "1111111111111111111111111111111111111111111111111111111111111111", + bitcoin.ReversedByteOrder, + ) + btcChain.SetTransaction(anchorTxHash1, &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 100000, + PublicKeyScript: []byte{}, + }}, + }) + tbtcChain.SetReservation(res1Key, &tbtc.Reservation{ + WalletPublicKeyHash: walletA, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash1, + OutputIndex: 1, + }, + Value: 100000, + }, + State: tbtc.ReservationStateActionPending, + RequestNonce: 1, + }) + + res2Key := big.NewInt(102) + anchorTxHash2, _ := bitcoin.NewHashFromString( + "2222222222222222222222222222222222222222222222222222222222222222", + bitcoin.ReversedByteOrder, + ) + btcChain.SetTransaction(anchorTxHash2, &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{{ + Value: 200000, + PublicKeyScript: []byte{}, + }}, + }) + tbtcChain.SetReservation(res2Key, &tbtc.Reservation{ + WalletPublicKeyHash: walletA, + AnchorUtxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: anchorTxHash2, + OutputIndex: 1, + }, + Value: 200000, + }, + State: tbtc.ReservationStateActive, + RequestNonce: 0, + }) + + tbtcChain.SetWalletReservations(walletA, []*big.Int{res1Key, res2Key}) + + task := tbtcpg.NewReservationReanchorTask(tbtcChain, btcChain) + + prop, ok, err := task.Run(&tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletA, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok || prop == nil { + t.Fatalf("expected proposal, got ok=%v, prop=%v", ok, prop) + } + proposal, ok := prop.(*tbtc.ReservationReanchorProposal) + if !ok { + t.Fatalf("unexpected proposal type: %T", prop) + } + if proposal.ReservationKey.Cmp(res2Key) != 0 { + t.Errorf("expected proposal for res2 [102], got [%v]", proposal.ReservationKey) + } + if proposal.TargetWalletPublicKeyHash != walletB { + t.Errorf("expected target walletB [%x], got [%x]", walletB, proposal.TargetWalletPublicKeyHash) + } +} diff --git a/pkg/tbtcpg/tbtcpg.go b/pkg/tbtcpg/tbtcpg.go index 38e2be8628..d28482a548 100644 --- a/pkg/tbtcpg/tbtcpg.go +++ b/pkg/tbtcpg/tbtcpg.go @@ -18,10 +18,11 @@ import ( "strings" "github.com/ipfs/go-log/v2" - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/tbtc" "go.uber.org/zap" "golang.org/x/exp/slices" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" ) var logger = log.Logger("keep-tbtcpg") @@ -57,10 +58,18 @@ func (pg *ProposalGenerator) SetRedemptionMetricsRecorder(recorder interface { } } -// NewProposalGenerator returns a new proposal generator. +// NewProposalGenerator returns a new proposal generator. When +// reservationsEnabled is true the proposal generator appends the reservation +// acceptance (anchor) and re-anchor tasks to the standard task list so that +// wallets with reservations can produce anchor and re-anchor proposals in +// addition to the default sweep / redemption / heartbeat / moving-funds / +// moved-funds-sweep proposals. When reservationsEnabled is false the +// reservation tasks are skipped entirely; the proposal generator is safe to +// construct in either mode and the existing task ordering is preserved. func NewProposalGenerator( chain Chain, btcChain bitcoin.Chain, + reservationsEnabled bool, ) *ProposalGenerator { tasks := []ProposalTask{ NewDepositSweepTask(chain, btcChain), @@ -70,6 +79,20 @@ func NewProposalGenerator( NewMovedFundsSweepTask(chain, btcChain), } + if reservationsEnabled { + // PR H: reservation acceptance (anchor) and re-anchor tasks. + // These tasks only run when the operator has opted into the m1 + // reservation feature via config.Reservations.Enabled; the gate + // is applied at task registration so the coordination loop + // never even considers these actions on a non-reservation + // deployment. + tasks = append( + tasks, + NewReservationAcceptanceTask(chain, btcChain), + NewReservationReanchorTask(chain, btcChain), + ) + } + return &ProposalGenerator{ tasks: tasks, } diff --git a/pkg/tbtcpg/tbtcpg_test.go b/pkg/tbtcpg/tbtcpg_test.go index 75e13d6744..3cafe218f6 100644 --- a/pkg/tbtcpg/tbtcpg_test.go +++ b/pkg/tbtcpg/tbtcpg_test.go @@ -202,6 +202,69 @@ func TestProposalGenerator_Generate(t *testing.T) { } } +// TestNewProposalGenerator_ReservationsEnabled verifies the constructor's +// reservationsEnabled gate: when true, the reservation acceptance and +// re-anchor tasks must be wired into the generator's task list; when false, +// they must be entirely absent so the coordination loop never attempts +// them. Presence/absence is observed indirectly through Generate(), since +// pg.tasks is unexported: a checklist made up solely of reservation action +// types is either dispatched to a real task (which fails deterministically +// against the unconfigured chain double, proving the task was found) or +// falls through as unsupported to a nil-error no-op proposal (proving no +// task claims that action type). +func TestNewProposalGenerator_ReservationsEnabled(t *testing.T) { + walletPublicKeyHash := [20]byte{1, 2, 3} + + request := &tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + ActionsChecklist: []tbtc.WalletActionType{ + tbtc.ActionReservationAnchor, + tbtc.ActionReservationReanchor, + }, + } + + t.Run("enabled: reservation tasks are wired in", func(t *testing.T) { + generator := NewProposalGenerator( + NewLocalChain(), + NewLocalBitcoinChain(), + true, + ) + + for _, action := range []tbtc.WalletActionType{ + tbtc.ActionReservationAnchor, + tbtc.ActionReservationReanchor, + } { + _, err := generator.Generate(&tbtc.CoordinationProposalRequest{ + WalletPublicKeyHash: walletPublicKeyHash, + ActionsChecklist: []tbtc.WalletActionType{action}, + }) + if err == nil { + t.Errorf("expected error for action %v, got nil", action) + } + } + }) + + t.Run("disabled: reservation tasks are absent", func(t *testing.T) { + generator := NewProposalGenerator( + NewLocalChain(), + NewLocalBitcoinChain(), + false, + ) + + proposal, err := generator.Generate(request) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + if !reflect.DeepEqual(&tbtc.NoopProposal{}, proposal) { + t.Fatalf( + "expected a no-op proposal since no task should claim "+ + "either reservation action type, got [%+v]", + proposal, + ) + } + }) +} + type mockProposalTaskResult uint8 const ( diff --git a/test/config.json b/test/config.json index 0792a2b49d..b3fc4be6b0 100644 --- a/test/config.json +++ b/test/config.json @@ -50,9 +50,18 @@ "HistoryDepth": 25000, "TransactionLimit": 80, "RestartBackoffTime": "2h", - "IdleBackoffTime": "15m" + "IdleBackoffTime": "15m", + "Reservations": { + "Enabled": true + } } }, + "Tbtc": { + "Reservations": { + "Enabled": true + } + }, + "Developer": { "RandomBeaconAddress": "0xcf64c2a367341170cb4e09cf8c0ed137d8473ceb", "WalletRegistryAddress": "0x143ba24e66fce8bca22f7d739f9a932c519b1c76", diff --git a/test/config.toml b/test/config.toml index 8e22dae494..7050f88b5e 100644 --- a/test/config.toml +++ b/test/config.toml @@ -47,6 +47,12 @@ TransactionLimit = 80 RestartBackoffTime = "2h" IdleBackoffTime = "15m" +[maintainer.Spv.Reservations] +Enabled = true + +[tbtc.reservations] +Enabled = true + [developer] RandomBeaconAddress = "0xcf64c2a367341170cb4e09cf8c0ed137d8473ceb" WalletRegistryAddress = "0x143ba24e66fce8bca22f7d739f9a932c519b1c76" diff --git a/test/config.yaml b/test/config.yaml index 8809033eeb..3f24e4c360 100644 --- a/test/config.yaml +++ b/test/config.yaml @@ -41,6 +41,12 @@ Maintainer: TransactionLimit: 80 RestartBackoffTime: "2h" IdleBackoffTime: "15m" + Reservations: + Enabled: true +Tbtc: + Reservations: + Enabled: true + Developer: RandomBeaconAddress: "0xcf64c2a367341170cb4e09cf8c0ed137d8473ceb" WalletRegistryAddress: "0x143ba24e66fce8bca22f7d739f9a932c519b1c76"