Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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);
Testing block-dependent logic:
// 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

ModeBehavior
Automine ONEach transaction immediately gets its own block
Automine OFFTransactions wait in mempool until mine() is called
Interval MiningBlocks 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 block

Time 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

MethodAnvilHardhatGanache
mine()anvil_minehardhat_mineevm_mine
mine(blocks)anvil_minehardhat_mineevm_mine
mine(blocks, interval)anvil_minehardhat_mineevm_mine
getAutomine()anvil_getAutominehardhat_getAutomineevm_getAutomine
setAutomine()evm_setAutominehardhat_setAutomineevm_setAutomine
setIntervalMining()evm_setIntervalMininghardhat_setIntervalMiningevm_setIntervalMining
setNextBlockTimestamp()evm_setNextBlockTimestamphardhat_setNextBlockTimestampevm_setNextBlockTimestamp
increaseTime()evm_increaseTimehardhat_increaseTimeevm_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 timestamp

Avoid 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