Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Concordium Protocol Version 7 #214

Merged
merged 10 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x

- name: Restore dependencies
run: dotnet restore ./backend/CcScan.Backend.sln

- name: Build
run: dotnet build ./backend/CcScan.Backend.sln -c Release --no-restore

- name: Test
run: dotnet test ./backend/CcScan.Backend.sln --filter Category!=IntegrationTests -c Release --no-build --verbosity normal
run: |
# Tests depend on docker-compose being available due to this issue https://github.com/mariotoffia/FluentDocker/issues/312.
# The soft linking should be remove when a fix is released.
ln -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose
dotnet test ./backend/CcScan.Backend.sln --filter Category!=IntegrationTests -c Release --no-build --verbosity normal
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "backend/concordium-net-sdk"]
path = backend/concordium-net-sdk
url = ../concordium-net-sdk.git
38 changes: 33 additions & 5 deletions backend/Application/Api/GraphQL/Import/AccountWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Dapper;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Concordium.Sdk.Types;

namespace Application.Api.GraphQL.Import;

Expand Down Expand Up @@ -157,20 +158,23 @@ private static IEnumerable<T> IterateBatchDbDataReader<T>(DbDataReader reader, F
public async Task UpdateAccount<TSource>(TSource item, Func<TSource, ulong> delegatorIdSelector, Action<TSource, Account> updateAction)
{
using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(UpdateAccount));

await using var context = await _dbContextFactory.CreateDbContextAsync();
var delegatorId = (long)delegatorIdSelector(item);

var account = await context.Accounts.SingleAsync(x => x.Id == delegatorId);
updateAction(item, account);

await context.SaveChangesAsync();
}


/// <summary>
/// Update using <paramref name="updateAction"/> on each account with a pending change for delegation that is effective before <paramref name="effectiveTimeEqualOrBefore"/>.
/// </summary>
public async Task UpdateAccountsWithPendingDelegationChange(DateTimeOffset effectiveTimeEqualOrBefore, Action<Account> updateAction)
{
using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(UpdateAccountsWithPendingDelegationChange));

await using var context = await _dbContextFactory.CreateDbContextAsync();

var sql = $"select * from graphql_accounts where delegation_pending_change->'data'->>'EffectiveTime' <= '{effectiveTimeEqualOrBefore:O}'";
Expand Down Expand Up @@ -202,7 +206,31 @@ public async Task UpdateAccounts(Expression<Func<Account, bool>> whereClause, Ac
await context.SaveChangesAsync();
}
}


/// <summary>
/// Remove baker and move its delegators to the passive pool.
/// Returns the number of delegators which were moved.
/// </summary>
public async Task<int> RemoveBaker(BakerId bakerId, DateTimeOffset effectiveTime) {
using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(RemoveBaker));
await using var context = await _dbContextFactory.CreateDbContextAsync();

var baker = await context.Bakers.SingleAsync(x => x.Id == (long)bakerId.Id.Index);
baker.State = new Bakers.RemovedBakerState(effectiveTime);

var target = new BakerDelegationTarget((long) bakerId.Id.Index);
var delegatorAccounts = await context.Accounts
.Where(account => account.Delegation != null && account.Delegation.DelegationTarget == target)
.ToArrayAsync();
foreach (var delegatorAccount in delegatorAccounts) {
var delegation = delegatorAccount.Delegation ?? throw new InvalidOperationException("Account delegating to baker target being removed has no delegation attached.");
delegation.DelegationTarget = new PassiveDelegationTarget();
}

await context.SaveChangesAsync();
return delegatorAccounts.Length;
}

public async Task UpdateDelegationStakeIfRestakingEarnings(AccountRewardSummary[] stakeUpdates)
{
using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(UpdateDelegationStakeIfRestakingEarnings));
Expand Down
63 changes: 52 additions & 11 deletions backend/Application/Api/GraphQL/Import/BakerChangeStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ internal interface IBakerChangeStrategy
Task UpdateBakersFromTransactionEvents(
IEnumerable<AccountTransactionDetails> transactionEvents,
ImportState importState,
BakerImportHandler.BakerUpdateResultsBuilder resultBuilder);

BakerImportHandler.BakerUpdateResultsBuilder resultBuilder,
DateTimeOffset blockSlotTime);

bool MustApplyPendingChangesDue(DateTimeOffset? nextPendingBakerChangeTime);
DateTimeOffset GetEffectiveTime();
/// <summary>Whether the protocol supports pending changes. Starting from Protocol version 7 this is not the case.</summary>
bool SupportsPendingChanges();
}

internal static class BakerChangeStrategyFactory
{
internal static IBakerChangeStrategy Create(
BlockInfo blockInfo,
ChainParameters chainParameters,
ChainParameters chainParameters,
BlockImportPaydayStatus importPaydayStatus,
BakerWriter writer,
AccountInfo[] bakersWithNewPendingChanges)
{
if (blockInfo.ProtocolVersion.AsInt() < 4)
return new PreProtocol4Strategy(bakersWithNewPendingChanges, blockInfo, writer);

ChainParameters.TryGetPoolOwnerCooldown(chainParameters, out var poolOwnerCooldown);
return new PostProtocol4Strategy(blockInfo, poolOwnerCooldown!.Value, importPaydayStatus, writer);

Expand All @@ -46,13 +49,17 @@ public PreProtocol4Strategy(AccountInfo[] accountInfos, BlockInfo blockInfo, Bak
_accountInfos = accountInfos;
}

public bool SupportsPendingChanges() => true;

/// <summary>
/// Prior to protocol 4 <see cref="Concordium.Sdk.Types.BakerConfigured"/> isn't used.
/// </summary>
public async Task UpdateBakersFromTransactionEvents(
IEnumerable<AccountTransactionDetails> transactionEvents,
ImportState importState,
BakerImportHandler.BakerUpdateResultsBuilder resultBuilder)
BakerImportHandler.BakerUpdateResultsBuilder resultBuilder,
DateTimeOffset blockSlotTime
)
{
foreach (var txEvent in transactionEvents)
{
Expand Down Expand Up @@ -184,6 +191,8 @@ public bool MustApplyPendingChangesDue(DateTimeOffset? nextPendingBakerChangeTim
return false;
}

public bool SupportsPendingChanges() => _blockInfo.ProtocolVersion < ProtocolVersion.P7;

public DateTimeOffset GetEffectiveTime()
{
if (_importPaydayStatus is FirstBlockAfterPayday firstBlockAfterPayday)
Expand All @@ -195,8 +204,9 @@ public DateTimeOffset GetEffectiveTime()
/// <see cref="Concordium.Sdk.Types.BakerConfigured"/> are used from protocol 4.
/// </summary>
public async Task UpdateBakersFromTransactionEvents(IEnumerable<AccountTransactionDetails> transactionEvents, ImportState importState,
BakerImportHandler.BakerUpdateResultsBuilder resultBuilder)
BakerImportHandler.BakerUpdateResultsBuilder resultBuilder, DateTimeOffset blockSlotTime)
{
bool supportsPendingChanges = SupportsPendingChanges();
foreach (var txEvent in transactionEvents)
{
switch (txEvent.Effects)
Expand All @@ -219,8 +229,21 @@ await _writer.AddOrUpdateBaker(bakerAdded,
resultBuilder.IncrementBakersAdded();
break;
case BakerRemovedEvent bakerRemovedEvent:
var pendingChange = await SetPendingChangeOnBaker(bakerRemovedEvent.BakerId, bakerRemovedEvent);
importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChange.EffectiveTime);
if (supportsPendingChanges) {
// If the protocol version (prior to 7) supports pending changes, then store a pending change.
var pendingChange = await SetPendingChangeOnBaker(bakerRemovedEvent.BakerId, bakerRemovedEvent);
importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChange.EffectiveTime);
} else {
// Otherwise update the stake immediately.
await _writer.UpdateBaker(bakerRemovedEvent,
src => src.BakerId.Id.Index,
(src, dst) =>
{
var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot remove a baker that is not active!");
dst.State = new RemovedBakerState(blockSlotTime);
resultBuilder.AddBakerRemoved((long) bakerRemovedEvent.BakerId.Id.Index);
});
}
break;
case BakerRestakeEarningsUpdatedEvent bakerRestakeEarningsUpdatedEvent:
await _writer.UpdateBaker(bakerRestakeEarningsUpdatedEvent,
Expand Down Expand Up @@ -280,18 +303,36 @@ await _writer.UpdateBaker(bakerSetOpenStatusEvent,
resultBuilder.AddBakerClosedForAll((long)bakerSetOpenStatusEvent.BakerId.Id.Index);
break;
case BakerStakeDecreasedEvent bakerStakeDecreasedEvent:
var pendingChangeStakeDecreased = await SetPendingChangeOnBaker(bakerStakeDecreasedEvent.BakerId, bakerStakeDecreasedEvent);
importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChangeStakeDecreased.EffectiveTime);
if (supportsPendingChanges) {
// If the protocol version (prior to 7) supports pending changes, then store a pending change.
var pendingChangeStakeDecreased = await SetPendingChangeOnBaker(bakerStakeDecreasedEvent.BakerId, bakerStakeDecreasedEvent);
importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChangeStakeDecreased.EffectiveTime);
} else {
// From protocol version 7 and onwards stake changes are immediate.
await _writer.UpdateBaker(bakerStakeDecreasedEvent,
src => src.BakerId.Id.Index,
(src, dst) =>
{
var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot decrease stake for a baker that is not active!");
activeState.StakedAmount = src.NewStake.Value;
});
}
break;
case BakerStakeIncreasedEvent bakerStakeIncreasedEvent:
await _writer.UpdateBaker(bakerStakeIncreasedEvent,
src => src.BakerId.Id.Index,
(src, dst) =>
{
var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot set restake earnings for a baker that is not active!");
var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot increase stake for a baker that is not active!");
activeState.StakedAmount = src.NewStake.Value;
});
break;
case BakerEventDelegationRemoved delegationRemoved:
// This event was introduced as part of Concordium Protocol Version 7,
// which also removes the logic around pending changes, meaning we can
// just update the state immediately.
await _writer.RemoveDelegator(delegationRemoved.DelegatorId);
break;
case BakerKeysUpdatedEvent:
default:
break;
Expand Down
39 changes: 24 additions & 15 deletions backend/Application/Api/GraphQL/Import/BakerImportHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ public BakerImportHandler(IDbContextFactory<GraphQlDbContext> dbContextFactory,
_logger = Log.ForContext(GetType());
}

/// <summary>
/// Process block data related to changes for bakers/validators.
/// </summary>
public async Task<BakerUpdateResults> HandleBakerUpdates(BlockDataPayload payload, RewardsSummary rewardsSummary,
ChainParametersState chainParameters, BlockImportPaydayStatus importPaydayStatus, ImportState importState)
{
using var counter = _metrics.MeasureDuration(nameof(BakerImportHandler), nameof(HandleBakerUpdates));

var changeStrategy = BakerChangeStrategyFactory.Create(payload.BlockInfo, chainParameters.Current, importPaydayStatus, _writer,
payload.AccountInfos.BakersWithNewPendingChanges);;

Expand Down Expand Up @@ -164,8 +167,8 @@ or BakerRestakeEarningsUpdated
or BakerConfigured
or BakerKeysUpdated
);
await bakerChangeStrategy.UpdateBakersFromTransactionEvents(txEvents, importState, resultBuilder);

await bakerChangeStrategy.UpdateBakersFromTransactionEvents(txEvents, importState, resultBuilder, payload.BlockInfo.BlockSlotTime);

// This should happen after the bakers from current block has been added to the database
if (isFirstBlockAfterPayday)
Expand Down Expand Up @@ -224,10 +227,10 @@ await _writer.UpdateBakers(
FinalizationCommission = source.PoolInfo.CommissionRates.FinalizationCommission.AsDecimal(),
BakingCommission = source.PoolInfo.CommissionRates.BakingCommission.AsDecimal()
},
DelegatedStake = source.DelegatedCapital.Value,
DelegatedStake = source.DelegatedCapital!.Value.Value,
DelegatorCount = 0,
DelegatedStakeCap = source.DelegatedCapitalCap.Value,
TotalStake = source.BakerEquityCapital.Value + source.DelegatedCapital.Value
DelegatedStakeCap = source.DelegatedCapitalCap!.Value.Value,
TotalStake = source.BakerEquityCapital!.Value.Value + source.DelegatedCapital.Value.Value
limemloh marked this conversation as resolved.
Show resolved Hide resolved
};
pool.ApplyPaydayStatus(source.CurrentPaydayStatus, source.PoolInfo.CommissionRates);

Expand Down Expand Up @@ -262,8 +265,8 @@ await _writer.UpdateBakers(baker =>
{
var rates = baker.ActiveState!.Pool!.CommissionRates;
rates.FinalizationCommission = AdjustValueToRange(rates.FinalizationCommission, currentFinalizationCommissionRange);
rates.BakingCommission = AdjustValueToRange(rates.BakingCommission, currentBakingCommissionRange);
rates.TransactionCommission = AdjustValueToRange(rates.TransactionCommission, currentTransactionCommissionRange);
rates.BakingCommission = AdjustValueToRange(rates.BakingCommission, currentBakingCommissionRange!);
rates.TransactionCommission = AdjustValueToRange(rates.TransactionCommission, currentTransactionCommissionRange!);
},
baker => baker.ActiveState!.Pool != null);

Expand Down Expand Up @@ -309,16 +312,22 @@ await _writer.UpdateBaker(1900UL, bakerId => bakerId, (bakerId, baker) =>
}
}

private async Task UpdateBakersWithPendingChangesDue(IBakerChangeStrategy bakerChangeStrategy,
private async Task UpdateBakersWithPendingChangesDue(IBakerChangeStrategy bakerChangeStrategy,
ImportState importState, BakerUpdateResultsBuilder resultBuilder)
{
if (bakerChangeStrategy.MustApplyPendingChangesDue(importState.NextPendingBakerChangeTime))
{
var effectiveTime = bakerChangeStrategy.GetEffectiveTime();
await _writer.UpdateBakersWithPendingChange(effectiveTime, baker => ApplyPendingChange(baker, resultBuilder));
// Check if this protocol supports pending changes.
if (bakerChangeStrategy.SupportsPendingChanges()) {
if (bakerChangeStrategy.MustApplyPendingChangesDue(importState.NextPendingBakerChangeTime))
{
var effectiveTime = bakerChangeStrategy.GetEffectiveTime();
await _writer.UpdateBakersWithPendingChange(effectiveTime, baker => ApplyPendingChange(baker, resultBuilder));

importState.NextPendingBakerChangeTime = await _writer.GetMinPendingChangeTime();
_logger.Information("NextPendingBakerChangeTime set to {value}", importState.NextPendingBakerChangeTime);
importState.NextPendingBakerChangeTime = await _writer.GetMinPendingChangeTime();
_logger.Information("NextPendingBakerChangeTime set to {value}", importState.NextPendingBakerChangeTime);
}
} else {
// Starting from protocol version 7 and onwards stake changes are immediate, so we apply all of them in the first block of P7 and this is a no-op for future blocks.
await _writer.UpdateBakersWithPendingChange(DateTimeOffset.MaxValue, baker => ApplyPendingChange(baker, resultBuilder));
}
}

Expand Down
27 changes: 27 additions & 0 deletions backend/Application/Api/GraphQL/Import/BakerWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,33 @@ public async Task AddBakerTransactionRelations(IEnumerable<BakerTransactionRelat
await context.SaveChangesAsync();
}

/// <summary>
/// Removes delegator information tracked for an account.
/// Throws for accounts with no delegation information.
/// </summary>
public async Task RemoveDelegator(DelegatorId delegatorId) {
using var counter = _metrics.MeasureDuration(nameof(BakerWriter), nameof(RemoveDelegator));
await using var context = await _dbContextFactory.CreateDbContextAsync();
var account = await context.Accounts.SingleAsync(x => x.Id == (long) delegatorId.Id.Index);
if (account.Delegation == null) throw new InvalidOperationException("Trying to remove delegator, but account is not delegating.");
// Update the delegation counter on the target.
switch (account.Delegation.DelegationTarget) {
case PassiveDelegationTarget passiveTarget:
var passive = await context.PassiveDelegations.SingleAsync();
passive.DelegatorCount -= 1;
break;
case BakerDelegationTarget target:
var baker = await context.Bakers.SingleAsync(baker => baker.BakerId == target.BakerId);
var activeState = baker.State as ActiveBakerState ?? throw new InvalidOperationException("Trying to remove delegator targeting a baker pool, but the baker state is not active.");
var pool = activeState.Pool ?? throw new InvalidOperationException("Trying to remove delegator targeting a baker pool, but the baker state had no pool information.");
pool.DelegatorCount -= 1;
break;
};
// Delete the delegation information
account.Delegation = null;
await context.SaveChangesAsync();
}

public async Task UpdateDelegatedStake()
{
using var counter = _metrics.MeasureDuration(nameof(BakerWriter), nameof(UpdateDelegatedStake));
Expand Down
Loading
Loading