Mining & Time Control
Overview
Brane.Tester provides precise control over block production and blockchain time. These capabilities are essential for testing:
- Time-dependent logic: Vesting schedules, lock periods, auction deadlines
- Transaction ordering: Batch multiple transactions into a single block
- Block timing: Simulate realistic block intervals or specific timestamps
- Mining behavior: Toggle between instant and manual block production
Mining Methods
mine()
Mines a single block immediately. This is the most common mining operation.
import sh.brane.rpc.Brane;
Brane.Tester tester = Brane.connectTest();
// Mine one block
tester.mine();mine(long blocks)
Mines multiple blocks at once.
// Mine 100 blocks
tester.mine(100);
// Useful for advancing block number without time changes
tester.mine(50);mine(long blocks, long intervalSeconds)
Mines multiple blocks with a specified time interval between each block. This simulates realistic block production with consistent block times.
// Mine 10 blocks with 12 seconds between each (like Ethereum mainnet)
tester.mine(10, 12);
// Mine 100 blocks with 2 second intervals (faster simulation)
tester.mine(100, 2);The total time advanced is approximately (blocks - 1) * intervalSeconds seconds, since the interval is applied between consecutive blocks.
mineAt(long timestamp)
Mines a single block with a specific timestamp.
// Mine a block at a specific Unix timestamp
long futureTime = System.currentTimeMillis() / 1000 + 86400; // 1 day from now
tester.mineAt(futureTime);Automine Control
Automine determines whether transactions are mined immediately upon submission. By default, automine is enabled on test nodes.
getAutomine()
Check the current automine status.
boolean isAutomine = tester.getAutomine();
System.out.println("Automine enabled: " + isAutomine);setAutomine(boolean enabled)
Enable or disable automatic mining.
// Disable automine
tester.setAutomine(false);
// ... transactions stay in mempool until mine() is called ...
// Re-enable automine
tester.setAutomine(true);Batching Transactions in a Single Block
Disabling automine allows you to batch multiple transactions into a single block:
// Disable automine to control block production
tester.setAutomine(false);
try {
// All transactions go into the mempool
Hash tx1 = tester.sendTransaction(request1);
Hash tx2 = tester.sendTransaction(request2);
Hash tx3 = tester.sendTransaction(request3);
// Mine them all in a single block
tester.mine();
// All three transactions are now in the same block
TransactionReceipt r1 = tester.getTransactionReceipt(tx1);
TransactionReceipt r2 = tester.getTransactionReceipt(tx2);
TransactionReceipt r3 = tester.getTransactionReceipt(tx3);
// Same block number for all
assert r1.blockNumber().equals(r2.blockNumber());
assert r2.blockNumber().equals(r3.blockNumber());
} finally {
// Always restore automine
tester.setAutomine(true);
}Interval Mining
Interval mining automatically produces blocks at a fixed time interval, simulating real network behavior without requiring manual mine() calls.
setIntervalMining(long intervalMs)
Configure automatic block production at a fixed interval.
// Mine a block every 12 seconds (like Ethereum mainnet)
tester.setIntervalMining(12_000);
// Mine a block every second (faster for testing)
tester.setIntervalMining(1_000);
// Disable interval mining (blocks only produced on mine() or transactions if automine is on)
tester.setIntervalMining(0);Use Cases
Simulating mainnet block timing:// Setup mainnet-like block production
tester.setIntervalMining(12_000); // 12 second blocks
// Now transactions will be included in blocks naturally
// without explicit mine() calls
Hash txHash = tester.sendTransaction(request);
// Wait for block (may take up to 12 seconds)
TransactionReceipt receipt = tester.waitForReceipt(txHash);// Test that requires blocks to be produced over time
tester.setIntervalMining(1_000); // 1 second blocks
// Start some time-based process
startVesting();
// Wait and let blocks accumulate
Thread.sleep(5_000);
// Check state after ~5 blocks
BigInteger vested = getVestedAmount();Interval Mining vs Automine
| Mode | Behavior |
|---|---|
| Automine ON | Each transaction immediately gets its own block |
| Automine OFF | Transactions wait in mempool until mine() is called |
| Interval Mining | Blocks produced automatically at fixed intervals |
You can combine these modes:
// Automine OFF + Interval Mining: blocks produced periodically, not per-transaction
tester.setAutomine(false);
tester.setIntervalMining(5_000);
// Now blocks are mined every 5 seconds
// Multiple transactions in that window share the same blockTime Manipulation
setNextBlockTimestamp(long timestamp)
Sets the timestamp for the next mined block. The timestamp must be greater than the current block's timestamp.
// Set the next block to be 1 day in the future
long futureTime = System.currentTimeMillis() / 1000 + 86400;
tester.setNextBlockTimestamp(futureTime);
tester.mine(); // Block is mined with the specified timestamp
// Verify the timestamp
BlockHeader block = tester.getLatestBlock();
System.out.println("Block timestamp: " + block.timestamp());increaseTime(long seconds)
Increases the blockchain time by a relative amount. Unlike setNextBlockTimestamp, this increments from the current time rather than setting an absolute value.
// Advance time by 1 hour
tester.increaseTime(3600);
tester.mine();
// Advance time by 30 days
tester.increaseTime(30 * 24 * 60 * 60);
tester.mine();Time Constants for Common Durations
// Useful constants for time manipulation
long ONE_HOUR = 3600L;
long ONE_DAY = 86400L;
long ONE_WEEK = 7 * ONE_DAY;
long THIRTY_DAYS = 30 * ONE_DAY;
long ONE_YEAR = 365 * ONE_DAY;
// Advance 30 days
tester.increaseTime(THIRTY_DAYS);
tester.mine();Common Patterns
Testing Vesting Contracts
Test time-based token unlocking:
@Test
void testVestingUnlock() {
Brane.Tester tester = Brane.connectTest();
SnapshotId snapshot = tester.snapshot();
try {
// Setup: create vesting position
Address beneficiary = new Address("0x...");
createVestingPosition(beneficiary, tokenAmount, THIRTY_DAYS);
// Initially, nothing should be vested
BigInteger vestedBefore = getVestedAmount(beneficiary);
assertEquals(BigInteger.ZERO, vestedBefore);
// Advance time past vesting period
tester.increaseTime(THIRTY_DAYS + 1);
tester.mine();
// Now tokens should be vested
BigInteger vestedAfter = getVestedAmount(beneficiary);
assertEquals(tokenAmount, vestedAfter);
// Beneficiary can now claim
TransactionReceipt receipt = claim(beneficiary);
assertTrue(receipt.status());
} finally {
tester.revert(snapshot);
}
}Testing Auction Deadlines
Test time-sensitive auction logic:
@Test
void testAuctionDeadline() {
Brane.Tester tester = Brane.connectTest();
SnapshotId snapshot = tester.snapshot();
try {
// Create auction ending in 1 hour
long auctionDuration = 3600L;
Address auctionContract = createAuction(auctionDuration);
// Place a bid (should succeed)
TransactionReceipt bid1 = placeBid(Wei.fromEther(new java.math.BigDecimal("1")));
assertTrue(bid1.status());
// Advance time past auction end
tester.increaseTime(auctionDuration + 1);
tester.mine();
// Bid after deadline should fail
assertThrows(RevertException.class, () -> placeBid(Wei.fromEther(new java.math.BigDecimal("2"))));
// Settlement should now succeed
TransactionReceipt settlement = settleAuction();
assertTrue(settlement.status());
} finally {
tester.revert(snapshot);
}
}Testing Timelock Guards
Test governance timelocks:
@Test
void testTimelockExecution() {
Brane.Tester tester = Brane.connectTest();
SnapshotId snapshot = tester.snapshot();
try {
long timelockDelay = 2 * ONE_DAY;
// Queue a governance action
Hash proposalId = queueProposal(action);
// Execution before timelock should fail
assertThrows(RevertException.class, () -> executeProposal(proposalId));
// Advance to just before timelock expires
tester.increaseTime(timelockDelay - 1);
tester.mine();
// Still too early
assertThrows(RevertException.class, () -> executeProposal(proposalId));
// Advance past timelock
tester.increaseTime(2);
tester.mine();
// Now execution succeeds
TransactionReceipt receipt = executeProposal(proposalId);
assertTrue(receipt.status());
} finally {
tester.revert(snapshot);
}
}Testing Transaction Ordering
Test scenarios where transaction order matters:
@Test
void testFrontrunningProtection() {
Brane.Tester tester = Brane.connectTest();
SnapshotId snapshot = tester.snapshot();
try {
// Disable automine to control ordering
tester.setAutomine(false);
// User submits swap
Hash userTx = tester.sendTransaction(swapRequest);
// Attacker tries to frontrun (submitted after but wants to execute first)
// In reality, this would have higher gas price
Hash attackerTx = tester.sendTransaction(frontrunRequest);
// Mine both in same block
tester.mine();
// Verify ordering and check if protection worked
TransactionReceipt userReceipt = tester.getTransactionReceipt(userTx);
TransactionReceipt attackerReceipt = tester.getTransactionReceipt(attackerTx);
// Assert the expected behavior based on your contract's protection mechanism
} finally {
tester.setAutomine(true);
tester.revert(snapshot);
}
}Simulating Block Production Over Time
Test behavior across many blocks:
@Test
void testRewardAccumulation() {
Brane.Tester tester = Brane.connectTest();
SnapshotId snapshot = tester.snapshot();
try {
// Stake tokens
stake(stakerAddress, stakeAmount);
// Simulate 100 blocks with 12-second intervals (like mainnet)
tester.mine(100, 12);
// Check accumulated rewards
BigInteger rewards = getPendingRewards(stakerAddress);
// Should have accumulated rewards over ~1200 seconds
assertTrue(rewards.compareTo(BigInteger.ZERO) > 0);
} finally {
tester.revert(snapshot);
}
}Test Node Compatibility
| Method | Anvil | Hardhat | Ganache |
|---|---|---|---|
mine() | anvil_mine | hardhat_mine | evm_mine |
mine(blocks) | anvil_mine | hardhat_mine | evm_mine |
mine(blocks, interval) | anvil_mine | hardhat_mine | evm_mine |
getAutomine() | anvil_getAutomine | hardhat_getAutomine | evm_getAutomine |
setAutomine() | evm_setAutomine | hardhat_setAutomine | evm_setAutomine |
setIntervalMining() | evm_setIntervalMining | hardhat_setIntervalMining | evm_setIntervalMining |
setNextBlockTimestamp() | evm_setNextBlockTimestamp | hardhat_setNextBlockTimestamp | evm_setNextBlockTimestamp |
increaseTime() | evm_increaseTime | hardhat_increaseTime | evm_increaseTime |
The correct RPC method is selected automatically based on the TestNodeMode configured when creating the tester.
Best Practices
Always Restore Mining State
When modifying automine or interval mining, restore the original state in a finally block:
boolean originalAutomine = tester.getAutomine();
try {
tester.setAutomine(false);
// Test code...
} finally {
tester.setAutomine(originalAutomine);
}Use Snapshots with Time Changes
Time manipulation affects global chain state. Always use snapshots to restore:
SnapshotId snapshot = tester.snapshot();
try {
tester.increaseTime(ONE_YEAR);
tester.mine();
// Test code...
} finally {
tester.revert(snapshot);
}Mine After Time Changes
Time manipulation methods like increaseTime() and setNextBlockTimestamp() only set the time for the next block. You must call mine() to produce a block with the new timestamp:
// Wrong - no block is mined with the new timestamp
tester.increaseTime(ONE_DAY);
// Current block still has old timestamp!
// Correct - mine a block to apply the timestamp
tester.increaseTime(ONE_DAY);
tester.mine();
// Now the latest block has the advanced timestampAvoid Time Travel Pitfalls
- Timestamps must always increase - you cannot set a timestamp in the past
- Large time jumps may affect contracts that calculate per-second rates
- Some protocols have maximum time jump limits for security
Consider Block Number vs Timestamp
Some contracts use block numbers, others use timestamps:
// For contracts using block.timestamp
tester.increaseTime(ONE_DAY);
tester.mine();
// For contracts using block.number
tester.mine(7200); // ~1 day at 12 second blocks
// For contracts using both, advance both appropriately
tester.mine(7200, 12); // 7200 blocks, 12 seconds each