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

feat: update epoch param data when param proposal is signed by a required number of delegation keys #88

Merged
merged 1 commit into from
Nov 23, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.springframework.transaction.annotation.Transactional;

import java.util.Set;

public interface GenesisDataService {

@Transactional
Expand All @@ -10,4 +12,8 @@ public interface GenesisDataService {
Long getByronKnownTime();

Integer getShelleyEpochLength();

Integer getUpdateQuorum();

Set<String> getDelegationKeyHashes();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.cardanofoundation.ledgersync.explorerconsumer.service.CostModelService;
import org.cardanofoundation.ledgersync.explorerconsumer.service.EpochParamService;
import org.cardanofoundation.ledgersync.explorerconsumer.service.GenesisDataService;
import org.cardanofoundation.ledgersync.explorerconsumer.util.EpochParamUtil;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

Expand Down Expand Up @@ -127,8 +128,12 @@ void handleEpochParam(int epochNo) {

List<ParamProposal> prevParamProposals = paramProposalRepository
.findParamProposalsByEpochNo(epochNo - 1);
prevParamProposals.forEach(
paramProposal -> epochParamMapper.updateByParamProposal(curEpochParam, paramProposal));

var paramProposalToUpdate = EpochParamUtil.getParamProposalToUpdate(prevParamProposals, genesisDataService.getDelegationKeyHashes(),
genesisDataService.getUpdateQuorum());
if (paramProposalToUpdate != null) {
epochParamMapper.updateByParamProposal(curEpochParam, paramProposalToUpdate);
}

Block block = blockRepository.findFirstByEpochNo(epochNo)
.orElseThrow(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public List<ParamProposal> handleParamProposals(
private List<ParamProposal> handleParamProposal(AggregatedTx aggregatedTx, Tx tx) {
int epochNo = (int) aggregatedTx.getUpdate().getEpoch();

Set<ParamProposal> paramProposals = aggregatedTx
List<ParamProposal> paramProposals = aggregatedTx
.getUpdate()
.getProtocolParamUpdates()
.entrySet()
Expand Down Expand Up @@ -140,7 +140,7 @@ private List<ParamProposal> handleParamProposal(AggregatedTx aggregatedTx, Tx tx
.maxCollateralInputs(maxCollateralInputs)
.registeredTx(tx)
.build();
}).collect(Collectors.toSet());
}).collect(Collectors.toList());

if (CollectionUtils.isEmpty(paramProposals)) {
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
import org.cardanofoundation.explorer.consumercommon.entity.*;
import org.cardanofoundation.explorer.consumercommon.enumeration.TokenType;
import org.cardanofoundation.ledgersync.common.common.Era;
import org.cardanofoundation.ledgersync.common.common.constant.Constant;
import org.cardanofoundation.ledgersync.common.util.HexUtil;
import org.cardanofoundation.ledgersync.explorerconsumer.aggregate.*;
import org.cardanofoundation.ledgersync.explorerconsumer.constant.ConsumerConstant;
import org.cardanofoundation.ledgersync.explorerconsumer.converter.AvvmAddressConverter;
import org.cardanofoundation.ledgersync.explorerconsumer.converter.CostModelConverter;
import org.cardanofoundation.ledgersync.explorerconsumer.dto.GenesisData;
Expand All @@ -29,7 +27,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.PostMapping;

import java.math.BigDecimal;
import java.math.BigInteger;
Expand Down Expand Up @@ -88,6 +85,9 @@ public class GenesisDataServiceImpl implements GenesisDataService {
public static final String EX_UNITS_STEPS = "exUnitsSteps";
public static final String MAX_VALUE_SIZE = "maxValueSize";
public static final String EPOCH_LENGTH = "epochLength";
public static final String UPDATE_QUORUM = "updateQuorum";

public static final String DELEGATION_KEYS = "genDelegs";

@Value("${genesis.byron}")
String genesisByron;
Expand All @@ -104,6 +104,9 @@ public class GenesisDataServiceImpl implements GenesisDataService {
GenesisData genesisData;
Integer shelleyEpochLength;
Long byronKnownTime;
Integer updateQuorum;
Set<String> delegationKeyHashes;

final ObjectMapper objectMapper;
final BlockRepository blockRepository;
final SlotLeaderRepository slotLeaderRepository;
Expand Down Expand Up @@ -221,6 +224,16 @@ public Integer getShelleyEpochLength() {
return shelleyEpochLength;
}

@Override
public Integer getUpdateQuorum() {
return updateQuorum;
}

@Override
public Set<String> getDelegationKeyHashes() {
return delegationKeyHashes;
}

/**
* Fetching data from byron-genesis.json link as json string then deserialize json string into map
* for extracting, mapping to Java object. If Webclient can't fetch data from it will retry 10
Expand Down Expand Up @@ -394,7 +407,8 @@ public void fetchShelleyGenesis(GenesisData genesisData) {
});

shelleyEpochLength = Integer.parseInt(genesisShelleyJsonMap.get(EPOCH_LENGTH).toString());

updateQuorum = Integer.parseInt(genesisShelleyJsonMap.get(UPDATE_QUORUM).toString());
delegationKeyHashes = ((Map<String, Object>) genesisShelleyJsonMap.get(DELEGATION_KEYS)).keySet();
var protocolParams = (Map<String, Object>) genesisShelleyJsonMap.get(PROTOCOL_PARAMS);
var extraEntropy = (Map<String, Object>) protocolParams.get(EXTRA_ENTROPY);
var protocolVersion = (Map<String, Object>) protocolParams.get(PROTOCOL_VERSION);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.cardanofoundation.ledgersync.explorerconsumer.util;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.cardanofoundation.explorer.consumercommon.entity.ParamProposal;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class EpochParamUtil {
private EpochParamUtil() {
}

/**
* Gets the parameter proposal to update epoch param based on the previous parameter proposals,
* delegation keys, and an updateQuorum value.
*
* @param prevParamProposals The collection of parameter proposals of previous epoch.
* @param delegationKeys The collection of hash of delegation keys in shelly genesis file.
* @param updateQuorum The updateQuorum value in shelly genesis file.
* @return The parameter proposal to update, or null if none meets the criteria.
*/
public static ParamProposal getParamProposalToUpdate(Collection<ParamProposal> prevParamProposals,
Collection<String> delegationKeys, int updateQuorum) {
Set<String> paramProposalKeys = prevParamProposals.stream().map(ParamProposal::getKey)
.collect(Collectors.toSet());
if (paramProposalKeys.size() < updateQuorum || !delegationKeys.containsAll(paramProposalKeys)) {
return null;
}

Map<String, ParamProposal> keyParamProposalMap = new HashMap<>();
for (ParamProposal paramProposal : prevParamProposals) {
keyParamProposalMap.put(paramProposal.getKey(), paramProposal);
}

Map<ParamProposalUpdate, Integer> paramProposalUpdateMap = new HashMap<>();

for (ParamProposal proposal : keyParamProposalMap.values()) {
ParamProposalUpdate update = new ParamProposalUpdate(proposal);
paramProposalUpdateMap.merge(update, 1, Integer::sum);
if (paramProposalUpdateMap.get(update) >= updateQuorum) {
return proposal;
}
}

return null;
}

private record ParamProposalUpdate(ParamProposal proposal) {

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}

ParamProposalUpdate other = (ParamProposalUpdate) obj;
return EqualsBuilder.reflectionEquals(proposal, other.proposal, "id", "key");
}

@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(proposal, "id", "key");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import static org.mockito.ArgumentMatchers.any;

Expand Down Expand Up @@ -178,8 +180,25 @@ void setDefShelleyEpochParamEraAlonzo() {
Optional<Epoch> optionalEpoch = Optional.of(epoch);
Optional<Epoch> optionalEpoch2 = Optional.of(epoch2);

ParamProposal paramProposal = Mockito.mock(ParamProposal.class);
List<ParamProposal> prevParamProposals = List.of(paramProposal);
Mockito.when(genesisDataService.getUpdateQuorum()).thenReturn(4);
Mockito.when(genesisDataService.getDelegationKeyHashes()).thenReturn(Set.of(
"637f2e950b0fd8f8e3e811c5fbeb19e411e7a2bf37272b84b29c1a0b",
"8a4b77c4f534f8b8cc6f269e5ebb7ba77fa63a476e50e05e66d7051c",
"b00470cd193d67aac47c373602fccd4195aad3002c169b5570de1126",
"b260ffdb6eba541fcf18601923457307647dce807851b9d19da133ab"
));

ParamProposal paramProposal1 = Mockito.mock(ParamProposal.class);
ParamProposal paramProposal2 = Mockito.mock(ParamProposal.class);
ParamProposal paramProposal3 = Mockito.mock(ParamProposal.class);
ParamProposal paramProposal4 = Mockito.mock(ParamProposal.class);

Mockito.when(paramProposal1.getKey()).thenReturn("637f2e950b0fd8f8e3e811c5fbeb19e411e7a2bf37272b84b29c1a0b");
Mockito.when(paramProposal2.getKey()).thenReturn("8a4b77c4f534f8b8cc6f269e5ebb7ba77fa63a476e50e05e66d7051c");
Mockito.when(paramProposal3.getKey()).thenReturn("b00470cd193d67aac47c373602fccd4195aad3002c169b5570de1126");
Mockito.when(paramProposal4.getKey()).thenReturn("b260ffdb6eba541fcf18601923457307647dce807851b9d19da133ab");

List<ParamProposal> prevParamProposals = List.of(paramProposal1, paramProposal2, paramProposal3, paramProposal4);
Mockito.when(paramProposalRepository.findParamProposalsByEpochNo(4))
.thenReturn(prevParamProposals);
Block cachedBlock = Mockito.mock(Block.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.cardanofoundation.ledgersync.explorerconsumer.util;

import org.cardanofoundation.explorer.consumercommon.entity.ParamProposal;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;
import java.util.Collection;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class EpochParamUtilTest {

@Test
void testGetParamProposalToUpdate_WhenParamProposalKeysSmallerThanUpdateQuorum() {
int updateQuorum = 5;
List<String> delegationKeys = List.of(
"ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c",
"b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497",
"60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1",
"f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757",
"162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82"
);

final Collection<ParamProposal> prevParamProposals = List.of(ParamProposal.builder()
.key("ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c")
.build());

final ParamProposal result = EpochParamUtil.getParamProposalToUpdate(prevParamProposals, delegationKeys, updateQuorum);

assertNull(result);
}

@Test
void testGetParamProposalToUpdate_WhenExistParamProposalKeyNotInGenesisDelegationKeys() {
int updateQuorum = 2;
List<String> delegationKeys = List.of(
"ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c",
"b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497",
"60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1"
);

final Collection<ParamProposal> prevParamProposals = List.of(ParamProposal.builder()
.key("162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82")
.build(),
ParamProposal.builder()
.key("60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1")
.build(),
ParamProposal.builder()
.key("b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497")
.build()
);

final ParamProposal result = EpochParamUtil.getParamProposalToUpdate(prevParamProposals, delegationKeys, updateQuorum);

assertNull(result);
}

@Test
void testGetParamProposalToUpdate_WhenProtocolUpdateCanBeUsedToUpdateEpochPram() {
int updateQuorum = 2;
List<String> delegationKeys = List.of(
"ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c",
"b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497",
"60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1"
);

final Collection<ParamProposal> prevParamProposals = List.of(
ParamProposal.builder()
.key("ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c")
.epochNo(200)
.decentralisation(0.0)
.coinsPerUtxoSize(BigInteger.TWO)
.build(),
ParamProposal.builder()
.key("ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c")
.epochNo(100)
.decentralisation(1.0)
.coinsPerUtxoSize(BigInteger.ONE)
.build(),
ParamProposal.builder()
.key("b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497")
.epochNo(100)
.decentralisation(1.0)
.coinsPerUtxoSize(BigInteger.ONE)
.build()
);

final ParamProposal result = EpochParamUtil.getParamProposalToUpdate(prevParamProposals, delegationKeys, updateQuorum);

assertNotNull(result);
assertEquals(100, result.getEpochNo());
assertEquals(1L, result.getDecentralisation());
assertEquals(BigInteger.ONE, result.getCoinsPerUtxoSize());
}
}
Loading