diff --git a/Tzkt.Data/Models/Accounts/Account.cs b/Tzkt.Data/Models/Accounts/Account.cs index 108d0b0a4..3516ac623 100644 --- a/Tzkt.Data/Models/Accounts/Account.cs +++ b/Tzkt.Data/Models/Accounts/Account.cs @@ -38,6 +38,7 @@ public class Account public int TransferTicketCount { get; set; } public int IncreasePaidStorageCount { get; set; } + public int UpdateConsensusKeyCount { get; set; } public int MigrationsCount { get; set; } diff --git a/Tzkt.Data/Models/AppState.cs b/Tzkt.Data/Models/AppState.cs index 9a82af91a..864c31b4b 100644 --- a/Tzkt.Data/Models/AppState.cs +++ b/Tzkt.Data/Models/AppState.cs @@ -72,6 +72,7 @@ public class AppState public int TransferTicketOpsCount { get; set; } public int IncreasePaidStorageOpsCount { get; set; } + public int UpdateConsensusKeyOpsCount { get; set; } public int MigrationOpsCount { get; set; } public int RevelationPenaltyOpsCount { get; set; } diff --git a/Tzkt.Data/Models/Blocks/Block.cs b/Tzkt.Data/Models/Blocks/Block.cs index 97ed69a4d..d02674804 100644 --- a/Tzkt.Data/Models/Blocks/Block.cs +++ b/Tzkt.Data/Models/Blocks/Block.cs @@ -84,6 +84,8 @@ public class Block public List IncreasePaidStorageOps { get; set; } public List VdfRevelationOps { get; set; } + public List UpdateConsensusKeyOps { get; set; } + public List Migrations { get; set; } public List RevelationPenalties { get; set; } #endregion diff --git a/Tzkt.Data/Models/Operations/Base/Operations.cs b/Tzkt.Data/Models/Operations/Base/Operations.cs index ddbfa67be..941365f41 100644 --- a/Tzkt.Data/Models/Operations/Base/Operations.cs +++ b/Tzkt.Data/Models/Operations/Base/Operations.cs @@ -45,5 +45,7 @@ public enum Operations IncreasePaidStorage = 0b_0001_0000_0000_0000_0000_0000_0000_0000, VdfRevelation = 0b_0010_0000_0000_0000_0000_0000_0000_0000, + + UpdateConsensusKey = 0b_0100_0000_0000_0000_0000_0000_0000_0000, } } diff --git a/Tzkt.Data/Models/Operations/UpdateConsensusKeyOperation.cs b/Tzkt.Data/Models/Operations/UpdateConsensusKeyOperation.cs new file mode 100644 index 000000000..1f025f61b --- /dev/null +++ b/Tzkt.Data/Models/Operations/UpdateConsensusKeyOperation.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using Tzkt.Data.Models.Base; + +namespace Tzkt.Data.Models +{ + public class UpdateConsensusKeyOperation : ManagerOperation + { + public int ActivationCycle { get; set; } + public string PublicKey { get; set; } + public string PublicKeyHash { get; set; } + } + + public static class UpdateConsensusKeyOperationModel + { + public static void BuildUpdateConsensusKeyOperationModel(this ModelBuilder modelBuilder) + { + #region keys + modelBuilder.Entity() + .HasKey(x => x.Id); + #endregion + + #region props + modelBuilder.Entity() + .Property(x => x.OpHash) + .IsFixedLength(true) + .HasMaxLength(51) + .IsRequired(); + #endregion + + #region indexes + modelBuilder.Entity() + .HasIndex(x => x.Level); + + modelBuilder.Entity() + .HasIndex(x => x.OpHash); + + modelBuilder.Entity() + .HasIndex(x => x.SenderId); + #endregion + + #region relations + modelBuilder.Entity() + .HasOne(x => x.Block) + .WithMany(x => x.UpdateConsensusKeyOps) + .HasForeignKey(x => x.Level) + .HasPrincipalKey(x => x.Level); + #endregion + } + } +} diff --git a/Tzkt.Data/TzktContext.cs b/Tzkt.Data/TzktContext.cs index 816b3e676..00171ab7b 100644 --- a/Tzkt.Data/TzktContext.cs +++ b/Tzkt.Data/TzktContext.cs @@ -54,6 +54,7 @@ public class TzktContext : DbContext public DbSet TransferTicketOps { get; set; } public DbSet IncreasePaidStorageOps { get; set; } + public DbSet UpdateConsensusKeyOps { get; set; } public DbSet EndorsingRewardOps { get; set; } public DbSet MigrationOps { get; set; } @@ -154,6 +155,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.BuildTransferTicketOperationModel(); modelBuilder.BuildIncreasePaidStorageOperationModel(); + modelBuilder.BuildUpdateConsensusKeyOperationModel(); modelBuilder.BuildEndorsingRewardOperationModel(); modelBuilder.BuildMigrationOperationModel(); diff --git a/Tzkt.Sync/Protocols/Handlers/Proto1/Commits/Operations/DelegationsCommit.cs b/Tzkt.Sync/Protocols/Handlers/Proto1/Commits/Operations/DelegationsCommit.cs index a9b293a0c..e4a2aa3e4 100644 --- a/Tzkt.Sync/Protocols/Handlers/Proto1/Commits/Operations/DelegationsCommit.cs +++ b/Tzkt.Sync/Protocols/Handlers/Proto1/Commits/Operations/DelegationsCommit.cs @@ -472,6 +472,7 @@ void UpgradeUser(DelegationOperation delegation) TxRollupReturnBondCount = user.TxRollupReturnBondCount, TxRollupSubmitBatchCount = user.TxRollupSubmitBatchCount, IncreasePaidStorageCount = user.IncreasePaidStorageCount, + UpdateConsensusKeyCount = user.UpdateConsensusKeyCount, RollupBonds = user.RollupBonds, RollupsCount = user.RollupsCount }; @@ -622,6 +623,13 @@ void UpgradeUser(DelegationOperation delegation) touched.Add((op, entry.State)); } break; + case UpdateConsensusKeyOperation op: + if (op.Sender?.Id == user.Id) + { + op.Sender = delegat; + touched.Add((op, entry.State)); + } + break; case Contract contract: if (contract.WeirdDelegate?.Id == user.Id || contract.Creator?.Id == user.Id || contract.Manager?.Id == user.Id) { @@ -695,6 +703,7 @@ void DowngradeDelegate(DelegationOperation delegation) TxRollupReturnBondCount = delegat.TxRollupReturnBondCount, TxRollupSubmitBatchCount = delegat.TxRollupSubmitBatchCount, IncreasePaidStorageCount = delegat.IncreasePaidStorageCount, + UpdateConsensusKeyCount = delegat.UpdateConsensusKeyCount, RollupBonds = delegat.RollupBonds, RollupsCount = delegat.RollupsCount }; @@ -854,6 +863,13 @@ void DowngradeDelegate(DelegationOperation delegation) touched.Add((op, entry.State)); } break; + case UpdateConsensusKeyOperation op: + if (op.Sender?.Id == delegat.Id) + { + op.Sender = user; + touched.Add((op, entry.State)); + } + break; case Contract contract: if (contract.WeirdDelegate?.Id == delegat.Id || contract.Creator?.Id == delegat.Id || contract.Manager?.Id == delegat.Id) { diff --git a/Tzkt.Sync/Protocols/Handlers/Proto12/Commits/StatisticsCommit.cs b/Tzkt.Sync/Protocols/Handlers/Proto12/Commits/StatisticsCommit.cs index 5a24233a2..70a00b240 100644 --- a/Tzkt.Sync/Protocols/Handlers/Proto12/Commits/StatisticsCommit.cs +++ b/Tzkt.Sync/Protocols/Handlers/Proto12/Commits/StatisticsCommit.cs @@ -179,6 +179,13 @@ public virtual async Task Apply(Block block, List endo statistics.TotalBurned += ops.Sum(x => x.StorageFee ?? 0); } + if (block.UpdateConsensusKeyOps != null) + { + var ops = block.UpdateConsensusKeyOps.Where(x => x.Status == OperationStatus.Applied); + if (ops.Any()) + statistics.TotalBurned += ops.Sum(x => x.StorageFee ?? 0); + } + if (block.Events.HasFlag(BlockEvents.CycleEnd)) statistics.Cycle = block.Cycle; diff --git a/Tzkt.Sync/Protocols/Handlers/Proto15/Commits/Operations/UpdateConsensusKeyCommit.cs b/Tzkt.Sync/Protocols/Handlers/Proto15/Commits/Operations/UpdateConsensusKeyCommit.cs new file mode 100644 index 000000000..d6c4959e6 --- /dev/null +++ b/Tzkt.Sync/Protocols/Handlers/Proto15/Commits/Operations/UpdateConsensusKeyCommit.cs @@ -0,0 +1,132 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Netezos.Keys; +using Tzkt.Data.Models; +using Tzkt.Data.Models.Base; + +namespace Tzkt.Sync.Protocols.Proto15 +{ + class UpdateConsensusKeyCommit : ProtocolCommit + { + public UpdateConsensusKeyCommit(ProtocolHandler protocol) : base(protocol) { } + + public virtual async Task Apply(Block block, JsonElement op, JsonElement content) + { + #region init + var sender = await Cache.Accounts.GetAsync(content.RequiredString("source")); + sender.Delegate ??= Cache.Accounts.GetDelegate(sender.DelegateId); + + var pubKey = content.RequiredString("pk"); + var pubKeyHash = PubKey.FromBase58(pubKey).Address; + var result = content.Required("metadata").Required("operation_result"); + var operation = new UpdateConsensusKeyOperation + { + Id = Cache.AppState.NextOperationId(), + OpHash = op.RequiredString("hash"), + Block = block, + Level = block.Level, + Timestamp = block.Timestamp, + BakerFee = content.RequiredInt64("fee"), + Counter = content.RequiredInt32("counter"), + GasLimit = content.RequiredInt32("gas_limit"), + StorageLimit = content.RequiredInt32("storage_limit"), + Sender = sender, + ActivationCycle = block.Cycle + block.Protocol.PreservedCycles + 1, + PublicKey = pubKey, + PublicKeyHash = pubKeyHash, + Status = result.RequiredString("status") switch + { + "applied" => OperationStatus.Applied, + "backtracked" => OperationStatus.Backtracked, + "failed" => OperationStatus.Failed, + "skipped" => OperationStatus.Skipped, + _ => throw new NotImplementedException() + }, + Errors = result.TryGetProperty("errors", out var errors) + ? OperationErrors.Parse(content, errors) + : null, + GasUsed = (int)(((result.OptionalInt64("consumed_milligas") ?? 0) + 999) / 1000) + }; + #endregion + + #region entities + var blockBaker = block.Proposer; + var senderDelegate = sender.Delegate ?? sender as Data.Models.Delegate; + + Db.TryAttach(blockBaker); + Db.TryAttach(sender); + Db.TryAttach(senderDelegate); + #endregion + + #region apply operation + sender.Balance -= operation.BakerFee; + if (senderDelegate != null) + { + senderDelegate.StakingBalance -= operation.BakerFee; + if (senderDelegate.Id != sender.Id) + senderDelegate.DelegatedBalance -= operation.BakerFee; + } + blockBaker.Balance += operation.BakerFee; + blockBaker.StakingBalance += operation.BakerFee; + + sender.UpdateConsensusKeyCount++; + + block.Operations |= Operations.UpdateConsensusKey; + block.Fees += operation.BakerFee; + + sender.Counter = operation.Counter; + #endregion + + #region apply result + #endregion + + Proto.Manager.Set(operation.Sender); + Db.UpdateConsensusKeyOps.Add(operation); + } + + public virtual async Task Revert(Block block, UpdateConsensusKeyOperation operation) + { + #region init + operation.Block ??= block; + operation.Block.Proposer ??= Cache.Accounts.GetDelegate(block.ProposerId); + + operation.Sender ??= await Cache.Accounts.GetAsync(operation.SenderId); + operation.Sender.Delegate ??= Cache.Accounts.GetDelegate(operation.Sender.DelegateId); + #endregion + + #region entities + var blockBaker = block.Proposer; + var sender = operation.Sender; + var senderDelegate = sender.Delegate ?? sender as Data.Models.Delegate; + + Db.TryAttach(blockBaker); + Db.TryAttach(sender); + Db.TryAttach(senderDelegate); + #endregion + + #region revert result + #endregion + + #region revert operation + sender.Balance += operation.BakerFee; + if (senderDelegate != null) + { + senderDelegate.StakingBalance += operation.BakerFee; + if (senderDelegate.Id != sender.Id) + senderDelegate.DelegatedBalance += operation.BakerFee; + } + blockBaker.Balance -= operation.BakerFee; + blockBaker.StakingBalance -= operation.BakerFee; + + sender.UpdateConsensusKeyCount--; + + sender.Counter = operation.Counter - 1; + #endregion + + Db.UpdateConsensusKeyOps.Remove(operation); + Cache.AppState.ReleaseManagerCounter(); + Cache.AppState.ReleaseOperationId(); + } + } +} diff --git a/Tzkt.Sync/Protocols/Handlers/Proto15/Proto15Handler.cs b/Tzkt.Sync/Protocols/Handlers/Proto15/Proto15Handler.cs index afdc70daa..468408a9c 100644 --- a/Tzkt.Sync/Protocols/Handlers/Proto15/Proto15Handler.cs +++ b/Tzkt.Sync/Protocols/Handlers/Proto15/Proto15Handler.cs @@ -144,6 +144,9 @@ public override async Task Commit(JsonElement block) case "increase_paid_storage": await new IncreasePaidStorageCommit(this).Apply(blockCommit.Block, operation, content); break; + case "update_consensus_key": + await new UpdateConsensusKeyCommit(this).Apply(blockCommit.Block, operation, content); + break; case "reveal": await new RevealsCommit(this).Apply(blockCommit.Block, operation, content); break; @@ -323,6 +326,9 @@ public override async Task Revert() if (currBlock.Operations.HasFlag(Operations.IncreasePaidStorage)) operations.AddRange(await Db.IncreasePaidStorageOps.Where(x => x.Level == currBlock.Level).ToListAsync()); + if (currBlock.Operations.HasFlag(Operations.UpdateConsensusKey)) + operations.AddRange(await Db.UpdateConsensusKeyOps.Where(x => x.Level == currBlock.Level).ToListAsync()); + if (currBlock.Operations.HasFlag(Operations.Revelations)) operations.AddRange(await Db.NonceRevelationOps.Where(x => x.Level == currBlock.Level).ToListAsync()); @@ -441,6 +447,9 @@ public override async Task Revert() case IncreasePaidStorageOperation op: await new IncreasePaidStorageCommit(this).Revert(currBlock, op); break; + case UpdateConsensusKeyOperation op: + await new UpdateConsensusKeyCommit(this).Revert(currBlock, op); + break; case RegisterConstantOperation registerConstant: await new RegisterConstantsCommit(this).Revert(currBlock, registerConstant); break; diff --git a/Tzkt.Sync/Protocols/Handlers/Proto15/Validation/Validator.cs b/Tzkt.Sync/Protocols/Handlers/Proto15/Validation/Validator.cs index c93eb7917..dccfeed9d 100644 --- a/Tzkt.Sync/Protocols/Handlers/Proto15/Validation/Validator.cs +++ b/Tzkt.Sync/Protocols/Handlers/Proto15/Validation/Validator.cs @@ -176,6 +176,7 @@ protected virtual async Task ValidateOperations(JsonElement operations) case "register_global_constant": await ValidateRegisterConstant(content); break; case "set_deposits_limit": await ValidateSetDepositsLimit(content); break; case "increase_paid_storage": await ValidateIncreasePaidStorage(content); break; + case "update_consensus_key": await ValidateUpdateConsensusKey(content); break; case "tx_rollup_origination": await ValidateTxRollupOrigination(content); break; case "tx_rollup_submit_batch": await ValidateTxRollupSubmitBatch(content); break; case "tx_rollup_commit": await ValidateTxRollupCommit(content); break; @@ -483,7 +484,7 @@ protected virtual async Task ValidateSetDepositsLimit(JsonElement content) source, content.RequiredInt64("fee")); } - + protected virtual async Task ValidateIncreasePaidStorage(JsonElement content) { var source = content.RequiredString("source"); @@ -497,6 +498,19 @@ protected virtual async Task ValidateIncreasePaidStorage(JsonElement content) content.RequiredInt64("fee")); } + protected virtual async Task ValidateUpdateConsensusKey(JsonElement content) + { + var source = content.RequiredString("source"); + + if (!await Cache.Accounts.ExistsAsync(source)) + throw new ValidationException("unknown source account"); + + ValidateFeeBalanceUpdates( + content.Required("metadata").OptionalArray("balance_updates")?.EnumerateArray() ?? Enumerable.Empty(), + source, + content.RequiredInt64("fee")); + } + protected virtual async Task ValidateTxRollupOrigination(JsonElement content) { var source = content.RequiredString("source"); diff --git a/Tzkt.Sync/Protocols/ProtocolHandler.cs b/Tzkt.Sync/Protocols/ProtocolHandler.cs index a1f173ba3..fd20a2cec 100644 --- a/Tzkt.Sync/Protocols/ProtocolHandler.cs +++ b/Tzkt.Sync/Protocols/ProtocolHandler.cs @@ -333,6 +333,7 @@ void ClearCachedRelations() b.TransferTicketOps = null; b.IncreasePaidStorageOps = null; b.VdfRevelationOps = null; + b.UpdateConsensusKeyOps = null; break; } }