Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions contracts/FlowTransactionScheduler.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "FlowToken"
import "FlowFees"
import "FlowStorageFees"
import "ViewResolver"
import "FlowEpoch"

/// FlowTransactionScheduler enables smart contracts to schedule autonomous execution in the future.
///
Expand Down Expand Up @@ -1284,6 +1285,14 @@ access(all) contract FlowTransactionScheduler {
return
}

// Skip processing if the epoch is in one of the phase transitions
// or expensive operations
Comment on lines +1288 to +1289
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the desire to avoid adding load to the blocks with expensive epoch operations, but I'm not convinced it is necessary.

The downside of adding load to these epoch operation blocks is that blocks which already have an abnormally long execution time, get longer still. (4 blocks per week on Mainnet which currently take X seconds to compute, now take X+Y seconds to compute.)

  • Are there any other downsides, besides adding Y seconds of execution time to these blocks?
    • If we are just increasing the execution time of 4 blocks per week, it's not clear to me this is necessary. If we are concerned about this exceeding hard limits which would cause halts then this makes more sense to me.
  • Do we know what X and Y are in practice?
    • If we already have a few blocks that take X=5 seconds, Y is small, and so this PR is preventing them from taking like 6 seconds, that is not clearly a good trade-off to me, because we still have the same basic problem that some blocks take abnormally long to execute.
    • On the other hand, if Y is large compared to X, then this will be a problem for every block (not just blocks where epoch operations are occurring).

// We don't skip if it is running in a test environment,
// so we check automaticRewardsEnabled() because it is only true on testnet and mainnet.
if FlowEpoch.isPhaseTransition() && FlowEpoch.automaticRewardsEnabled() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to skip this check if it is on the emulator because epochs don't run on the emulator. Is checking automatic rewards enabled an okay way to do that? I assume that it is only enabled on testnet and mainnet

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emulator has the FlowEpoch contract, but regular epoch functionality isn't enabled, which is why I skip this check for the emulator

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if FlowEpoch.isPhaseTransition() && FlowEpoch.automaticRewardsEnabled() {
if FlowEpoch.automaticRewardsEnabled() && FlowEpoch.isPhaseTransition() {

Maybe front-load the less expensive operation. Not sure about Cadence, but usually if the first condition is false, we will skip evaluating the second.

return
}

self.removeExecutedTransactions(currentTimestamp)

let pendingTransactions = self.pendingQueue()
Expand Down
35 changes: 35 additions & 0 deletions contracts/epochs/FlowEpoch.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,41 @@ access(all) contract FlowEpoch {
?? 0.0
}

/// Checks if the current epoch is in a phase transition
/// so that other system contracts can skip computation-heavy operations
/// during the phase transition.
access(all) fun isPhaseTransition(): Bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes me nervous that this is duplicating the state machine transition logic from advanceBlock. Any future changes would need to be carefully applied to both functions.

I know it's hard to refactor deployed contracts, so maybe there just isn't a good way to do this, but I'm wondering if there is a way to use the same logic here and in advanceBlock. Maybe this function could return an enum which maps to a possible state machine transition in advanceBlock, like Enum{``PayRewards``, ``StakingToSetup``, ``SetupToCommitted``, ``EndEpoch``, ``Noop``}. Then advanceBlock could switch on the returned enum, and callers interested in whether any expensive operation is occurring could check isPhaseOperation != Noop.

switch self.currentEpochPhase {
case EpochPhase.STAKINGAUCTION:
if let previousEpochMetadata = self.getEpochMetadata(FlowEpoch.currentEpochCounter.saturatingSubtract(1)) {
// Paying rewards for the previous epoch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if auto-rewards are disabled? If we are manually paying rewards for some reason, we might pass this check for a portion of the staking auction period (for example, suppose on Mainnet we manually paid rewards Thursday morning).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is also not really a phase transition. The phase transition from Committed to Staking happened in the prior block and is captured by line 1327. This is just "an expensive thing the FlowEpoch contract does", which I think is a more accurate description of what this function is trying to capture.)

if self.currentEpochCounter > 0 && !previousEpochMetadata.rewardsPaid {
return true
}
}
let currentBlock = getCurrentBlock()
let currentEpochMetadata = self.getEpochMetadata(self.currentEpochCounter)!
// Staking auction is ending
if currentBlock.view >= currentEpochMetadata.stakingEndView {
return true
}
case EpochPhase.EPOCHSETUP:
// QC and DKG are completed and will be cleaned up
if FlowClusterQC.votingCompleted() && (FlowDKG.dkgCompleted() != nil) {
return true
}
case EpochPhase.EPOCHCOMMIT:
let currentBlock = getCurrentBlock()
let currentEpochMetadata = FlowEpoch.getEpochMetadata(FlowEpoch.currentEpochCounter)!
// Epoch is ending
if currentBlock.view >= currentEpochMetadata.endView {
return true
}
}

return false
}

init (currentEpochCounter: UInt64,
numViewsInEpoch: UInt64,
numViewsInStakingAuction: UInt64,
Expand Down
2 changes: 1 addition & 1 deletion flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"aliases": {
"emulator": "f8d6e0586b0a20c7",
"mainnet": "8624b52f9ddcd04a",
"testing": "0000000000000007",
"testing": "0000000000000001",
"testnet": "9eca2b38b18b5dfe"
}
},
Expand Down
12 changes: 6 additions & 6 deletions lib/go/contracts/internal/assets/assets.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions lib/go/templates/epoch_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
getCurrentViewFilename = "epoch/scripts/get_current_view.cdc"
getFlowTotalSupplyFilename = "flowToken/scripts/get_supply.cdc"
getFlowBonusTokensFilename = "epoch/scripts/get_bonus_tokens.cdc"
isPhaseTransitionFilename = "epoch/scripts/is_phase_transition.cdc"

// test scripts
getRandomizeFilename = "epoch/scripts/get_randomize.cdc"
Expand Down Expand Up @@ -238,3 +239,9 @@ func GenerateGetBonusTokensScript(env Environment) []byte {

return []byte(ReplaceAddresses(code, env))
}

func GenerateIsPhaseTransitionScript(env Environment) []byte {
code := assets.MustAssetString(isPhaseTransitionFilename)

return []byte(ReplaceAddresses(code, env))
}
23 changes: 23 additions & 0 deletions lib/go/templates/internal/assets/assets.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion lib/go/test/flow_epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func TestEpochDeployment(t *testing.T) {
clusterQCs: nil,
dkgKeys: nil})

// Should not be in a phase transition since the epoch is just starting
result := executeScriptAndCheck(t, b, templates.GenerateIsPhaseTransitionScript(env), nil)
assertEqual(t, cadence.NewBool(false), result)
}

func TestEpochClusters(t *testing.T) {
Expand Down Expand Up @@ -586,6 +589,10 @@ func TestEpochAdvance(t *testing.T) {
// Advance to epoch Setup and make sure that the epoch cannot be ended
advanceView(t, b, env, idTableAddress, IDTableSigner, 1, "EPOCHSETUP", false)

// Should not be in a phase transition since we already advanced to epoch setup
result := executeScriptAndCheck(t, b, templates.GenerateIsPhaseTransitionScript(env), nil)
assertEqual(t, cadence.NewBool(false), result)

verifyConfigMetadata(t, b, env,
ConfigMetadata{
currentEpochCounter: startEpochCounter,
Expand Down Expand Up @@ -638,7 +645,7 @@ func TestEpochAdvance(t *testing.T) {
})

// QC Contract Checks
result := executeScriptAndCheck(t, b, templates.GenerateGetClusterWeightScript(env), [][]byte{jsoncdc.MustEncode(cadence.UInt16(uint16(0)))})
result = executeScriptAndCheck(t, b, templates.GenerateGetClusterWeightScript(env), [][]byte{jsoncdc.MustEncode(cadence.UInt16(uint16(0)))})
assert.Equal(t, cadence.NewUInt64(100), result)

result = executeScriptAndCheck(t, b, templates.GenerateGetNodeWeightScript(env), [][]byte{jsoncdc.MustEncode(cadence.UInt16(uint16(1))), jsoncdc.MustEncode(cadence.String(ids[0]))})
Expand Down Expand Up @@ -864,6 +871,10 @@ func TestEpochQCDKG(t *testing.T) {
false,
)

// Should be in a phase transition since we haven't advanced to epoch setup and view is greater than the staking end view (50)
result := executeScriptAndCheck(t, b, templates.GenerateIsPhaseTransitionScript(env), nil)
assertEqual(t, cadence.NewBool(true), result)

// Advance to epoch Setup and make sure that the epoch cannot be ended
advanceView(t, b, env, idTableAddress, IDTableSigner, 1, "EPOCHSETUP", false)

Expand Down Expand Up @@ -1001,6 +1012,10 @@ func TestEpochQCDKG(t *testing.T) {
// Advance to epoch commit
advanceView(t, b, env, idTableAddress, IDTableSigner, 1, "EPOCHCOMMIT", false)

// Should be in a phase transition since we advanced to epoch commit and view is greater than the end view (70)
result = executeScriptAndCheck(t, b, templates.GenerateIsPhaseTransitionScript(env), nil)
assertEqual(t, cadence.NewBool(true), result)

verifyConfigMetadata(t, b, env,
ConfigMetadata{
currentEpochCounter: startEpochCounter,
Expand Down Expand Up @@ -1087,6 +1102,10 @@ func TestEpochQCDKG(t *testing.T) {
rewards: "6571204.6775",
})

// Should be in a phase transition since we just ended the epoch but haven't paid rewards
result = executeScriptAndCheck(t, b, templates.GenerateIsPhaseTransitionScript(env), nil)
assertEqual(t, cadence.NewBool(true), result)

tx = createTxWithTemplateAndAuthorizer(b, templates.GenerateEpochPayRewardsScript(env), idTableAddress)

signAndSubmit(
Expand Down
21 changes: 21 additions & 0 deletions tests/scheduled_transaction_test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,27 @@ access(all) fun upgradeSchedulerUtilsContract() {
Test.expect(upgradeResult, Test.beSucceeded())
}

access(all) fun upgradeEpochContract() {
var epochCode = Test.readFile("../contracts/epochs/FlowEpoch.cdc")
epochCode = epochCode.replaceAll(of: "\"FungibleToken\"", with: "FungibleToken from 0x0000000000000002")
epochCode = epochCode.replaceAll(of: "\"FlowToken\"", with: "FlowToken from 0x0000000000000003")
epochCode = epochCode.replaceAll(of: "\"FlowFees\"", with: "FlowFees from 0x0000000000000004")
epochCode = epochCode.replaceAll(of: "\"FlowIDTableStaking\"", with: "FlowIDTableStaking from 0x0000000000000001")
epochCode = epochCode.replaceAll(of: "\"FlowClusterQC\"", with: "FlowClusterQC from 0x0000000000000001")
epochCode = epochCode.replaceAll(of: "\"FlowDKG\"", with: "FlowDKG from 0x0000000000000001")

var upgradeTx = Test.Transaction(
code: Test.readFile("./transactions/upgrade_contract.cdc"),
authorizers: [serviceAccount.address],
signers: [serviceAccount],
arguments: ["FlowEpoch", epochCode],
)
var upgradeResult = Test.executeTransaction(
upgradeTx,
)
Test.expect(upgradeResult, Test.beSucceeded())
}

access(all) fun getTimestamp(): UFix64 {
var timestamp = _executeScript(
"./scripts/get_timestamp.cdc",
Expand Down
3 changes: 3 additions & 0 deletions tests/transactionScheduler_events_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ access(all) var accountBalanceBefore: UFix64 = 0.0
access(all)
fun setup() {

// upgrade the FlowEpoch contract to the latest version
upgradeEpochContract()

var err = Test.deployContract(
name: "FlowTransactionScheduler",
path: "../contracts/FlowTransactionScheduler.cdc",
Expand Down
3 changes: 3 additions & 0 deletions tests/transactionScheduler_manager_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ access(all) var timeInFuture: UFix64 = 0.0
access(all)
fun setup() {

// upgrade the FlowEpoch contract to the latest version
upgradeEpochContract()

var err = Test.deployContract(
name: "FlowTransactionScheduler",
path: "../contracts/FlowTransactionScheduler.cdc",
Expand Down
3 changes: 3 additions & 0 deletions tests/transactionScheduler_misc_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ access(all) var accountBalanceBefore: UFix64 = 0.0
access(all)
fun setup() {

// upgrade the FlowEpoch contract to the latest version
upgradeEpochContract()

var err = Test.deployContract(
name: "FlowTransactionScheduler",
path: "../contracts/FlowTransactionScheduler.cdc",
Expand Down
3 changes: 3 additions & 0 deletions tests/transactionScheduler_schedule_test_tmp_no_ci.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import "scheduled_transaction_test_helpers.cdc"
access(all)
fun setup() {

// upgrade the FlowEpoch contract to the latest version
upgradeEpochContract()

var err = Test.deployContract(
name: "FlowTransactionScheduler",
path: "../contracts/FlowTransactionScheduler.cdc",
Expand Down
3 changes: 3 additions & 0 deletions tests/transactionScheduler_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import "scheduled_transaction_test_helpers.cdc"
access(all)
fun setup() {

// upgrade the FlowEpoch contract to the latest version
upgradeEpochContract()

var err = Test.deployContract(
name: "FlowTransactionScheduler",
path: "../contracts/FlowTransactionScheduler.cdc",
Expand Down
5 changes: 5 additions & 0 deletions transactions/epoch/scripts/is_phase_transition.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "FlowEpoch"

access(all) fun main(): Bool {
return FlowEpoch.isPhaseTransition()
}
Loading