diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 8a2060e036..1cdbffd82a 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -82,6 +82,7 @@ jobs: - tests::nakamoto_integrations::vote_for_aggregate_key_burn_op - tests::nakamoto_integrations::follower_bootup - tests::nakamoto_integrations::forked_tenure_is_ignored + - tests::nakamoto_integrations::nakamoto_attempt_time - tests::signer::v0::block_proposal_rejection - tests::signer::v0::miner_gather_signatures - tests::signer::v1::dkg diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts index 15f4d4ddc0..bf8b63ffe7 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts @@ -124,13 +124,17 @@ it("statefully interacts with PoX-4", async () => { poolMembers: [], delegatedTo: "", delegatedMaxAmount: 0, + // We initialize delegatedUntilBurnHt to 0. It will be updated + // after successful delegate-stx calls. It's value will be either + // the unwrapped until-burn-ht uint passed to the delegate-stx, + // or undefined for indefinite delegations. delegatedUntilBurnHt: 0, delegatedPoxAddress: "", amountLocked: 0, amountUnlocked: 100_000_000_000_000, unlockHeight: 0, firstLockedRewardCycle: 0, - allowedContractCaller: "", + allowedContractCallers: [], callerAllowedBy: [], committedRewCycleIndexes: [], }])), diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts index dad1a381a5..141676cdae 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts @@ -74,32 +74,17 @@ export class AllowContractCallerCommand implements PoxCommand { // Get the wallets involved from the model and update it with the new state. const wallet = model.stackers.get(this.wallet.stxAddress)!; - const callerAllowedBefore = wallet.allowedContractCaller; - - const callerAllowedBeforeState = model.stackers.get(callerAllowedBefore) || - null; - - if (callerAllowedBeforeState) { - // Remove the allower from the ex-allowed caller's allowance list. - - const walletIndexInsideAllowedByList = callerAllowedBeforeState - .callerAllowedBy.indexOf( - this.wallet.stxAddress, - ); - - expect(walletIndexInsideAllowedByList).toBeGreaterThan(-1); - - callerAllowedBeforeState.callerAllowedBy.splice( - walletIndexInsideAllowedByList, - 1, - ); - } const callerToAllow = model.stackers.get(this.allowanceTo.stxAddress)!; // Update model so that we know this wallet has authorized a contract-caller. + // If the caller is already allowed, there's no need to add it again. + const callerToAllowIndexInAllowedList = wallet.allowedContractCallers + .indexOf(this.allowanceTo.stxAddress); - wallet.allowedContractCaller = this.allowanceTo.stxAddress; - callerToAllow.callerAllowedBy.push(this.wallet.stxAddress); + if (callerToAllowIndexInAllowedList == -1) { + wallet.allowedContractCallers.push(this.allowanceTo.stxAddress); + callerToAllow.callerAllowedBy.push(this.wallet.stxAddress); + } // Log to console for debugging purposes. This is not necessary for the // test to pass but it is useful for debugging and eyeballing the test. diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts index 6d4d582b58..ce1d2a28b4 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts @@ -76,9 +76,14 @@ export class Stub { // Get the wallet's ex-delegators by comparing their delegatedUntilBurnHt // to the current burn block height (only if the wallet is a delegatee). - const expiredDelegators = wallet.poolMembers.filter((stackerAddress) => - this.stackers.get(stackerAddress)!.delegatedUntilBurnHt < - burnBlockHeight + // If the delegatedUntilBurnHt is undefined, the delegator is considered + // active for an indefinite period (until a revoke-delegate-stx call). + const expiredDelegators = wallet.poolMembers.filter( + (stackerAddress) => + this.stackers.get(stackerAddress)!.delegatedUntilBurnHt !== + undefined && + this.stackers.get(stackerAddress)!.delegatedUntilBurnHt as number < + burnBlockHeight, ); // Get the operator's pool stackers that no longer have partially commited @@ -180,13 +185,13 @@ export type Stacker = { poolMembers: StxAddress[]; delegatedTo: StxAddress; delegatedMaxAmount: number; - delegatedUntilBurnHt: number; + delegatedUntilBurnHt: number | undefined; delegatedPoxAddress: BtcAddress; amountLocked: number; amountUnlocked: number; unlockHeight: number; firstLockedRewardCycle: number; - allowedContractCaller: StxAddress; + allowedContractCallers: StxAddress[]; callerAllowedBy: StxAddress[]; committedRewCycleIndexes: number[]; }; diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts index ba7043d5ec..bafbe38a43 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts @@ -163,13 +163,16 @@ export function PoxCommands( fc.record({ wallet: fc.constantFrom(...wallets.values()), delegateTo: fc.constantFrom(...wallets.values()), - untilBurnHt: fc.integer({ min: 1 }), + untilBurnHt: fc.oneof( + fc.constant(Cl.none()), + fc.integer({ min: 1 }).map((value) => Cl.some(Cl.uint(value))), + ), amount: fc.bigInt({ min: 0n, max: 100_000_000_000_000n }), }).map(( r: { wallet: Wallet; delegateTo: Wallet; - untilBurnHt: number; + untilBurnHt: OptionalCV; amount: bigint; }, ) => diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts index cfd385cf5a..2875551342 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts @@ -83,7 +83,8 @@ export class DelegateStackExtendCommand implements PoxCommand { stackerWallet.hasDelegated === true && stackerWallet.isStacking === true && stackerWallet.delegatedTo === this.operator.stxAddress && - stackerWallet.delegatedUntilBurnHt >= newUnlockHeight && + (stackerWallet.delegatedUntilBurnHt === undefined || + stackerWallet.delegatedUntilBurnHt >= newUnlockHeight) && stackerWallet.delegatedMaxAmount >= stackedAmount && operatorWallet.poolMembers.includes(this.stacker.stxAddress) && operatorWallet.lockedAddresses.includes(this.stacker.stxAddress) && diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts index 456983807f..e3d9dd25c1 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts @@ -94,7 +94,8 @@ export class DelegateStackStxCommand implements PoxCommand { Number(this.amountUstx) <= stackerWallet.ustxBalance && Number(this.amountUstx) >= model.stackingMinimum && operatorWallet.poolMembers.includes(this.stacker.stxAddress) && - this.unlockBurnHt <= stackerWallet.delegatedUntilBurnHt + (stackerWallet.delegatedUntilBurnHt === undefined || + this.unlockBurnHt <= stackerWallet.delegatedUntilBurnHt) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts index 4a12b0140d..e70d466c9d 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts @@ -7,7 +7,15 @@ import { } from "./pox_CommandModel.ts"; import { poxAddressToTuple } from "@stacks/stacking"; import { expect } from "vitest"; -import { boolCV, Cl } from "@stacks/transactions"; +import { + boolCV, + Cl, + ClarityType, + cvToValue, + isClarityType, + OptionalCV, + UIntCV, +} from "@stacks/transactions"; /** * The `DelegateStxCommand` delegates STX for stacking within PoX-4. This @@ -22,7 +30,7 @@ import { boolCV, Cl } from "@stacks/transactions"; export class DelegateStxCommand implements PoxCommand { readonly wallet: Wallet; readonly delegateTo: Wallet; - readonly untilBurnHt: number; + readonly untilBurnHt: OptionalCV; readonly amount: bigint; /** @@ -37,7 +45,7 @@ export class DelegateStxCommand implements PoxCommand { constructor( wallet: Wallet, delegateTo: Wallet, - untilBurnHt: number, + untilBurnHt: OptionalCV, amount: bigint, ) { this.wallet = wallet; @@ -74,7 +82,7 @@ export class DelegateStxCommand implements PoxCommand { // (delegate-to principal) Cl.principal(this.delegateTo.stxAddress), // (until-burn-ht (optional uint)) - Cl.some(Cl.uint(this.untilBurnHt)), + this.untilBurnHt, // (pox-addr (optional { version: (buff 1), hashbytes: (buff 32) })) Cl.some(poxAddressToTuple(this.delegateTo.btcAddress)), ], @@ -93,7 +101,10 @@ export class DelegateStxCommand implements PoxCommand { wallet.hasDelegated = true; wallet.delegatedTo = this.delegateTo.stxAddress; wallet.delegatedMaxAmount = amountUstx; - wallet.delegatedUntilBurnHt = this.untilBurnHt; + wallet.delegatedUntilBurnHt = + isClarityType(this.untilBurnHt, ClarityType.OptionalNone) + ? undefined + : Number(cvToValue(this.untilBurnHt).value); wallet.delegatedPoxAddress = this.delegateTo.btcAddress; delegatedWallet.poolMembers.push(this.wallet.stxAddress); diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts index 09618db49c..16b830b5fb 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts @@ -42,7 +42,9 @@ export class DisallowContractCallerCommand implements PoxCommand { this.callerToDisallow.stxAddress, )!; return ( - stacker.allowedContractCaller === this.callerToDisallow.stxAddress && + stacker.allowedContractCallers.includes( + this.callerToDisallow.stxAddress, + ) && callerToDisallow.callerAllowedBy.includes( this.stacker.stxAddress, ) === @@ -76,7 +78,12 @@ export class DisallowContractCallerCommand implements PoxCommand { // Update model so that we know that the stacker has revoked stacking // allowance. const stacker = model.stackers.get(this.stacker.stxAddress)!; - stacker.allowedContractCaller = ""; + const callerToDisallowIndex = stacker.allowedContractCallers.indexOf( + this.callerToDisallow.stxAddress, + ); + + expect(callerToDisallowIndex).toBeGreaterThan(-1); + stacker.allowedContractCallers.splice(callerToDisallowIndex, 1); // Remove the operator from the caller to disallow's allowance list. const walletIndexAllowedByList = callerToDisallow.callerAllowedBy.indexOf( diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts index 1c30e3d569..c39a1a5e42 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts @@ -15,6 +15,7 @@ import { Cl, someCV, tupleCV } from "@stacks/transactions"; * * Constraints for running this command include: * - The `Stacker` has to currently be delegating. + * - The `Stacker`'s delegation must not be expired. */ export class RevokeDelegateStxCommand implements PoxCommand { readonly wallet: Wallet; @@ -31,10 +32,13 @@ export class RevokeDelegateStxCommand implements PoxCommand { check(model: Readonly): boolean { // Constraints for running this command include: // - The Stacker has to currently be delegating. - + // - The Stacker's delegation must not be expired. + const stacker = model.stackers.get(this.wallet.stxAddress)!; return ( model.stackingMinimum > 0 && - model.stackers.get(this.wallet.stxAddress)!.hasDelegated === true + stacker.hasDelegated === true && + (stacker.delegatedUntilBurnHt === undefined || + stacker.delegatedUntilBurnHt > model.burnBlockHeight) ); } @@ -43,6 +47,9 @@ export class RevokeDelegateStxCommand implements PoxCommand { const wallet = model.stackers.get(this.wallet.stxAddress)!; const operatorWallet = model.stackers.get(wallet.delegatedTo)!; + const expectedUntilBurnHt = wallet.delegatedUntilBurnHt === undefined + ? Cl.none() + : Cl.some(Cl.uint(wallet.delegatedUntilBurnHt)); // Act const revokeDelegateStx = real.network.callPublicFn( @@ -63,7 +70,7 @@ export class RevokeDelegateStxCommand implements PoxCommand { "pox-addr": Cl.some( poxAddressToTuple(wallet.delegatedPoxAddress || ""), ), - "until-burn-ht": Cl.some(Cl.uint(wallet.delegatedUntilBurnHt)), + "until-burn-ht": expectedUntilBurnHt, }), ), ); @@ -73,6 +80,8 @@ export class RevokeDelegateStxCommand implements PoxCommand { // Update model so that we know this wallet is not delegating anymore. // This is important in order to prevent the test from revoking the // delegation multiple times with the same address. + // We update delegatedUntilBurnHt to 0, and not undefined. Undefined + // stands for indefinite delegation. wallet.hasDelegated = false; wallet.delegatedTo = ""; wallet.delegatedUntilBurnHt = 0; diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts index 5312679833..62622f4bd3 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts @@ -9,6 +9,7 @@ import { poxAddressToTuple } from "@stacks/stacking"; import { expect } from "vitest"; import { Cl } from "@stacks/transactions"; import { currentCycle } from "./pox_Commands.ts"; +import { tx } from "@hirosystems/clarinet-sdk"; /** * The `StackAggregationCommitAuthCommand` allows an operator to commit @@ -60,54 +61,61 @@ export class StackAggregationCommitAuthCommand implements PoxCommand { const operatorWallet = model.stackers.get(this.operator.stxAddress)!; const committedAmount = operatorWallet.amountToCommit; - const { result: setSignature } = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "set-signer-key-authorization", - [ - // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) - poxAddressToTuple(this.operator.btcAddress), - // (period uint) - Cl.uint(1), - // (reward-cycle uint) - Cl.uint(currentRewCycle + 1), - // (topic (string-ascii 14)) - Cl.stringAscii("agg-commit"), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.operator.signerPubKey), - // (allowed bool) - Cl.bool(true), - // (max-amount uint) - Cl.uint(committedAmount), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.operator.stxAddress, - ); - expect(setSignature).toBeOk(Cl.bool(true)); - // Act - const stackAggregationCommit = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "stack-aggregation-commit", - [ - // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) - poxAddressToTuple(this.operator.btcAddress), - // (reward-cycle uint) - Cl.uint(currentRewCycle + 1), - // (signer-sig (optional (buff 65))) - Cl.none(), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.operator.signerPubKey), - // (max-amount uint) - Cl.uint(committedAmount), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.operator.stxAddress, - ); + + // Include the authorization and the `stack-aggregation-commit` transactions + // in a single block. This way we ensure both the authorization and the + // stack-aggregation-commit transactions are called during the same reward + // cycle, so the authorization currentRewCycle param is relevant for the + // upcoming stack-aggregation-commit call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (period uint) + Cl.uint(1), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (topic (string-ascii 14)) + Cl.stringAscii("agg-commit"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-commit", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + ]); // Assert - expect(stackAggregationCommit.result).toBeOk(Cl.bool(true)); + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeOk(Cl.bool(true)); operatorWallet.amountToCommit -= committedAmount; operatorWallet.committedRewCycleIndexes.push(model.nextRewardSetIndex); diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts index dfe7f2beef..cfafccc674 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts @@ -9,6 +9,7 @@ import { poxAddressToTuple } from "@stacks/stacking"; import { expect } from "vitest"; import { Cl } from "@stacks/transactions"; import { currentCycle } from "./pox_Commands.ts"; +import { tx } from "@hirosystems/clarinet-sdk"; /** * The `StackAggregationCommitIndexedAuthCommand` allows an operator to @@ -65,54 +66,61 @@ export class StackAggregationCommitIndexedAuthCommand implements PoxCommand { const operatorWallet = model.stackers.get(this.operator.stxAddress)!; const committedAmount = operatorWallet.amountToCommit; - const { result: setSignature } = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "set-signer-key-authorization", - [ - // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) - poxAddressToTuple(this.operator.btcAddress), - // (period uint) - Cl.uint(1), - // (reward-cycle uint) - Cl.uint(currentRewCycle + 1), - // (topic (string-ascii 14)) - Cl.stringAscii("agg-commit"), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.operator.signerPubKey), - // (allowed bool) - Cl.bool(true), - // (max-amount uint) - Cl.uint(committedAmount), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.operator.stxAddress, - ); - expect(setSignature).toBeOk(Cl.bool(true)); - // Act - const stackAggregationCommitIndexed = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "stack-aggregation-commit-indexed", - [ - // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) - poxAddressToTuple(this.operator.btcAddress), - // (reward-cycle uint) - Cl.uint(currentRewCycle + 1), - // (signer-sig (optional (buff 65))) - Cl.none(), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.operator.signerPubKey), - // (max-amount uint) - Cl.uint(committedAmount), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.operator.stxAddress, - ); + + // Include the authorization and the `stack-aggregation-commit-indexed` + // transactions in a single block. This way we ensure both the authorization + // and the stack-aggregation-commit-indexed transactions are called during + // the same reward cycle, so the authorization currentRewCycle param is + // relevant for the upcoming stack-aggregation-commit-indexed call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (period uint) + Cl.uint(1), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (topic (string-ascii 14)) + Cl.stringAscii("agg-commit"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-commit-indexed", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + ]); // Assert - expect(stackAggregationCommitIndexed.result).toBeOk( + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeOk( Cl.uint(model.nextRewardSetIndex), ); diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts index a7dbf49cbb..fa796673ea 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts @@ -7,6 +7,7 @@ import { } from "./pox_Commands"; import { Cl, ClarityType, isClarityType } from "@stacks/transactions"; import { assert, expect } from "vitest"; +import { tx } from "@hirosystems/clarinet-sdk"; export class StackExtendAuthCommand implements PoxCommand { readonly wallet: Wallet; @@ -77,51 +78,6 @@ export class StackExtendAuthCommand implements PoxCommand { const stacker = model.stackers.get(this.wallet.stxAddress)!; - const { result: setAuthorization } = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "set-signer-key-authorization", - [ - // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) - poxAddressToTuple(this.wallet.btcAddress), - // (period uint) - Cl.uint(this.extendCount), - // (reward-cycle uint) - Cl.uint(currentRewCycle), - // (topic (string-ascii 14)) - Cl.stringAscii("stack-extend"), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.wallet.signerPubKey), - // (allowed bool) - Cl.bool(true), - // (max-amount uint) - Cl.uint(stacker.amountLocked), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.wallet.stxAddress, - ); - - expect(setAuthorization).toBeOk(Cl.bool(true)); - const stackExtend = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "stack-extend", - [ - // (extend-count uint) - Cl.uint(this.extendCount), - // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) - poxAddressToTuple(this.wallet.btcAddress), - // (signer-sig (optional (buff 65))) - Cl.none(), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.wallet.signerPubKey), - // (max-amount uint) - Cl.uint(stacker.amountLocked), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.wallet.stxAddress, - ); - const { result: firstExtendCycle } = real.network.callReadOnlyFn( "ST000000000000000000002AMW42H.pox-4", "burn-height-to-reward-cycle", @@ -143,7 +99,57 @@ export class StackExtendAuthCommand implements PoxCommand { const newUnlockHeight = extendedUnlockHeight.value; - expect(stackExtend.result).toBeOk( + // Include the authorization and the `stack-extend` transactions in a single + // block. This way we ensure both the authorization and the stack-extend + // transactions are called during the same reward cycle, so the authorization + // currentRewCycle param is relevant for the upcoming stack-extend call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (period uint) + Cl.uint(this.extendCount), + // (reward-cycle uint) + Cl.uint(currentRewCycle), + // (topic (string-ascii 14)) + Cl.stringAscii("stack-extend"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(stacker.amountLocked), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-extend", + [ + // (extend-count uint) + Cl.uint(this.extendCount), + // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + poxAddressToTuple(this.wallet.btcAddress), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(stacker.amountLocked), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + ]); + + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeOk( Cl.tuple({ stacker: Cl.principal(this.wallet.stxAddress), "unlock-burn-height": Cl.uint(newUnlockHeight), diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts index 108f0956b5..53f34ca0bb 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts @@ -15,6 +15,7 @@ import { isClarityType, } from "@stacks/transactions"; import { currentCycle } from "./pox_Commands.ts"; +import { tx } from "@hirosystems/clarinet-sdk"; /** * The `StackStxAuthCommand` locks STX for stacking within PoX-4. This self-service @@ -80,31 +81,6 @@ export class StackStxAuthCommand implements PoxCommand { // generated number passed to the constructor of this class. const maxAmount = model.stackingMinimum * this.margin; - const { result: setAuthorization } = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "set-signer-key-authorization", - [ - // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) - poxAddressToTuple(this.wallet.btcAddress), - // (period uint) - Cl.uint(this.period), - // (reward-cycle uint) - Cl.uint(currentRewCycle), - // (topic (string-ascii 14)) - Cl.stringAscii("stack-stx"), - // (signer-key (buff 33)) - Cl.bufferFromHex(this.wallet.signerPubKey), - // (allowed bool) - Cl.bool(true), - // (max-amount uint) - Cl.uint(maxAmount), - // (auth-id uint) - Cl.uint(this.authId), - ], - this.wallet.stxAddress, - ); - - expect(setAuthorization).toBeOk(Cl.bool(true)); const burnBlockHeightCV = real.network.runSnippet("burn-block-height"); const burnBlockHeight = Number( cvToValue(burnBlockHeightCV as ClarityValue), @@ -115,17 +91,41 @@ export class StackStxAuthCommand implements PoxCommand { // signer key. const amountUstx = maxAmount; - // Act - const stackStx = real.network.callPublicFn( - "ST000000000000000000002AMW42H.pox-4", - "stack-stx", - [ + // Include the authorization and the `stack-stx` transactions in a single + // block. This way we ensure both the authorization and the stack-stx + // transactions are called during the same reward cycle, so the authorization + // currentRewCycle param is relevant for the upcoming stack-stx call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (period uint) + Cl.uint(this.period), + // (reward-cycle uint) + Cl.uint(currentRewCycle), + // (topic (string-ascii 14)) + Cl.stringAscii("stack-stx"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + tx.callPublicFn("ST000000000000000000002AMW42H.pox-4", "stack-stx", [ // (amount-ustx uint) Cl.uint(amountUstx), // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) poxAddressToTuple(this.wallet.btcAddress), // (start-burn-ht uint) - Cl.uint(burnBlockHeight), + Cl.uint(burnBlockHeight + 1), // (lock-period uint) Cl.uint(this.period), // (signer-sig (optional (buff 65))) @@ -136,9 +136,10 @@ export class StackStxAuthCommand implements PoxCommand { Cl.uint(maxAmount), // (auth-id uint) Cl.uint(this.authId), - ], - this.wallet.stxAddress, - ); + ], this.wallet.stxAddress), + ]); + + expect(block[0].result).toBeOk(Cl.bool(true)); const { result: rewardCycle } = real.network.callReadOnlyFn( "ST000000000000000000002AMW42H.pox-4", @@ -156,8 +157,7 @@ export class StackStxAuthCommand implements PoxCommand { ); assert(isClarityType(unlockBurnHeight, ClarityType.UInt)); - // Assert - expect(stackStx.result).toBeOk( + expect(block[1].result).toBeOk( Cl.tuple({ "lock-amount": Cl.uint(amountUstx), "signer-key": Cl.bufferFromHex(this.wallet.signerPubKey), diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts index baa87015a1..100d84a6e0 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts @@ -123,7 +123,7 @@ export class StackStxSigCommand implements PoxCommand { // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) poxAddressToTuple(this.wallet.btcAddress), // (start-burn-ht uint) - Cl.uint(burnBlockHeight), + Cl.uint(burnBlockHeight + 1), // (lock-period uint) Cl.uint(this.period), // (signer-sig (optional (buff 65))) diff --git a/docs/mining.md b/docs/mining.md index 891358af03..e113f12d93 100644 --- a/docs/mining.md +++ b/docs/mining.md @@ -26,7 +26,7 @@ subsequent_attempt_time_ms = 60000 # Time to spend mining a microblock, in milliseconds. microblock_attempt_time_ms = 30000 # Time to spend mining a Nakamoto block, in milliseconds. -nakamoto_attempt_time_ms = 10000 +nakamoto_attempt_time_ms = 20000 ``` You can verify that your node is operating as a miner by checking its log output diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 8f416b4c39..633588faf0 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -10927,4 +10927,12 @@ pub mod tests { let db_epochs = SortitionDB::get_stacks_epochs(sortdb.conn()).unwrap(); assert_eq!(db_epochs, STACKS_EPOCHS_MAINNET.to_vec()); } + + #[test] + fn latest_db_version_supports_latest_epoch() { + assert!(SortitionDB::is_db_version_supported_in_epoch( + StacksEpochId::latest(), + SORTITION_DB_VERSION + )); + } } diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index d92b373bdd..c106132b34 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -79,7 +79,7 @@ use crate::chainstate::burn::operations::{LeaderBlockCommitOp, LeaderKeyRegister use crate::chainstate::burn::{BlockSnapshot, SortitionHash}; use crate::chainstate::coordinator::{BlockEventDispatcher, Error}; use crate::chainstate::nakamoto::signer_set::NakamotoSigners; -use crate::chainstate::nakamoto::tenure::NAKAMOTO_TENURES_SCHEMA; +use crate::chainstate::nakamoto::tenure::{NAKAMOTO_TENURES_SCHEMA_1, NAKAMOTO_TENURES_SCHEMA_2}; use crate::chainstate::stacks::address::PoxAddress; use crate::chainstate::stacks::boot::{POX_4_NAME, SIGNERS_UPDATE_STATE}; use crate::chainstate::stacks::db::{DBConfig as ChainstateConfig, StacksChainState}; @@ -147,7 +147,7 @@ lazy_static! { reward_set TEXT NOT NULL, PRIMARY KEY (index_block_hash) );"#.into(), - NAKAMOTO_TENURES_SCHEMA.into(), + NAKAMOTO_TENURES_SCHEMA_1.into(), r#" -- Table for Nakamoto block headers CREATE TABLE nakamoto_block_headers ( @@ -210,14 +210,21 @@ lazy_static! { ); CREATE INDEX nakamoto_block_headers_by_consensus_hash ON nakamoto_block_headers(consensus_hash); "#.into(), - format!( - r#"ALTER TABLE payments - ADD COLUMN schedule_type TEXT NOT NULL DEFAULT "{}"; - "#, - HeaderTypeNames::Epoch2.get_name_str()), - r#" - UPDATE db_config SET version = "4"; - "#.into(), + format!( + r#"ALTER TABLE payments + ADD COLUMN schedule_type TEXT NOT NULL DEFAULT "{}"; + "#, + HeaderTypeNames::Epoch2.get_name_str()), + r#" + UPDATE db_config SET version = "4"; + "#.into(), + ]; + + pub static ref NAKAMOTO_CHAINSTATE_SCHEMA_2: Vec = vec![ + NAKAMOTO_TENURES_SCHEMA_2.into(), + r#" + UPDATE db_config SET version = "5"; + "#.into(), ]; } diff --git a/stackslib/src/chainstate/nakamoto/signer_set.rs b/stackslib/src/chainstate/nakamoto/signer_set.rs index e776ca41db..a7e8df6ed0 100644 --- a/stackslib/src/chainstate/nakamoto/signer_set.rs +++ b/stackslib/src/chainstate/nakamoto/signer_set.rs @@ -58,7 +58,6 @@ use crate::chainstate::burn::operations::{ }; use crate::chainstate::burn::{BlockSnapshot, SortitionHash}; use crate::chainstate::coordinator::{BlockEventDispatcher, Error}; -use crate::chainstate::nakamoto::tenure::NAKAMOTO_TENURES_SCHEMA; use crate::chainstate::stacks::address::PoxAddress; use crate::chainstate::stacks::boot::{ PoxVersions, RawRewardSetEntry, RewardSet, BOOT_TEST_POX_4_AGG_KEY_CONTRACT, diff --git a/stackslib/src/chainstate/nakamoto/tenure.rs b/stackslib/src/chainstate/nakamoto/tenure.rs index 6238e2905a..f33a0b7249 100644 --- a/stackslib/src/chainstate/nakamoto/tenure.rs +++ b/stackslib/src/chainstate/nakamoto/tenure.rs @@ -118,7 +118,7 @@ use crate::util_lib::db::{ FromRow, }; -pub static NAKAMOTO_TENURES_SCHEMA: &'static str = r#" +pub static NAKAMOTO_TENURES_SCHEMA_1: &'static str = r#" CREATE TABLE nakamoto_tenures ( -- consensus hash of start-tenure block (i.e. the consensus hash of the sortition in which the miner's block-commit -- was mined) @@ -155,6 +155,46 @@ pub static NAKAMOTO_TENURES_SCHEMA: &'static str = r#" CREATE INDEX nakamoto_tenures_by_parent ON nakamoto_tenures(tenure_id_consensus_hash,prev_tenure_id_consensus_hash); "#; +pub static NAKAMOTO_TENURES_SCHEMA_2: &'static str = r#" + -- Drop the nakamoto_tenures table if it exists + DROP TABLE IF EXISTS nakamoto_tenures; + + CREATE TABLE nakamoto_tenures ( + -- consensus hash of start-tenure block (i.e. the consensus hash of the sortition in which the miner's block-commit + -- was mined) + tenure_id_consensus_hash TEXT NOT NULL, + -- consensus hash of the previous tenure's start-tenure block + prev_tenure_id_consensus_hash TEXT NOT NULL, + -- consensus hash of the last-processed sortition + burn_view_consensus_hash TEXT NOT NULL, + -- whether or not this tenure was triggered by a sortition (as opposed to a tenure-extension). + -- this is equal to the `cause` field in a TenureChange + cause INTEGER NOT NULL, + -- block hash of start-tenure block + block_hash TEXT NOT NULL, + -- block ID of this start block (this is the StacksBlockId of the above tenure_id_consensus_hash and block_hash) + block_id TEXT NOT NULL, + -- this field is the total number of _sortition-induced_ tenures in the chain history (including this tenure), + -- as of the _end_ of this block. A tenure can contain multiple TenureChanges; if so, then this + -- is the height of the _sortition-induced_ TenureChange that created it. + coinbase_height INTEGER NOT NULL, + -- number of blocks this tenure. + -- * for tenure-changes induced by sortitions, this is the number of blocks in the previous tenure + -- * for tenure-changes induced by extension, this is the number of blocks in the current tenure so far. + num_blocks_confirmed INTEGER NOT NULL, + -- this is the ith tenure transaction in its respective Nakamoto chain history. + tenure_index INTEGER NOT NULL, + + PRIMARY KEY(burn_view_consensus_hash,tenure_index) + ); + CREATE INDEX nakamoto_tenures_by_block_id ON nakamoto_tenures(block_id); + CREATE INDEX nakamoto_tenures_by_tenure_id ON nakamoto_tenures(tenure_id_consensus_hash); + CREATE INDEX nakamoto_tenures_by_block_and_consensus_hashes ON nakamoto_tenures(tenure_id_consensus_hash,block_hash); + CREATE INDEX nakamoto_tenures_by_burn_view_consensus_hash ON nakamoto_tenures(burn_view_consensus_hash); + CREATE INDEX nakamoto_tenures_by_tenure_index ON nakamoto_tenures(tenure_index); + CREATE INDEX nakamoto_tenures_by_parent ON nakamoto_tenures(tenure_id_consensus_hash,prev_tenure_id_consensus_hash); +"#; + #[derive(Debug, Clone, PartialEq)] pub struct NakamotoTenure { /// consensus hash of start-tenure block diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index 374fc11ae1..865758ed01 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -53,7 +53,7 @@ use crate::chainstate::burn::operations::{ use crate::chainstate::burn::{ConsensusHash, ConsensusHashExtensions}; use crate::chainstate::nakamoto::{ HeaderTypeNames, NakamotoBlock, NakamotoBlockHeader, NakamotoChainState, - NakamotoStagingBlocksConn, NAKAMOTO_CHAINSTATE_SCHEMA_1, + NakamotoStagingBlocksConn, NAKAMOTO_CHAINSTATE_SCHEMA_1, NAKAMOTO_CHAINSTATE_SCHEMA_2, }; use crate::chainstate::stacks::address::StacksAddressExtensions; use crate::chainstate::stacks::boot::*; @@ -294,16 +294,32 @@ impl DBConfig { || self.version == "2" || self.version == "3" || self.version == "4" + || self.version == "5" } StacksEpochId::Epoch2_05 => { - self.version == "2" || self.version == "3" || self.version == "4" + self.version == "2" + || self.version == "3" + || self.version == "4" + || self.version == "5" + } + StacksEpochId::Epoch21 => { + self.version == "3" || self.version == "4" || self.version == "5" + } + StacksEpochId::Epoch22 => { + self.version == "3" || self.version == "4" || self.version == "5" + } + StacksEpochId::Epoch23 => { + self.version == "3" || self.version == "4" || self.version == "5" + } + StacksEpochId::Epoch24 => { + self.version == "3" || self.version == "4" || self.version == "5" + } + StacksEpochId::Epoch25 => { + self.version == "3" || self.version == "4" || self.version == "5" + } + StacksEpochId::Epoch30 => { + self.version == "3" || self.version == "4" || self.version == "5" } - StacksEpochId::Epoch21 => self.version == "3" || self.version == "4", - StacksEpochId::Epoch22 => self.version == "3" || self.version == "4", - StacksEpochId::Epoch23 => self.version == "3" || self.version == "4", - StacksEpochId::Epoch24 => self.version == "3" || self.version == "4", - StacksEpochId::Epoch25 => self.version == "3" || self.version == "4", - StacksEpochId::Epoch30 => self.version == "3" || self.version == "4", } } } @@ -668,7 +684,7 @@ impl<'a> DerefMut for ChainstateTx<'a> { } } -pub const CHAINSTATE_VERSION: &'static str = "4"; +pub const CHAINSTATE_VERSION: &'static str = "5"; const CHAINSTATE_INITIAL_SCHEMA: &'static [&'static str] = &[ "PRAGMA foreign_keys = ON;", @@ -1079,6 +1095,13 @@ impl StacksChainState { tx.execute_batch(cmd)?; } } + "4" => { + // migrate to nakamoto 2 + info!("Migrating chainstate schema from version 4 to 5: fix nakamoto tenure typo"); + for cmd in NAKAMOTO_CHAINSTATE_SCHEMA_2.iter() { + tx.execute_batch(cmd)?; + } + } _ => { error!( "Invalid chain state database: expected version = {}, got {}", @@ -2926,4 +2949,14 @@ pub mod test { MAINNET_2_0_GENESIS_ROOT_HASH ); } + + #[test] + fn latest_db_version_supports_latest_epoch() { + let db = DBConfig { + version: CHAINSTATE_VERSION.to_string(), + mainnet: true, + chain_id: CHAIN_ID_MAINNET, + }; + assert!(db.supports_epoch(StacksEpochId::latest())); + } } diff --git a/stackslib/src/net/api/gettenure.rs b/stackslib/src/net/api/gettenure.rs index c3eb4493fe..24c3c87d71 100644 --- a/stackslib/src/net/api/gettenure.rs +++ b/stackslib/src/net/api/gettenure.rs @@ -19,7 +19,7 @@ use std::{fs, io}; use regex::{Captures, Regex}; use serde::de::Error as de_Error; -use stacks_common::codec::{StacksMessageCodec, MAX_MESSAGE_LEN}; +use stacks_common::codec::{StacksMessageCodec, MAX_PAYLOAD_LEN}; use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId}; use stacks_common::types::net::PeerHost; use stacks_common::util::hash::to_hex; @@ -46,7 +46,7 @@ pub struct RPCNakamotoTenureRequestHandler { /// Block to start streaming from. It and its ancestors will be incrementally streamed until one of /// hte following happens: /// * we reach the first block in the tenure - /// * we would exceed MAX_MESSAGE_LEN bytes transmitted if we started sending the next block + /// * we would exceed MAX_PAYLOAD_LEN bytes transmitted if we started sending the next block pub block_id: Option, /// What's the final block ID to stream from? /// Passed as `stop=` query parameter @@ -132,7 +132,7 @@ impl NakamotoTenureStream { self.total_sent = self .total_sent .saturating_add(self.block_stream.total_bytes); - if self.total_sent.saturating_add(parent_size) > MAX_MESSAGE_LEN.into() { + if self.total_sent.saturating_add(parent_size) > MAX_PAYLOAD_LEN.into() { // out of space to send this return Ok(false); } @@ -284,7 +284,7 @@ impl HttpResponse for RPCNakamotoTenureRequestHandler { preamble: &HttpResponsePreamble, body: &[u8], ) -> Result { - let bytes = parse_bytes(preamble, body, MAX_MESSAGE_LEN.into())?; + let bytes = parse_bytes(preamble, body, MAX_PAYLOAD_LEN.into())?; Ok(HttpResponsePayload::Bytes(bytes)) } } diff --git a/stackslib/src/net/download/nakamoto/download_state_machine.rs b/stackslib/src/net/download/nakamoto/download_state_machine.rs index c95dc6d5f3..27b675ae49 100644 --- a/stackslib/src/net/download/nakamoto/download_state_machine.rs +++ b/stackslib/src/net/download/nakamoto/download_state_machine.rs @@ -271,8 +271,7 @@ impl NakamotoDownloadStateMachine { .pox_constants .reward_cycle_to_block_height(sortdb.first_block_height, tip_rc.saturating_add(1)) .saturating_sub(1) - .min(tip.block_height) - .saturating_add(1); + .min(tip.block_height.saturating_add(1)); test_debug!( "Load tip sortitions between {} and {} (loaded_so_far = {})", @@ -1232,6 +1231,8 @@ impl NakamotoDownloadStateMachine { .any(|(_, available)| available.contains_key(&wt.tenure_id_consensus_hash)); if is_available && !wt.processed { + // a tenure is available but not yet processed, so we can't yet transition to + // fetching unconfirmed tenures (we'd have no way to validate them). return false; } } @@ -1297,14 +1298,16 @@ impl NakamotoDownloadStateMachine { count: usize, downloaders: &mut HashMap, highest_processed_block_id: Option, - ) { - while downloaders.len() < count { - let Some(naddr) = schedule.front() else { - break; - }; + ) -> usize { + let mut added = 0; + schedule.retain(|naddr| { if downloaders.contains_key(naddr) { - continue; + return true; + } + if added >= count { + return true; } + let unconfirmed_tenure_download = NakamotoUnconfirmedTenureDownloader::new( naddr.clone(), highest_processed_block_id.clone(), @@ -1312,8 +1315,10 @@ impl NakamotoDownloadStateMachine { test_debug!("Request unconfirmed tenure state from neighbor {}", &naddr); downloaders.insert(naddr.clone(), unconfirmed_tenure_download); - schedule.pop_front(); - } + added += 1; + false + }); + added } /// Update our unconfirmed tenure download state machines diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs index 0100eb0ecd..6c53ff924c 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs @@ -432,8 +432,8 @@ impl NakamotoTenureDownloaderSet { self.num_scheduled_downloaders() ); - self.clear_available_peers(); self.clear_finished_downloaders(); + self.clear_available_peers(); self.try_transition_fetch_tenure_end_blocks(tenure_block_ids); while self.inflight() < count { let Some(ch) = schedule.front() else { diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs index 4d4d4dee47..101229f7f6 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs @@ -449,6 +449,7 @@ impl NakamotoUnconfirmedTenureDownloader { // If there's a tenure-start block, it must be last. let mut expected_block_id = last_block_id; let mut finished_download = false; + let mut last_block_index = None; for (cnt, block) in tenure_blocks.iter().enumerate() { if &block.header.block_id() != expected_block_id { warn!("Unexpected Nakamoto block -- not part of tenure"; @@ -498,6 +499,7 @@ impl NakamotoUnconfirmedTenureDownloader { } finished_download = true; + last_block_index = Some(cnt); break; } @@ -506,7 +508,9 @@ impl NakamotoUnconfirmedTenureDownloader { if let Some(highest_processed_block_id) = self.highest_processed_block_id.as_ref() { if expected_block_id == highest_processed_block_id { // got all the blocks we asked for + debug!("Cancelling unconfirmed tenure download to {}: have processed block up to block {} already", &self.naddr, highest_processed_block_id); finished_download = true; + last_block_index = Some(cnt); break; } } @@ -516,15 +520,22 @@ impl NakamotoUnconfirmedTenureDownloader { if let Some(highest_processed_block_height) = self.highest_processed_block_height.as_ref() { - if &block.header.chain_length < highest_processed_block_height { + if &block.header.chain_length <= highest_processed_block_height { // no need to continue this download debug!("Cancelling unconfirmed tenure download to {}: have processed block at height {} already", &self.naddr, highest_processed_block_height); finished_download = true; + last_block_index = Some(cnt); break; } } expected_block_id = &block.header.parent_block_id; + last_block_index = Some(cnt); + } + + // blocks after the last_block_index were not processed, so should be dropped + if let Some(last_block_index) = last_block_index { + tenure_blocks.truncate(last_block_index + 1); } if let Some(blocks) = self.unconfirmed_tenure_blocks.as_mut() { diff --git a/stackslib/src/net/inv/nakamoto.rs b/stackslib/src/net/inv/nakamoto.rs index 867be5a507..ceee9c0b12 100644 --- a/stackslib/src/net/inv/nakamoto.rs +++ b/stackslib/src/net/inv/nakamoto.rs @@ -248,8 +248,6 @@ impl InvGenerator { #[derive(Debug, PartialEq, Clone)] pub struct NakamotoTenureInv { - /// What state is the machine in? - pub state: NakamotoInvState, /// Bitmap of which tenures a peer has. /// Maps reward cycle to bitmap. pub tenures_inv: BTreeMap>, @@ -280,7 +278,6 @@ impl NakamotoTenureInv { neighbor_address: NeighborAddress, ) -> Self { Self { - state: NakamotoInvState::GetNakamotoInvBegin, tenures_inv: BTreeMap::new(), last_updated_at: 0, first_block_height, @@ -336,7 +333,8 @@ impl NakamotoTenureInv { /// Add in a newly-discovered inventory. /// NOTE: inventories are supposed to be aligned to the reward cycle - /// Returns true if we learned about at least one new tenure-start block + /// Returns true if the tenure bitvec has changed -- we either learned about a new tenure-start + /// block, or the remote peer "un-learned" it (e.g. due to a reorg). /// Returns false if not. pub fn merge_tenure_inv(&mut self, tenure_inv: BitVec<2100>, reward_cycle: u64) -> bool { // populate the tenures bitmap to we can fit this tenures inv @@ -368,7 +366,6 @@ impl NakamotoTenureInv { && (self.cur_reward_cycle >= cur_rc || !self.online) { test_debug!("Reset inv comms for {}", &self.neighbor_address); - self.state = NakamotoInvState::GetNakamotoInvBegin; self.online = true; self.start_sync_time = now; self.cur_reward_cycle = start_rc; @@ -475,13 +472,6 @@ impl NakamotoTenureInv { } } -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum NakamotoInvState { - GetNakamotoInvBegin, - GetNakamotoInvFinish, - Done, -} - /// Nakamoto inventory state machine pub struct NakamotoInvStateMachine { /// Communications links diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 1cead0306a..1da3af6622 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -2541,6 +2541,7 @@ pub mod test { &mut stacks_node.chainstate, &sortdb, old_stackerdb_configs, + config.connection_opts.num_neighbors, ) .expect("Failed to refresh stackerdb configs"); diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index fbb6c375ed..ca32fe0671 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -5366,6 +5366,7 @@ impl PeerNetwork { chainstate, sortdb, stacker_db_configs, + self.connection_opts.num_neighbors, )?; Ok(()) } diff --git a/stackslib/src/net/stackerdb/config.rs b/stackslib/src/net/stackerdb/config.rs index f2d8521ae4..5545aa46cd 100644 --- a/stackslib/src/net/stackerdb/config.rs +++ b/stackslib/src/net/stackerdb/config.rs @@ -292,6 +292,7 @@ impl StackerDBConfig { contract_id: &QualifiedContractIdentifier, tip: &StacksBlockId, signers: Vec<(StacksAddress, u32)>, + local_max_neighbors: u64, ) -> Result { let value = chainstate.eval_read_only(burn_dbconn, tip, contract_id, "(stackerdb-get-config)")?; @@ -365,11 +366,12 @@ impl StackerDBConfig { )); } - let max_neighbors = config_tuple + let mut max_neighbors = config_tuple .get("max-neighbors") .expect("FATAL: missing 'max-neighbors'") .clone() .expect_u128()?; + if max_neighbors > usize::MAX as u128 { let reason = format!( "Contract {} stipulates a maximum number of neighbors beyond usize::MAX", @@ -382,6 +384,16 @@ impl StackerDBConfig { )); } + if max_neighbors > u128::from(local_max_neighbors) { + warn!( + "Contract {} stipulates a maximum number of neighbors ({}) beyond locally-configured maximum {}; defaulting to locally-configured maximum", + contract_id, + max_neighbors, + local_max_neighbors, + ); + max_neighbors = u128::from(local_max_neighbors); + } + let hint_replicas_list = config_tuple .get("hint-replicas") .expect("FATAL: missing 'hint-replicas'") @@ -435,7 +447,7 @@ impl StackerDBConfig { )); } - if port < 1024 || port > ((u16::MAX - 1) as u128) { + if port < 1024 || port > u128::from(u16::MAX - 1) { let reason = format!( "Contract {} stipulates a port lower than 1024 or above u16::MAX - 1", contract_id @@ -446,11 +458,20 @@ impl StackerDBConfig { reason, )); } + // NOTE: port is now known to be in range [1024, 65535] let mut pubkey_hash_slice = [0u8; 20]; pubkey_hash_slice.copy_from_slice(&pubkey_hash_bytes[0..20]); let peer_addr = PeerAddress::from_slice(&addr_bytes).expect("FATAL: not 16 bytes"); + if peer_addr.is_in_private_range() { + debug!( + "Ignoring private IP address '{}' in hint-replicas", + &peer_addr.to_socketaddr(port as u16) + ); + continue; + } + let naddr = NeighborAddress { addrbytes: peer_addr, port: port as u16, @@ -475,6 +496,7 @@ impl StackerDBConfig { chainstate: &mut StacksChainState, sortition_db: &SortitionDB, contract_id: &QualifiedContractIdentifier, + max_neighbors: u64, ) -> Result { let chain_tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), sortition_db)? @@ -542,7 +564,14 @@ impl StackerDBConfig { // evaluate the contract for these two functions let signers = Self::eval_signer_slots(chainstate, &dbconn, contract_id, &chain_tip_hash)?; - let config = Self::eval_config(chainstate, &dbconn, contract_id, &chain_tip_hash, signers)?; + let config = Self::eval_config( + chainstate, + &dbconn, + contract_id, + &chain_tip_hash, + signers, + max_neighbors, + )?; Ok(config) } } diff --git a/stackslib/src/net/stackerdb/mod.rs b/stackslib/src/net/stackerdb/mod.rs index da3ffa4555..5774ab4817 100644 --- a/stackslib/src/net/stackerdb/mod.rs +++ b/stackslib/src/net/stackerdb/mod.rs @@ -267,6 +267,7 @@ impl StackerDBs { chainstate: &mut StacksChainState, sortdb: &SortitionDB, stacker_db_configs: HashMap, + num_neighbors: u64, ) -> Result, net_error> { let existing_contract_ids = self.get_stackerdb_contract_ids()?; let mut new_stackerdb_configs = HashMap::new(); @@ -288,15 +289,20 @@ impl StackerDBs { }) } else { // attempt to load the config from the contract itself - StackerDBConfig::from_smart_contract(chainstate, &sortdb, &stackerdb_contract_id) - .unwrap_or_else(|e| { - warn!( - "Failed to load StackerDB config"; - "contract" => %stackerdb_contract_id, - "err" => ?e, - ); - StackerDBConfig::noop() - }) + StackerDBConfig::from_smart_contract( + chainstate, + &sortdb, + &stackerdb_contract_id, + num_neighbors, + ) + .unwrap_or_else(|e| { + warn!( + "Failed to load StackerDB config"; + "contract" => %stackerdb_contract_id, + "err" => ?e, + ); + StackerDBConfig::noop() + }) }; // Create the StackerDB replica if it does not exist already if !existing_contract_ids.contains(&stackerdb_contract_id) { diff --git a/stackslib/src/net/stackerdb/tests/config.rs b/stackslib/src/net/stackerdb/tests/config.rs index 9600ed79a8..a075d7b974 100644 --- a/stackslib/src/net/stackerdb/tests/config.rs +++ b/stackslib/src/net/stackerdb/tests/config.rs @@ -133,7 +133,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -152,7 +152,7 @@ fn test_valid_and_invalid_stackerdb_configs() { write_freq: 4, max_writes: 56, hint_replicas: vec![NeighborAddress { - addrbytes: PeerAddress::from_ipv4(127, 0, 0, 1), + addrbytes: PeerAddress::from_ipv4(142, 150, 80, 100), port: 8901, public_key_hash: Hash160::from_hex("0123456789abcdef0123456789abcdef01234567") .unwrap(), @@ -174,7 +174,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -193,7 +193,7 @@ fn test_valid_and_invalid_stackerdb_configs() { write_freq: 4, max_writes: 56, hint_replicas: vec![NeighborAddress { - addrbytes: PeerAddress::from_ipv4(127, 0, 0, 1), + addrbytes: PeerAddress::from_ipv4(142, 150, 80, 100), port: 8901, public_key_hash: Hash160::from_hex("0123456789abcdef0123456789abcdef01234567") .unwrap(), @@ -212,7 +212,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -234,7 +234,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -256,7 +256,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -278,7 +278,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -300,7 +300,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -322,7 +322,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -344,7 +344,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -366,7 +366,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -388,7 +388,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u18446744073709551617, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u8901, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -432,7 +432,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u1, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -454,7 +454,7 @@ fn test_valid_and_invalid_stackerdb_configs() { max-neighbors: u7, hint-replicas: (list { - addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u127 u0 u0 u1), + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100), port: u65537, public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 }) @@ -462,6 +462,44 @@ fn test_valid_and_invalid_stackerdb_configs() { "#, None, ), + ( + // valid, but private IP and absurd max neighbors are both handled + r#" + (define-public (stackerdb-get-signer-slots) + (ok (list { signer: 'ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B, num-slots: u3 }))) + + (define-public (stackerdb-get-config) + (ok { + chunk-size: u123, + write-freq: u4, + max-writes: u56, + max-neighbors: u1024, + hint-replicas: (list + { + addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u192 u168 u0 u1), + port: u8901, + public-key-hash: 0x0123456789abcdef0123456789abcdef01234567 + }) + })) + "#, + Some(StackerDBConfig { + chunk_size: 123, + signers: vec![( + StacksAddress { + version: 26, + bytes: Hash160::from_hex("b4fdae98b64b9cd6c9436f3b965558966afe890b") + .unwrap(), + }, + 3, + )], + write_freq: 4, + max_writes: 56, + // no neighbors + hint_replicas: vec![], + // max neighbors is truncated + max_neighbors: 32, + }), + ), ]; for (i, (code, _result)) in testcases.iter().enumerate() { @@ -490,7 +528,7 @@ fn test_valid_and_invalid_stackerdb_configs() { ContractName::try_from(format!("test-{}", i)).unwrap(), ); peer.with_db_state(|sortdb, chainstate, _, _| { - match StackerDBConfig::from_smart_contract(chainstate, sortdb, &contract_id) { + match StackerDBConfig::from_smart_contract(chainstate, sortdb, &contract_id, 32) { Ok(config) => { let expected = result .clone() diff --git a/stackslib/src/net/tests/download/nakamoto.rs b/stackslib/src/net/tests/download/nakamoto.rs index 47dabd176e..6e1e4c1bcb 100644 --- a/stackslib/src/net/tests/download/nakamoto.rs +++ b/stackslib/src/net/tests/download/nakamoto.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::sync::mpsc::sync_channel; use std::thread; @@ -438,6 +438,62 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { burn_height: peer.network.burnchain_tip.block_height, }; + // we can make unconfirmed tenure downloaders + { + let mut empty_schedule = VecDeque::new(); + let mut full_schedule = { + let mut sched = VecDeque::new(); + sched.push_back(naddr.clone()); + sched + }; + let mut empty_downloaders = HashMap::new(); + let mut full_downloaders = { + let mut dl = HashMap::new(); + let utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(tip_block_id)); + dl.insert(naddr.clone(), utd); + dl + }; + assert_eq!( + NakamotoDownloadStateMachine::make_unconfirmed_tenure_downloaders( + &mut empty_schedule, + 10, + &mut empty_downloaders, + None + ), + 0 + ); + assert_eq!( + NakamotoDownloadStateMachine::make_unconfirmed_tenure_downloaders( + &mut empty_schedule, + 10, + &mut full_downloaders, + None + ), + 0 + ); + assert_eq!( + NakamotoDownloadStateMachine::make_unconfirmed_tenure_downloaders( + &mut full_schedule, + 10, + &mut full_downloaders, + None + ), + 0 + ); + assert_eq!(full_schedule.len(), 1); + assert_eq!( + NakamotoDownloadStateMachine::make_unconfirmed_tenure_downloaders( + &mut full_schedule, + 10, + &mut empty_downloaders, + None + ), + 1 + ); + assert_eq!(full_schedule.len(), 0); + assert_eq!(empty_downloaders.len(), 1); + } + // we've processed the tip already, so we transition straight to the Done state { let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(tip_block_id)); @@ -855,6 +911,74 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .try_accept_unconfirmed_tenure_blocks(vec![bad_block]) .is_err()); } + + // Does not consume blocks beyond the highest processed block ID + { + let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), None); + utd.confirmed_aggregate_public_key = + Some(agg_pubkeys.get(&tip_rc).cloned().unwrap().unwrap()); + utd.unconfirmed_aggregate_public_key = + Some(agg_pubkeys.get(&tip_rc).cloned().unwrap().unwrap()); + + assert_eq!(utd.state, NakamotoUnconfirmedDownloadState::GetTenureInfo); + + let tenure_tip = RPCGetTenureInfo { + consensus_hash: peer.network.stacks_tip.0.clone(), + tenure_start_block_id: peer.network.tenure_start_block_id.clone(), + parent_consensus_hash: peer.network.parent_stacks_tip.0.clone(), + parent_tenure_start_block_id: StacksBlockId::new( + &peer.network.parent_stacks_tip.0, + &peer.network.parent_stacks_tip.1, + ), + tip_block_id: StacksBlockId::new( + &peer.network.stacks_tip.0, + &peer.network.stacks_tip.1, + ), + tip_height: peer.network.stacks_tip.2, + reward_cycle: tip_rc, + }; + + let sortdb = peer.sortdb.take().unwrap(); + let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); + utd.try_accept_tenure_info( + &sortdb, + &sort_tip, + peer.chainstate(), + tenure_tip.clone(), + &agg_pubkeys, + ) + .unwrap(); + + peer.sortdb = Some(sortdb); + + assert!(utd.unconfirmed_tenure_start_block.is_some()); + + utd.highest_processed_block_id = Some(unconfirmed_tenure[1].header.block_id()); + let res = utd + .try_accept_unconfirmed_tenure_blocks( + unconfirmed_tenure.clone().into_iter().rev().collect(), + ) + .unwrap(); + assert_eq!(res.unwrap().as_slice(), &unconfirmed_tenure[1..]); + + assert_eq!(utd.state, NakamotoUnconfirmedDownloadState::Done); + + // we can request the highest-complete tenure + assert!(!utd.need_highest_complete_tenure(peer.chainstate()).unwrap()); + + let ntd = utd + .make_highest_complete_tenure_downloader( + &highest_confirmed_wanted_tenure, + &unconfirmed_wanted_tenure, + ) + .unwrap(); + assert_eq!( + ntd.state, + NakamotoTenureDownloadState::GetTenureStartBlock( + unconfirmed_wanted_tenure.winning_block_id.clone() + ) + ); + } } #[test] diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index ad02341343..c101da090d 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -2320,7 +2320,7 @@ impl Default for MinerConfig { first_attempt_time_ms: 10, subsequent_attempt_time_ms: 120_000, microblock_attempt_time_ms: 30_000, - nakamoto_attempt_time_ms: 10_000, + nakamoto_attempt_time_ms: 20_000, probability_pick_no_estimate_tx: 25, block_reward_recipient: None, segwit: false, diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index b6ac17e51e..a6b3035938 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -4610,7 +4610,12 @@ impl StacksNode { stackerdb_configs.insert(contract.clone(), StackerDBConfig::noop()); } let stackerdb_configs = stackerdbs - .create_or_reconfigure_stackerdbs(&mut chainstate, &sortdb, stackerdb_configs) + .create_or_reconfigure_stackerdbs( + &mut chainstate, + &sortdb, + stackerdb_configs, + config.connection_options.num_neighbors, + ) .unwrap(); let stackerdb_contract_ids: Vec = diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 3c7e422e8d..4ecddf6e78 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -46,7 +46,8 @@ use stacks::chainstate::stacks::boot::{ }; use stacks::chainstate::stacks::db::StacksChainState; use stacks::chainstate::stacks::miner::{BlockBuilder, BlockLimitFunction, TransactionResult}; -use stacks::chainstate::stacks::{StacksTransaction, TransactionPayload}; +use stacks::chainstate::stacks::{StacksTransaction, TransactionPayload, MAX_BLOCK_LEN}; +use stacks::core::mempool::MAXIMUM_MEMPOOL_TX_CHAINING; use stacks::core::{ StacksEpoch, StacksEpochId, BLOCK_LIMIT_MAINNET_10, HELIUM_BLOCK_LIMIT_20, PEER_VERSION_EPOCH_1_0, PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05, @@ -3928,3 +3929,299 @@ fn check_block_heights() { run_loop_thread.join().unwrap(); } + +/// Test config parameter `nakamoto_attempt_time_ms` +#[test] +#[ignore] +fn nakamoto_attempt_time() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let password = "12345".to_string(); + naka_conf.connection_options.block_proposal_token = Some(password.clone()); + // Use fixed timing params for this test + let nakamoto_attempt_time_ms = 20_000; + naka_conf.miner.nakamoto_attempt_time_ms = nakamoto_attempt_time_ms; + let stacker_sk = setup_stacker(&mut naka_conf); + + let sender_sk = Secp256k1PrivateKey::new(); + let sender_addr = tests::to_addr(&sender_sk); + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + 1_000_000_000, + ); + + let sender_signer_sk = Secp256k1PrivateKey::new(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100_000, + ); + + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + + // We'll need a lot of accounts for one subtest to avoid MAXIMUM_MEMPOOL_TX_CHAINING + struct Account { + nonce: u64, + privk: Secp256k1PrivateKey, + _address: StacksAddress, + } + let num_accounts = 1_000; + let init_account_balance = 1_000_000_000; + let account_keys = add_initial_balances(&mut naka_conf, num_accounts, init_account_balance); + let mut account = account_keys + .into_iter() + .map(|privk| { + let _address = tests::to_addr(&privk); + Account { + nonce: 0, + privk, + _address, + } + }) + .collect::>(); + + // only subscribe to the block proposal events + test_observer::spawn(); + let observer_port = test_observer::EVENT_OBSERVER_PORT; + naka_conf.events_observers.insert(EventObserverConfig { + endpoint: format!("localhost:{observer_port}"), + events_keys: vec![EventKeyType::BlockProposal], + }); + + let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone()); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, + naka_submitted_vrfs: vrfs_submitted, + naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, + .. + } = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::spawn(move || run_loop.start(None, 0)); + wait_for_runloop(&blocks_processed); + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk], + &[sender_signer_sk], + Some(&signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + blind_signer(&naka_conf, &signers, proposals_submitted); + + let burnchain = naka_conf.get_burnchain(); + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let (chainstate, _) = StacksChainState::open( + naka_conf.is_mainnet(), + naka_conf.burnchain.chain_id, + &naka_conf.get_chainstate_path_str(), + None, + ) + .unwrap(); + + let _block_height_pre_3_0 = + NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap() + .stacks_block_height; + + info!("Nakamoto miner started..."); + + // first block wakes up the run loop, wait until a key registration has been submitted. + next_block_and(&mut btc_regtest_controller, 60, || { + let vrf_count = vrfs_submitted.load(Ordering::SeqCst); + Ok(vrf_count >= 1) + }) + .unwrap(); + + // second block should confirm the VRF register, wait until a block commit is submitted + next_block_and(&mut btc_regtest_controller, 60, || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + Ok(commits_count >= 1) + }) + .unwrap(); + + // Mine 3 nakamoto tenures + for _ in 0..3 { + next_block_and_mine_commit( + &mut btc_regtest_controller, + 60, + &coord_channel, + &commits_submitted, + ) + .unwrap(); + } + + // TODO (hack) instantiate the sortdb in the burnchain + _ = btc_regtest_controller.sortdb_mut(); + + // ----- Setup boilerplate finished, test block proposal API endpoint ----- + + let mut sender_nonce = 0; + let tenure_count = 3; + let inter_blocks_per_tenure = 10; + + // Subtest 1 + // Mine nakamoto tenures with a few transactions + // Blocks should be produced at least every 20 seconds + for _ in 0..tenure_count { + let commits_before = commits_submitted.load(Ordering::SeqCst); + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + let mut last_tip = BlockHeaderHash([0x00; 32]); + let mut last_tip_height = 0; + + // mine the interim blocks + for _ in 0..inter_blocks_per_tenure { + let blocks_processed_before = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + + let txs_per_block = 3; + let tx_fee = 500; + let amount = 500; + + for _ in 0..txs_per_block { + let transfer_tx = + make_stacks_transfer(&sender_sk, sender_nonce, tx_fee, &recipient, amount); + sender_nonce += 1; + submit_tx(&http_origin, &transfer_tx); + } + + // Sleep a bit longer than what our max block time should be + thread::sleep(Duration::from_millis(nakamoto_attempt_time_ms + 100)); + + // Miner should have made a new block by now + let blocks_processed = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + + assert!(blocks_processed > blocks_processed_before); + + let info = get_chain_info_result(&naka_conf).unwrap(); + assert_ne!(info.stacks_tip, last_tip); + assert_ne!(info.stacks_tip_height, last_tip_height); + + last_tip = info.stacks_tip; + last_tip_height = info.stacks_tip_height; + } + + let start_time = Instant::now(); + while commits_submitted.load(Ordering::SeqCst) <= commits_before { + if start_time.elapsed() >= Duration::from_secs(20) { + panic!("Timed out waiting for block-commit"); + } + thread::sleep(Duration::from_millis(100)); + } + } + + // Subtest 2 + // Confirm that no blocks are mined if there are no transactions + for _ in 0..2 { + let blocks_processed_before = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + + let info_before = get_chain_info_result(&naka_conf).unwrap(); + + // Wait long enough for a block to be mined + thread::sleep(Duration::from_millis(nakamoto_attempt_time_ms * 2)); + + let blocks_processed = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + + let info = get_chain_info_result(&naka_conf).unwrap(); + + // Assert that no block was mined while waiting + assert_eq!(blocks_processed, blocks_processed_before); + assert_eq!(info.stacks_tip, info_before.stacks_tip); + assert_eq!(info.stacks_tip_height, info_before.stacks_tip_height); + } + + // Subtest 3 + // Add more than `nakamoto_attempt_time_ms` worth of transactions into mempool + // Multiple blocks should be mined + for _ in 0..tenure_count { + let info_before = get_chain_info_result(&naka_conf).unwrap(); + + let blocks_processed_before = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + + let tx_limit = 10000; + let tx_fee = 500; + let amount = 500; + let mut tx_total_size = 0; + let mut tx_count = 0; + let mut acct_idx = 0; + + // Submit max # of txs from each account to reach tx_limit + 'submit_txs: loop { + let acct = &mut account[acct_idx]; + for _ in 0..MAXIMUM_MEMPOOL_TX_CHAINING { + let transfer_tx = + make_stacks_transfer(&acct.privk, acct.nonce, tx_fee, &recipient, amount); + submit_tx(&http_origin, &transfer_tx); + tx_total_size += transfer_tx.len(); + tx_count += 1; + acct.nonce += 1; + if tx_count >= tx_limit { + break 'submit_txs; + } + } + acct_idx += 1; + } + + // Make sure that these transactions *could* fit into a single block + assert!(tx_total_size < MAX_BLOCK_LEN as usize); + + // Wait long enough for 2 blocks to be made + thread::sleep(Duration::from_millis(nakamoto_attempt_time_ms * 2 + 100)); + + // Check that 2 blocks were made + let blocks_processed = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + + let blocks_mined = blocks_processed - blocks_processed_before; + assert!(blocks_mined > 2); + + let info = get_chain_info_result(&naka_conf).unwrap(); + assert_ne!(info.stacks_tip, info_before.stacks_tip); + assert_ne!(info.stacks_tip_height, info_before.stacks_tip_height); + } + + // ----- Clean up ----- + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +}