diff --git a/src/helpers.ts b/src/helpers.ts index 504cf20..3d6a644 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -35,6 +35,7 @@ import { Wallet, generateNewAccount, generateWallet } from '@stacks/wallet-sdk'; import { Toxiproxy } from 'toxiproxy-node-client'; import { ENV } from './env'; import { withRetry, withTimeout } from './utils'; +import { c32addressDecode } from 'c32check'; export function newSocketClient(): StacksApiSocketClient { return new StacksApiSocketClient({ @@ -218,6 +219,7 @@ export async function getStackerSet(cycle: number) { ).stacker_set; } +/** Uses the clarity map entries of the pox contract to get the reward set of a cycle */ export async function getTentativeStackerSet(cycle: number, poxInfo: PoxInfo) { const [contractAddress, contractName] = poxInfo.contract_id.split('.'); const lenTuple = (await getContractMapEntry({ @@ -317,6 +319,7 @@ export function getAccount(key: string) { network.isMainnet() ? NETWORK : TEST_NETWORK ) as string, client: new StackingClient(address, network), + hashBytes: c32addressDecode(address)[1], }; } @@ -509,6 +512,7 @@ export async function resumeProxy(name: ProxyName) { // BITCOIND RPC ================================================================ export const bitcoindClient = new RpcClient('http://btc:btc@localhost:18443').Typed; +// ============================================================================= export function getPubKeyHashFromTx(tx: string) { const transaction = btc.Transaction.fromRaw(hexToBytes(tx), { @@ -519,4 +523,3 @@ export function getPubKeyHashFromTx(tx: string) { const decodedScript = btc.Script.decode(input.finalScriptSig); return bytesToHex(decodedScript[1] as Uint8Array); } -// ============================================================================= diff --git a/src/tests/misc.test.ts b/src/tests/misc.test.ts index cb93035..9cad568 100644 --- a/src/tests/misc.test.ts +++ b/src/tests/misc.test.ts @@ -1,12 +1,4 @@ -import { - waitForBurnBlockHeight, - getStacksBlock, - getStacksBlockRaw, - getStacksBlockHeight, - bitcoindClient, - getPubKeyHashFromTx, -} from '../helpers'; -import { regtestComposeDown, regtestComposeUp } from '../utils'; +import { waitForBurnBlockHeight, getStacksBlock, getStacksBlockRaw } from '../helpers'; test('wip test', async () => { await waitForBurnBlockHeight(132); @@ -17,39 +9,3 @@ test('wip test', async () => { const blockRaw = await getStacksBlockRaw(block.height); console.log('stx block raw', blockRaw); }); - -test('signer rollover', async () => { - await waitForBurnBlockHeight(110); - console.log(await regtestComposeDown('stacker')); - console.log(await regtestComposeUp('stacker', '--env-file .env-signers-5')); - // cycle 5 (reward phase) - // stacker script should have stacked until 6 - // shut off stackers (in cycle 5) - // original signers can take on cycle 6 - // power up new stackers (in cycle 6) - // new stackers take on cycle 7 -}); - -test('multiple miners are active', async () => { - // PREP - await waitForBurnBlockHeight(109); - - const height = await getStacksBlockHeight(); - const range = Array.from({ length: height - 1 }, (_, i) => i + 1); - console.log('height', height, 'range', range.length); - - const pubKeyHashes = await Promise.all( - range.map(async height => { - const block = await getStacksBlock(height); - const tx = await bitcoindClient.getrawtransaction({ - txid: block.miner_txid.replace('0x', ''), - }); - return getPubKeyHashFromTx(tx as string); - }) - ); - - expect(range.length).toBeGreaterThan(0); - expect(pubKeyHashes.length).toBeGreaterThan(0); - - expect(new Set(pubKeyHashes).size).toBe(2); -}); diff --git a/src/tests/regtest-multiminer.test.ts b/src/tests/regtest-multiminer.test.ts new file mode 100644 index 0000000..7b97f3c --- /dev/null +++ b/src/tests/regtest-multiminer.test.ts @@ -0,0 +1,91 @@ +import { + bitcoindClient, + getPubKeyHashFromTx, + getStacksBlock, + getStacksBlockHeight, + stacksNetwork, + waitForBurnBlockHeight, + waitForNetwork, +} from '../helpers'; +import { networkEnvDown, networkEnvUp, regtestComposeDown, regtestComposeUp } from '../utils'; + +const network = stacksNetwork(); + +beforeEach(async () => { + await networkEnvUp(); + await waitForNetwork(); +}); + +afterEach(async () => { + await networkEnvDown(); +}); + +test('multiple miners are active', async () => { + // TEST CASE + // wait for some stacks blocks to be mined + // get the pubkey hashes from the stacks blocks + // ensure there are EXACTLY TWO unique pubkeys mining + + await waitForBurnBlockHeight(300); + + const height = await getStacksBlockHeight(); + const range = Array.from({ length: height - 1 }, (_, i) => i + 1); + + const pubKeyHashes = await Promise.all( + range.map(async height => { + const block = await getStacksBlock(height); + const tx = await bitcoindClient.getrawtransaction({ + txid: block.miner_txid.replace('0x', ''), + }); + return getPubKeyHashFromTx(tx as string); + }) + ); + + console.log('pubkey hashes length', pubKeyHashes.length); + + expect(range.length).toBeGreaterThan(0); + expect(pubKeyHashes.length).toBeGreaterThan(0); + + const uniques = new Set(pubKeyHashes); + console.log('unique pubkeys', uniques.size); + + const counts = pubKeyHashes.reduce( + (acc, hash) => { + acc[hash] = (acc[hash] || 0) + 1; + return acc; + }, + {} as Record + ); + console.log('miner counts', counts); + + expect(uniques.size).toBe(2); +}); + +test('miner recovers after restart in live network', async () => { + // TEST CASE + // two miner setup + // wait for some stacks blocks to be mined + // compose DOWN a miner (without signers or other event observers) + // wait for a few blocks + // compose UP the miner + // wait for a few blocks + // ensure the miner has caught up to the network + + await waitForBurnBlockHeight(120); + + await regtestComposeDown('stacks-miner-2'); + + await waitForBurnBlockHeight(130); + + await regtestComposeUp('stacks-miner-2'); + + await waitForBurnBlockHeight(140); + + const info1: any = await fetch('http://localhost:20443/v2/info').then(r => r.json()); + const info2: any = await fetch('http://localhost:40443/v2/info').then(r => r.json()); + + expect(info1.pox_consensus).toBe(info2.pox_consensus); + expect(info1.burn_block_height).toBe(info2.burn_block_height); + expect(info1.stacks_tip_height).toBe(info2.stacks_tip_height); + expect(info1.stacks_tip_consensus_hash).toBe(info2.stacks_tip_consensus_hash); +}); diff --git a/src/tests/regtest-signer.test.ts b/src/tests/regtest-signer.test.ts new file mode 100644 index 0000000..91e2ad0 --- /dev/null +++ b/src/tests/regtest-signer.test.ts @@ -0,0 +1,340 @@ +import { StackingClient } from '@stacks/stacking'; +import { + bitcoindClient, + getPubKeyHashFromTx, + getStacksBlock, + getStacksBlockHeight, + isInPreparePhase, + stacksNetwork, + waitForBurnBlockHeight, + waitForNetwork, + waitForNextCycle, +} from '../helpers'; +import { networkEnvDown, networkEnvUp, regtestComposeDown, regtestComposeUp } from '../utils'; + +const network = stacksNetwork(); + +beforeEach(async () => { + await networkEnvUp(); + await waitForNetwork(); +}); + +afterEach(async () => { + await networkEnvDown(); +}); + +test('signer rollover', async () => { + // TEST CASE + // some signers are active 3/5 (stacking) + // we start two additional signers 5/5 (stacking) + + const client = new StackingClient('', stacksNetwork()); + + // =========================================================================== + // CYCLE 8: + await waitForBurnBlockHeight(165); + expect((await client.getPoxInfo()).reward_cycle_id).toBe(8); + + // we're still in the reward phase + let info = await client.getPoxInfo(); + expect(isInPreparePhase(info.current_burnchain_block_height as number, info)).toBe(false); + + // cycle 8 (reward phase) + // stacker script should have stacked until 9 + const cycle8 = await getStacksBlock(); + console.log('cycle: 8; stx height:', cycle8.height, cycle8.hash); + + // shut off stackers (still in cycle 8) + console.log('shutting off stackers; stx height:', await getStacksBlockHeight()); + await regtestComposeDown('stacker'); + console.log('shut off all stackers; stx height:', await getStacksBlockHeight()); + + // =========================================================================== + // CYCLE 9: original signers can take on cycle 9 + await waitForNextCycle(await client.getPoxInfo()); + expect((await client.getPoxInfo()).reward_cycle_id).toBe(9); + + // we're still in the reward phase + info = await client.getPoxInfo(); + expect(isInPreparePhase(info.current_burnchain_block_height as number, info)).toBe(false); + + // cycle 9 (reward phase) + const cycle9 = await getStacksBlock(); + console.log('cycle: 9; stx height:', cycle9.height, cycle9.hash); + + // wait for a few blocks to make sure signers can sign cycle 9 + info = await client.getPoxInfo(); + await waitForBurnBlockHeight((info.current_burnchain_block_height as number) + 5); + // todo: make sure signer set is working + + // check pox is active + info = await client.getPoxInfo(); + expect(info.current_cycle.is_pox_active).toBe(true); + + // power up new stackers (in cycle 9) + console.log('powering up new stackers; stx height:', await getStacksBlockHeight()); + console.log(await regtestComposeUp('stacker', '--env-file .env-signers-5')); + console.log('powered up new stackers; stx height:', await getStacksBlockHeight()); + + // =========================================================================== + // CYCLE 10: new stackers take on cycle 10 + await waitForNextCycle(await client.getPoxInfo()); + expect((await client.getPoxInfo()).reward_cycle_id).toBe(10); + + // we're still in the reward phase + info = await client.getPoxInfo(); + expect(isInPreparePhase(info.current_burnchain_block_height as number, info)).toBe(false); + + // cycle 10 (reward phase) + const cycle10 = await getStacksBlock(); + console.log('cycle: 10; stx height:', cycle10.height, cycle10.hash); + + // check pox is active + info = await client.getPoxInfo(); + expect(info.current_cycle.is_pox_active).toBe(true); + + // =========================================================================== + // make sure 2 cycles from now everything still works + await waitForNextCycle(await client.getPoxInfo()); + await waitForNextCycle(await client.getPoxInfo()); + + // todo: check signer set is different +}); + +// import * as P from 'micro-packed'; +// import { sha512_256 } from '@noble/hashes/sha2'; + +// export type BitVec = { +// length: number; +// bytes: Uint8Array; +// }; + +// function dataLen(_len: number) { +// const len = BigInt(_len); +// const extra = len % 8n === 0n ? 0n : 1n; +// return len / 8n + extra; +// } + +// export const BitVecCoder = P.wrap({ +// encodeStream: (w: P.Writer, data: BitVec) => { +// // const len = data.length; +// P.U16BE.encodeStream(w, data.length); +// P.U32BE.encodeStream(w, data.bytes.length); +// w.bytes(data.bytes); +// }, +// decodeStream: (r: P.Reader) => { +// const len = P.U16BE.decodeStream(r); +// // console.log("len", len); +// const bytesLen = dataLen(len); +// // console.log("bytesLen", bytesLen); +// const bytes = P.bytes(P.U32BE).decodeStream(r); +// return { +// length: len, +// bytes, +// }; +// }, +// }); + +// export const SignerMessageTypePrefix = { +// /// Block Proposal message from miners +// BlockProposal: 0, +// /// Block Response message from signers +// BlockResponse: 1, +// /// Block Pushed message from miners +// BlockPushed: 2, +// /// Mock block proposal message from Epoch 2.5 miners +// MockProposal: 3, +// /// Mock block signature message from Epoch 2.5 signers +// MockSignature: 4, +// /// Mock block message from Epoch 2.5 miners +// MockBlock: 5, +// } as const; + +// export type SignerMessageTypeId = +// (typeof SignerMessageTypePrefix)[keyof typeof SignerMessageTypePrefix]; + +// export function TypeIdCoder(id: T): P.CoderType { +// return P.wrap({ +// encodeStream: (w: P.Writer, value: T) => w.byte(value), +// decodeStream: (r: P.Reader): T => { +// const value = r.byte(); +// return value as T; +// }, +// validate: (value: T) => { +// if (typeof value !== 'number') throw new Error(`TypeIdCoder: invalid value ${value}`); +// if (value !== id) throw new Error(`TypeIdCoder: invalid value ${value}`); +// return value; +// }, +// }); +// } + +// export const _TypeIdCoder = (id: SignerMessageTypeId): P.CoderType => +// P.wrap({ +// encodeStream: (w: P.Writer, value: SignerMessageTypeId) => w.byte(value), +// decodeStream: (r: P.Reader): SignerMessageTypeId => { +// const value = r.byte(); +// return value as SignerMessageTypeId; +// }, +// validate: (value: SignerMessageTypeId) => { +// if (typeof value !== 'number') throw new Error(`TypeIdCoder: invalid value ${value}`); +// if (value !== id) throw new Error(`TypeIdCoder: invalid value ${value}`); +// return value; +// }, +// }); + +// export const MockProposalCoder = P.struct({ +// typeId: TypeIdCoder(SignerMessageTypePrefix.MockProposal), +// burnBlockHeight: P.U64BE, +// consensusHash: P.bytes(20), +// stacksTip: P.bytes(32), +// stacksHeight: P.U64BE, +// server: P.prefix(P.U8, P.string(null)), +// poxConsensus: P.bytes(20), +// chainId: P.U32BE, +// signature: P.bytes(65), +// }); + +// export const MockBlockCoder = P.struct({ +// typeId: TypeIdCoder(SignerMessageTypePrefix.MockBlock), +// proposal: MockProposalCoder, +// signatures: P.array(P.U32BE, P.bytes(65)), +// rest: P.bytes(null), +// }); + +// export const BlockProposalCoder = P.struct({ +// typeId: TypeIdCoder(SignerMessageTypePrefix.BlockProposal), +// blockVersion: P.U8, +// chainLength: P.U64BE, +// burnSpent: P.U64BE, +// consensusHash: P.bytes(20), +// parentBlockId: P.bytes(32), +// txMerkleRoot: P.bytes(32), +// stateRoot: P.bytes(32), +// timestamp: P.U64BE, +// minerSignature: P.bytes(65), +// signerSignature: P.array(P.U32BE, P.bytes(65)), +// bitvec: BitVecCoder, +// // txs +// // burn height +// // reward cycle +// rest: P.bytes(null), +// }); + +// export const BlockPushedCoder = P.struct({ +// typeId: TypeIdCoder(SignerMessageTypePrefix.BlockPushed), +// blockVersion: P.U8, +// chainLength: P.U64BE, +// burnSpent: P.U64BE, +// consensusHash: P.bytes(20), +// parentBlockId: P.bytes(32), +// txMerkleRoot: P.bytes(32), +// stateRoot: P.bytes(32), +// timestamp: P.U64BE, +// minerSignature: P.bytes(65), +// signerSignature: P.array(P.U32BE, P.bytes(65)), +// bitvec: BitVecCoder, +// // txs +// rest: P.bytes(null), +// }); + +// export const SignerSignatureHashCoder = P.struct({ +// blockVersion: P.U8, +// chainLength: P.U64BE, +// burnSpent: P.U64BE, +// consensusHash: P.bytes(20), +// parentBlockId: P.bytes(32), +// txMerkleRoot: P.bytes(32), +// stateRoot: P.bytes(32), +// timestamp: P.U64BE, +// minerSignature: P.bytes(65), +// bitvec: BitVecCoder, +// }); + +// export const InverseBoolCoder: P.CoderType = /* @__PURE__ */ P.wrap({ +// size: 1, +// encodeStream: (w: P.Writer, value: boolean) => w.byte(value ? 0 : 1), +// decodeStream: (r: P.Reader): boolean => { +// const value = r.byte(); +// if (value !== 0 && value !== 1) throw r.err(`bool: invalid value ${value}`); +// return value === 0; +// }, +// validate: value => { +// if (typeof value !== 'boolean') throw new Error(`bool: invalid value ${value}`); +// return value; +// }, +// }); + +// export const BlockResponseAcceptedCoder = P.struct({ +// typeId: TypeIdCoder(SignerMessageTypePrefix.BlockResponse), +// accepted: InverseBoolCoder, +// signerSignatureHash: P.bytes(32), +// signature: P.bytes(65), +// }); + +// export const BlockResponseRejectedCoder = P.struct({ +// typeId: TypeIdCoder(SignerMessageTypePrefix.BlockResponse), +// accepted: InverseBoolCoder, +// reason: P.prefix(P.U32BE, P.string(null)), +// reason_code: P.U8, +// signerSignatureHash: P.bytes(32), +// chain_id: P.U32BE, +// signature: P.bytes(65), +// }); + +// export type MockBlock = P.UnwrapCoder; +// export type MockProposal = P.UnwrapCoder; +// export type BlockProposal = P.UnwrapCoder; +// export type BlockResponseAccepted = P.UnwrapCoder; +// export type BlockResponseRejected = P.UnwrapCoder; +// export type BlockPushed = P.UnwrapCoder; + +// export type NakamotoBlock = Omit; + +// export function chunkTypeName(chunk: Uint8Array) { +// const type = chunk[0]; +// return Object.keys(SignerMessageTypePrefix)[type]; +// } + +// export type DecodedChunk = ReturnType; + +// export function decodeChunk(chunk: Uint8Array) { +// const type = chunk[0]; +// // console.log(Object.keys(SignerMessageTypePrefix)); +// // console.log("Type: ", type); +// // console.log(Object.keys(SignerMessageTypePrefix)[type]); +// if (type === SignerMessageTypePrefix.MockBlock) { +// return MockBlockCoder.decode(chunk); +// } +// if (type === SignerMessageTypePrefix.MockProposal) { +// return MockProposalCoder.decode(chunk); +// } +// if (type === SignerMessageTypePrefix.BlockResponse) { +// const accepted = chunk[1]; +// if (accepted === 0) { +// return BlockResponseAcceptedCoder.decode(chunk); +// } +// return BlockResponseRejectedCoder.decode(chunk); +// } +// if (type === SignerMessageTypePrefix.BlockProposal) { +// const { rest: remaining, ...proposal } = BlockProposalCoder.decode(chunk); +// const withoutTxs = remaining.slice(-16); +// const extra = P.struct({ +// blockHeight: P.U64BE, +// rewardCycle: P.U64BE, +// }).decode(withoutTxs); +// return { +// ...proposal, +// blockHeight: extra.blockHeight, +// rewardCycle: extra.rewardCycle, +// }; +// } +// if (type === SignerMessageTypePrefix.BlockPushed) { +// return BlockPushedCoder.decode(chunk); +// } +// throw new Error(`Unknown type: ${type} (${Object.keys(SignerMessageTypePrefix)[type]})`); +// } + +// export function makeSignerSignatureHash(block: Omit) { +// const message = SignerSignatureHashCoder.encode(block); +// return sha512_256(message); +// } diff --git a/src/tests/regtest-total-locked.test.ts b/src/tests/regtest-total-locked.test.ts new file mode 100644 index 0000000..f4e0090 --- /dev/null +++ b/src/tests/regtest-total-locked.test.ts @@ -0,0 +1,167 @@ +import { Cl, getContractMapEntry, OptionalCV, UIntCV } from '@stacks/transactions'; +import { ENV } from '../env'; +import { + bitcoindClient, + getAccount, + getPubKeyHashFromTx, + getStacksBlock, + getStacksBlockHeight, + getTentativeStackerSet, + stacksNetwork, + waitForBurnBlockHeight, + waitForNetwork, + waitForNextCycle, + waitForRewardPhase, + waitForTransaction, +} from '../helpers'; +import { networkEnvDown, networkEnvUp, regtestComposeDown, regtestComposeUp } from '../utils'; +import { poxAddressToTuple, PoxInfo, StackingClient } from '@stacks/stacking'; +import { timeout } from '@hirosystems/api-toolkit'; + +const network = stacksNetwork(); + +beforeEach(async () => { + await networkEnvUp(); + await waitForNetwork(); +}); + +afterEach(async () => { + await networkEnvDown(); +}); + +test('total locked is incorrect', async () => { + // TEST CASE + // alice stacks minimum times 13 stx + // bob stacks minumum times 3 stx (less than alice) to the same pox address + // check total locked + + const alice = getAccount(ENV.PRIVATE_KEYS[0]); + const bob = getAccount(ENV.PRIVATE_KEYS[1]); + const signer = getAccount(ENV.PRIVATE_KEYS[2]); + + const poxAddress = alice.btcAddress; + console.log('pox address', poxAddress, alice.hashBytes); + + // PREP + const client = new StackingClient('', network); + + await waitForBurnBlockHeight(140); // nakamoto + + await regtestComposeDown('stacker'); // ensure signers aren't changing their stacking + + let poxInfo = await client.getPoxInfo(); + await waitForRewardPhase(poxInfo, +10); + + poxInfo = await client.getPoxInfo(); + const poxInfoPreAlice = poxInfo; + + const minAmount = BigInt(poxInfo.min_amount_ustx); + const aliceAmount = minAmount * 11n; + const bobAmount = minAmount * 3n; + const totalAmount = aliceAmount + bobAmount; + + expect(aliceAmount).toBeGreaterThan(bobAmount); + expect(bobAmount).toBeGreaterThan(0n); + + console.log('reward cycle id', poxInfo.reward_cycle_id); + + // TRANSACTION (alice stack) + const aliceSignature = client.signPoxSignature({ + topic: 'stack-stx', + period: 1, + rewardCycle: poxInfo.reward_cycle_id, + poxAddress, + signerPrivateKey: alice.signerPrivateKey, + maxAmount: aliceAmount, + authId: 0, + }); + const { txid: aliceStack } = await alice.client.stack({ + amountMicroStx: aliceAmount, + poxAddress, + cycles: 1, + burnBlockHeight: poxInfo.current_burnchain_block_height, + signerKey: alice.signerPublicKey, + signerSignature: aliceSignature, + maxAmount: aliceAmount, + authId: 0, + privateKey: alice.key, + }); + const aliceStackTx = await waitForTransaction(aliceStack); + expect(aliceStackTx.tx_result.repr).toContain('(ok'); + expect(aliceStackTx.tx_status).toBe('success'); + + await timeout(500); + poxInfo = await client.getPoxInfo(); + + let stackerSet = await getTentativeStackerSet(poxInfo.reward_cycle_id + 1, poxInfo); + let stackerMap = new Map(stackerSet.map(s => [s['pox-addr'].match(/[a-f0-9]{40}/)?.at(0), s])); + console.log('stacker set (w/ alice)', stackerSet); + + expect(stackerMap.get(alice.hashBytes)).toBeDefined(); + expect(stackerMap.get(bob.hashBytes)).not.toBeDefined(); + expect(stackerMap.get(alice.hashBytes)?.['total-ustx']).toBe(`u${aliceAmount}`); // as expected + + const poxInfoPreBob = poxInfo; + const stackerSetTotalPreBob = stackerSet.reduce( + (acc, s) => acc + BigInt(s['total-ustx'].replace('u', '')), + 0n + ); + expect(stackerSetTotalPreBob).toBe(BigInt(poxInfo.next_cycle.stacked_ustx)); + console.log('stacker set total pre bob', stackerSetTotalPreBob); + + // TRANSACTION (bob stack) + const bobSignature = client.signPoxSignature({ + topic: 'stack-stx', + period: 1, + rewardCycle: poxInfo.reward_cycle_id, + poxAddress, + signerPrivateKey: bob.signerPrivateKey, + maxAmount: bobAmount, + authId: 0, + }); + const { txid: bobStack } = await bob.client.stack({ + amountMicroStx: bobAmount, + poxAddress, + cycles: 1, + burnBlockHeight: poxInfo.current_burnchain_block_height, + signerKey: bob.signerPublicKey, + signerSignature: bobSignature, + maxAmount: bobAmount, + authId: 0, + privateKey: bob.key, + }); + const bobStackTx = await waitForTransaction(bobStack); + expect(bobStackTx.tx_result.repr).toContain('(ok'); + expect(bobStackTx.tx_status).toBe('success'); + + await timeout(500); + poxInfo = await client.getPoxInfo(); + + stackerSet = await getTentativeStackerSet(poxInfo.reward_cycle_id + 1, poxInfo); + stackerMap = new Map(stackerSet.map(s => [s['pox-addr'].match(/[a-f0-9]{40}/)?.at(0), s])); + console.log('stacker set (w/ alice & bob)', stackerSet); + + expect(stackerMap.get(alice.hashBytes)).toBeDefined(); + expect(stackerMap.get(alice.hashBytes)?.['total-ustx']).toBe(`u${bobAmount}`); // bob used alices pox address, and overrode her stack/reward + expect(stackerMap.get(alice.hashBytes)?.signer).toBe(`0x${bob.signerPublicKey}`); + + const poxInfoPostBob = poxInfo; + const stackerSetTotalPostBob = stackerSet.reduce( + (acc, s) => acc + BigInt(s['total-ustx'].replace('u', '')), + 0n + ); + console.log('stacker set total post bob', stackerSetTotalPostBob); + + expect(stackerSetTotalPostBob).toBeLessThan(stackerSetTotalPreBob); // stacker/reward set total went down + + expect(stackerSetTotalPostBob).not.toBe(BigInt(poxInfo.next_cycle.stacked_ustx)); + + expect(poxInfoPostBob.next_cycle.stacked_ustx).toBeLessThan( + poxInfoPreBob.next_cycle.stacked_ustx // total went down?! + ); + + console.log('reward cycle id', poxInfo.reward_cycle_id); +}); + +// alice u8892290000000000 +// bob u2425170000000000 diff --git a/src/tests/regtest.test.ts b/src/tests/regtest.test.ts index e34f4a2..10149d8 100644 --- a/src/tests/regtest.test.ts +++ b/src/tests/regtest.test.ts @@ -33,7 +33,7 @@ import { } from '../helpers'; import { networkEnvDown, networkEnvUp } from '../utils'; -jest.retryTimes(3); +// jest.retryTimes(3); describe('regtest-env pox-4', () => { const network = stacksNetwork(); @@ -3080,40 +3080,40 @@ describe('regtest-env pox-4', () => { }); }); -// ✓ stack-stx (in reward-phase) (203603 ms) -// ✕ stack-stx (before prepare-phase) (52573 ms) -// ✕ stack-stx (on prepare-phase start) (68493 ms) -// ✓ stack-stx (in prepare-phase) (156150 ms) -// ✓ stack-stx (reward-phase), stack-extend (reward-phase) (401007 ms) -// ✓ stack-stx (reward-phase), stack-extend (prepare-phase) (308613 ms) -// ✕ stack-stx (reward-phase), stack-increase (reward-phase) (309899 ms) -// ✓ stack-stx (reward-phase), stack-increase (prepare-phase) (292046 ms) -// ✓ pool: delegate-stack, agg-increase (prepare-phase) (61738 ms) -// ✓ pool: agg increase over maxAmount (68536 ms) -// ✓ pool: delegate with invalid hashbyte length (200336 ms) -// ✓ Pool delegate can only delegate-stack-stx for the next cycle (40894 ms) -// ✓ Cannot stack if delegating (39574 ms) -// ✓ Pool delegate cannot delegate-stack-stx if already stacking (44710 ms) -// ✓ Pool delegate cannot delegate-stack-stx more STX than what delegator has explicitly allowed (41472 ms) -// ✓ Pool delegate cannot delegate-stack-stx on behalf of a delegator that delegated to another pool (40864 ms) -// ✓ Pool delegate cannot delegate-stack-stx for the current cycle (37921 ms) -// ✓ Pool delegate cannot delegate-stack-stx to an un-delegated entity (34515 ms) -// ✓ Pool stacker, if actively stacked, cannot revoke delegate status for the current reward cycle (66307 ms) -// ✓ Pool can pre-approve a signature for participants (49776 ms) -// ✓ Stacker switches signers for stack-increase (41325 ms) -// ✓ Stacker switches signers for stack-extend (42526 ms) -// ✓ Call readonly with weird string (38611 ms) -// ✓ Pool stacker can delegate-stx, Pool stacker cannot submit an invalid pox-addr-version (38901 ms) -// ✓ Pool stacker cannot delegate to two pool operators at once (48660 ms) -// ✓ Revoke fails if stacker is not currently delegated (37214 ms) -// ✓ Pool delegate can successfully provide a stacking lock for a pool stacker (delegate-stack-stx) (151942 ms) -// ✓ Pool delegate cannot delegate-stack-stx to an un-delegated solo stacker (42282 ms) -// ✓ Pool delegate cannot delegate-stack-stx for the current cycle (44362 ms) -// ✓ Pool delegate cannot delegate-stack-stx more STX than what delegator has explicitly allowed (43610 ms) -// ✓ Pool delegate cannot change the pox-addr provided by delegator (39571 ms) -// ✓ Pool delegate cannot delegate-stack-stx if the delegation expires before the next cycle ends (45491 ms) -// ✓ Pool delegate-stack-stx fails if the delegator has insufficient balance (39176 ms) -// ✓ Pool delegate cannot delegate-stack 0 stx, Pool delegate cannot delegate-stack-stx for 0 cycles, Pool delegate cannot delegate-stack-stx for > 12 cycles (46677 ms) -// ✓ Pool delegate cannot submit an invalid pox-addr-ver (40326 ms) -// ✓ Pool stacker can revoke delegate status (revoke-delegate-stx) (46296 ms) -// ✓ Pool delegate can successfully delegate-stack-extend (42510 ms) +// ✓ stack-stx (in reward-phase) (214993 ms) +// ✕ stack-stx (before prepare-phase) (212341 ms) +// ✓ stack-stx (on prepare-phase start) (173151 ms) +// ✓ stack-stx (in prepare-phase) (168200 ms) +// ✓ stack-stx (reward-phase), stack-extend (reward-phase) (457973 ms) +// ✓ stack-stx (reward-phase), stack-extend (prepare-phase) (321140 ms) +// ✕ stack-stx (reward-phase), stack-increase (reward-phase) (77181 ms) +// ✓ stack-stx (reward-phase), stack-increase (prepare-phase) (335007 ms) +// ✓ pool: delegate-stack, agg-increase (prepare-phase) (64761 ms) +// ✓ pool: agg increase over maxAmount (70097 ms) +// ✓ pool: delegate with invalid hashbyte length (222699 ms) +// ✓ Pool delegate can only delegate-stack-stx for the next cycle (42379 ms) +// ✓ Cannot stack if delegating (42083 ms) +// ✓ Pool delegate cannot delegate-stack-stx if already stacking (46055 ms) +// ✕ Pool delegate cannot delegate-stack-stx more STX than what delegator has explicitly allowed (45939 ms) +// ✓ Pool delegate cannot delegate-stack-stx on behalf of a delegator that delegated to another pool (38606 ms) +// ✓ Pool delegate cannot delegate-stack-stx for the current cycle (42371 ms) +// ✓ Pool delegate cannot delegate-stack-stx to an un-delegated entity (36334 ms) +// ✕ Pool stacker, if actively stacked, cannot revoke delegate status for the current reward cycle (48338 ms) +// ✓ Pool can pre-approve a signature for participants (54701 ms) +// ✓ Stacker switches signers for stack-increase (40429 ms) +// ✓ Stacker switches signers for stack-extend (43000 ms) +// ✓ Call readonly with weird string (40015 ms) +// ✓ Pool stacker can delegate-stx, Pool stacker cannot submit an invalid pox-addr-version (41038 ms) +// ✓ Pool stacker cannot delegate to two pool operators at once (42404 ms) +// ✓ Revoke fails if stacker is not currently delegated (38524 ms) +// ✕ Pool delegate can successfully provide a stacking lock for a pool stacker (delegate-stack-stx) (41756 ms) +// ✓ Pool delegate cannot delegate-stack-stx to an un-delegated solo stacker (45076 ms) +// ✓ Pool delegate cannot delegate-stack-stx for the current cycle (53483 ms) +// ✓ Pool delegate cannot delegate-stack-stx more STX than what delegator has explicitly allowed (47335 ms) +// ✓ Pool delegate cannot change the pox-addr provided by delegator (42008 ms) +// ✓ Pool delegate cannot delegate-stack-stx if the delegation expires before the next cycle ends (43200 ms) +// ✓ Pool delegate-stack-stx fails if the delegator has insufficient balance (46139 ms) +// ✓ Pool delegate cannot delegate-stack 0 stx, Pool delegate cannot delegate-stack-stx for 0 cycles, Pool delegate cannot delegate-stack-stx for > 12 cycles (60007 ms) +// ✓ Pool delegate cannot submit an invalid pox-addr-ver (42065 ms) +// ✓ Pool stacker can revoke delegate status (revoke-delegate-stx) (40824 ms) +// ✓ Pool delegate can successfully delegate-stack-extend (46788 ms) diff --git a/src/tests/scripts.test.ts b/src/tests/scripts.test.ts index c9d35e1..0b0f605 100644 --- a/src/tests/scripts.test.ts +++ b/src/tests/scripts.test.ts @@ -1,8 +1,19 @@ -import { StacksDevnet } from '@stacks/network'; +import { StacksDevnet, StacksMainnet, StacksTestnet } from '@stacks/network'; import { StackingClient } from '@stacks/stacking'; import { c32addressDecode } from 'c32check'; import { ENV } from '../env'; -import { getAccount, getRewardSlots, getTentativeStackerSet, getTransactions } from '../helpers'; +import { + getAccount, + getRewardSlots, + getStacksBlock, + getTentativeStackerSet, + getTransactions, + stacksNetwork, + waitForTransaction, +} from '../helpers'; +import { callReadOnlyFunction, Cl } from '@stacks/transactions'; +import { Configuration, SmartContractsApi } from '@stacks/blockchain-api-client'; +import { bytesToHex } from '@stacks/common'; test('get info', async () => { const steph = getAccount(ENV.PRIVATE_KEYS[0]); @@ -18,7 +29,7 @@ test('get info', async () => { }); test('get account', async () => { - const steph = getAccount('7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01'); + const steph = getAccount(ENV.PRIVATE_KEYS[0]); console.log(steph); const balances = await steph.client.getAccountExtendedBalances(); console.log(balances); @@ -53,3 +64,124 @@ test('log all account addresses', () => { const addresses = ENV.PRIVATE_KEYS.map(key => getAccount(key).address); console.log(addresses); }); + +test('stack for Steph (nakamoto)', async () => { + const steph = getAccount(ENV.PRIVATE_KEYS[0]); + const client = new StackingClient( + steph.address, + new StacksTestnet({ + url: 'https://api.nakamoto-1.hiro.so', + }) + ); + + // Get POX info + const poxInfo = await client.getPoxInfo(); + + // Get the account balance and use it as the amount to stack + const balances = await client.getAccountExtendedBalances(); + const amount = BigInt(balances.stx.balance as number) - 10_000_000n; + + console.log('amount', amount); + + // Get the current burn block height + const burnBlockHeight = poxInfo.current_burnchain_block_height as number; + + // Create a signature for stacking + const authId = 2; // You can use a random number here if you prefer + const signature = client.signPoxSignature({ + topic: 'stack-stx', + period: 1, // Stack for 1 cycle + rewardCycle: poxInfo.reward_cycle_id, + poxAddress: steph.btcAddress, + signerPrivateKey: steph.signerPrivateKey, + maxAmount: amount, + authId, + }); + + // Perform the stacking transaction + const res = await client.stack({ + amountMicroStx: amount, + poxAddress: steph.btcAddress, + cycles: 1, + burnBlockHeight, + signerKey: steph.signerPublicKey, + signerSignature: signature, + maxAmount: amount, + authId, + privateKey: steph.key, + }); + + console.log('Stacking transaction ID:', res); + + // Wait for the transaction to be processed + const result = await waitForTransaction(res.txid); + + // Check if the stacking was successful + expect(result.tx_status).toBe('success'); + expect(result.tx_result.repr).toContain('(ok'); + + // Verify that Steph is now stacking + const stackingStatus = await client.getStatus(); + expect(stackingStatus.stacked).toBe(true); +}); + +test('stacker set total (nakamoto)', async () => { + const client = new StackingClient('', stacksNetwork()); + const poxInfo = await client.getPoxInfo(); + + console.log('reward cycle id', poxInfo.reward_cycle_id); + const stackerSet = await getTentativeStackerSet(poxInfo.reward_cycle_id, poxInfo); + const stackerSetTotal = stackerSet.reduce( + (acc, s) => acc + BigInt(s['total-ustx'].replace('u', '')), + 0n + ); + + console.log('total signers', stackerSet.length); + + console.log('stacker set total', stackerSetTotal); + console.log('current cycle total', poxInfo.current_cycle.stacked_ustx); + console.log('next cycle total', poxInfo.next_cycle.stacked_ustx); +}); + +test('read total stacked from pox contract', async () => { + const network = stacksNetwork(); + const alice = getAccount(ENV.PRIVATE_KEYS[0]); + const client = new StackingClient('', network); + const poxInfo = await client.getPoxInfo(); + + const config = new Configuration({ + basePath: ENV.STACKS_API, + }); + const api = new SmartContractsApi(config); + + const cycle = 93; + const tip = 165966; + + const [contractAddress, contractName] = poxInfo.contract_id.split('.'); + + const blockTip = await getStacksBlock(tip); + const rawWithTip = await api.callReadOnlyFunction({ + contractAddress, + contractName, + functionName: 'get-total-ustx-stacked', + readOnlyFunctionArgs: { + arguments: [bytesToHex(Cl.serialize(Cl.uint(cycle)))], + sender: alice.address, + }, + tip: blockTip.index_block_hash.replace('0x', ''), + }); + console.log(Cl.deserialize(rawWithTip.result as string)); + + const blockTipBefore = await getStacksBlock(tip - 1); + const rawBeforeTip = await api.callReadOnlyFunction({ + contractAddress, + contractName, + functionName: 'get-total-ustx-stacked', + readOnlyFunctionArgs: { + arguments: [bytesToHex(Cl.serialize(Cl.uint(cycle)))], + sender: alice.address, + }, + tip: blockTipBefore.index_block_hash.replace('0x', ''), + }); + console.log(Cl.deserialize(rawBeforeTip.result as string)); +});