[NC-1615] Upgrade ethereum reference tests (#54)

* Upgrade ethereum reference tests

* Add support for sealEngine: NoProof by skipping PoW validation for ommer headers as well.  Production code continues to always use full validation for ommers.

* Add Constantinople to reference test schedules ready for when we enable Constantinople tests.

* Blacklist the new reference tests that are failing while we investigate them.
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Adrian Sutton 6 years ago committed by GitHub
parent 0b3991b4a0
commit 681b17cce1
  1. 6
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporter.java
  2. 6
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporterTest.java
  3. 5
      ethereum/core/build.gradle
  4. 22
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/BlockImporter.java
  5. 10
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/BlockBodyValidator.java
  6. 31
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetBlockBodyValidator.java
  7. 9
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetBlockImporter.java
  8. 9
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/BlockchainReferenceTestCaseSpec.java
  9. 15
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/BlockchainReferenceTestTools.java
  10. 61
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/GeneralStateReferenceTestTools.java
  11. 6
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/ReferenceTestProtocolSchedules.java
  12. 4
      ethereum/core/src/test/resources/tech/pegasys/pantheon/ethereum/vm/BlockchainReferenceTest.java.template
  13. 4
      ethereum/core/src/test/resources/tech/pegasys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template
  14. 2
      ethereum/referencetests/src/test/resources

@ -27,8 +27,10 @@ public class IbftBlockImporter implements BlockImporter<IbftContext> {
public boolean importBlock( public boolean importBlock(
final ProtocolContext<IbftContext> context, final ProtocolContext<IbftContext> context,
final Block block, final Block block,
final HeaderValidationMode headerValidationMode) { final HeaderValidationMode headerValidationMode,
final boolean result = delegate.importBlock(context, block, headerValidationMode); final HeaderValidationMode ommerValidationMode) {
final boolean result =
delegate.importBlock(context, block, headerValidationMode, ommerValidationMode);
updateVoteTally(result, block.getHeader(), context); updateVoteTally(result, block.getHeader(), context);
return result; return result;
} }

@ -46,7 +46,8 @@ public class IbftBlockImporterTest {
headerBuilder.buildHeader(), headerBuilder.buildHeader(),
new BlockBody(Collections.emptyList(), Collections.emptyList())); new BlockBody(Collections.emptyList(), Collections.emptyList()));
when(delegate.importBlock(context, block, HeaderValidationMode.FULL)).thenReturn(false); when(delegate.importBlock(context, block, HeaderValidationMode.FULL, HeaderValidationMode.FULL))
.thenReturn(false);
importer.importBlock(context, block, HeaderValidationMode.FULL); importer.importBlock(context, block, HeaderValidationMode.FULL);
@ -76,7 +77,8 @@ public class IbftBlockImporterTest {
new BlockHeaderTestFixture().buildHeader(), new BlockHeaderTestFixture().buildHeader(),
new BlockBody(Collections.emptyList(), Collections.emptyList())); new BlockBody(Collections.emptyList(), Collections.emptyList()));
when(delegate.importBlock(context, block, HeaderValidationMode.FULL)).thenReturn(true); when(delegate.importBlock(context, block, HeaderValidationMode.FULL, HeaderValidationMode.FULL))
.thenReturn(true);
importer.importBlock(context, block, HeaderValidationMode.FULL); importer.importBlock(context, block, HeaderValidationMode.FULL);

@ -59,7 +59,10 @@ def generateTestFiles(FileTree jsonPath, File resourcesPath, File templateFile,
fileSets.each { fileSet -> fileSets.each { fileSet ->
def resPath = resourcesPath.getPath().replaceAll("\\\\", "/") def resPath = resourcesPath.getPath().replaceAll("\\\\", "/")
def name = fileSet.first().getPath().toString() def name = fileSet
.find({ !it.getName().toString().startsWith(".")})
.getPath()
.toString()
.replaceAll("\\\\", "/") .replaceAll("\\\\", "/")
.replaceAll(resPath + "/", "") .replaceAll(resPath + "/", "")
.replaceAll(pathstrip, "") .replaceAll(pathstrip, "")

@ -23,8 +23,28 @@ public interface BlockImporter<C> {
* @return {@code true} if the block was added somewhere in the blockchain; otherwise {@code * @return {@code true} if the block was added somewhere in the blockchain; otherwise {@code
* false} * false}
*/ */
default boolean importBlock(
final ProtocolContext<C> context,
final Block block,
final HeaderValidationMode headerValidationMode) {
return importBlock(context, block, headerValidationMode, HeaderValidationMode.FULL);
}
/**
* Attempts to import the given block to the specificed blockchain and world state.
*
* @param context The context to attempt to update
* @param block The block
* @param headerValidationMode Determines the validation to perform on this header.
* @param ommerValidationMode Determines the validation to perform on ommer headers.
* @return {@code true} if the block was added somewhere in the blockchain; otherwise {@code
* false}
*/
boolean importBlock( boolean importBlock(
ProtocolContext<C> context, Block block, HeaderValidationMode headerValidationMode); ProtocolContext<C> context,
Block block,
HeaderValidationMode headerValidationMode,
HeaderValidationMode ommerValidationMode);
/** /**
* Attempts to import the given block. Uses "fast" validation. Performs light validation using the * Attempts to import the given block. Uses "fast" validation. Performs light validation using the

@ -18,13 +18,15 @@ public interface BlockBodyValidator<C> {
* @param receipts The receipts that correspond to the blocks transactions * @param receipts The receipts that correspond to the blocks transactions
* @param worldStateRootHash The rootHash defining the world state after processing this block and * @param worldStateRootHash The rootHash defining the world state after processing this block and
* all of its transactions. * all of its transactions.
* @param ommerValidationMode The validation mode to use for ommer headers
* @return {@code true} if valid; otherwise {@code false} * @return {@code true} if valid; otherwise {@code false}
*/ */
boolean validateBody( boolean validateBody(
ProtocolContext<C> context, ProtocolContext<C> context,
Block block, Block block,
List<TransactionReceipt> receipts, List<TransactionReceipt> receipts,
Hash worldStateRootHash); Hash worldStateRootHash,
final HeaderValidationMode ommerValidationMode);
/** /**
* Validates that the block body is valid, but skips state root validation. * Validates that the block body is valid, but skips state root validation.
@ -32,8 +34,12 @@ public interface BlockBodyValidator<C> {
* @param context The context to validate against * @param context The context to validate against
* @param block The block to validate * @param block The block to validate
* @param receipts The receipts that correspond to the blocks transactions * @param receipts The receipts that correspond to the blocks transactions
* @param ommerValidationMode The validation mode to use for ommer headers
* @return {@code true} if valid; otherwise {@code false} * @return {@code true} if valid; otherwise {@code false}
*/ */
boolean validateBodyLight( boolean validateBodyLight(
ProtocolContext<C> context, Block block, List<TransactionReceipt> receipts); ProtocolContext<C> context,
Block block,
List<TransactionReceipt> receipts,
final HeaderValidationMode ommerValidationMode);
} }

@ -34,9 +34,10 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
final ProtocolContext<C> context, final ProtocolContext<C> context,
final Block block, final Block block,
final List<TransactionReceipt> receipts, final List<TransactionReceipt> receipts,
final Hash worldStateRootHash) { final Hash worldStateRootHash,
final HeaderValidationMode ommerValidationMode) {
if (!validateBodyLight(context, block, receipts)) { if (!validateBodyLight(context, block, receipts, ommerValidationMode)) {
return false; return false;
} }
@ -51,7 +52,8 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
public boolean validateBodyLight( public boolean validateBodyLight(
final ProtocolContext<C> context, final ProtocolContext<C> context,
final Block block, final Block block,
final List<TransactionReceipt> receipts) { final List<TransactionReceipt> receipts,
final HeaderValidationMode ommerValidationMode) {
final BlockHeader header = block.getHeader(); final BlockHeader header = block.getHeader();
final BlockBody body = block.getBody(); final BlockBody body = block.getBody();
@ -75,7 +77,7 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
return false; return false;
} }
if (!validateEthHash(context, block)) { if (!validateEthHash(context, block, ommerValidationMode)) {
return false; return false;
} }
@ -130,7 +132,10 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
return true; return true;
} }
private boolean validateEthHash(final ProtocolContext<C> context, final Block block) { private boolean validateEthHash(
final ProtocolContext<C> context,
final Block block,
final HeaderValidationMode ommerValidationMode) {
final BlockHeader header = block.getHeader(); final BlockHeader header = block.getHeader();
final BlockBody body = block.getBody(); final BlockBody body = block.getBody();
@ -139,7 +144,7 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
return false; return false;
} }
if (!validateOmmers(context, header, body.getOmmers())) { if (!validateOmmers(context, header, body.getOmmers(), ommerValidationMode)) {
return false; return false;
} }
@ -156,7 +161,10 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
} }
private boolean validateOmmers( private boolean validateOmmers(
final ProtocolContext<C> context, final BlockHeader header, final List<BlockHeader> ommers) { final ProtocolContext<C> context,
final BlockHeader header,
final List<BlockHeader> ommers,
final HeaderValidationMode ommerValidationMode) {
if (ommers.size() > MAX_OMMERS) { if (ommers.size() > MAX_OMMERS) {
LOG.warn("Invalid block: ommer count {} exceeds ommer limit {}", ommers.size(), MAX_OMMERS); LOG.warn("Invalid block: ommer count {} exceeds ommer limit {}", ommers.size(), MAX_OMMERS);
return false; return false;
@ -168,7 +176,7 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
} }
for (final BlockHeader ommer : ommers) { for (final BlockHeader ommer : ommers) {
if (!isOmmerValid(context, header, ommer)) { if (!isOmmerValid(context, header, ommer, ommerValidationMode)) {
LOG.warn("Invalid block: ommer is invalid"); LOG.warn("Invalid block: ommer is invalid");
return false; return false;
} }
@ -193,11 +201,14 @@ public class MainnetBlockBodyValidator<C> implements BlockBodyValidator<C> {
} }
private boolean isOmmerValid( private boolean isOmmerValid(
final ProtocolContext<C> context, final BlockHeader current, final BlockHeader ommer) { final ProtocolContext<C> context,
final BlockHeader current,
final BlockHeader ommer,
final HeaderValidationMode ommerValidationMode) {
final ProtocolSpec<C> protocolSpec = protocolSchedule.getByBlockNumber(ommer.getNumber()); final ProtocolSpec<C> protocolSpec = protocolSchedule.getByBlockNumber(ommer.getNumber());
if (!protocolSpec if (!protocolSpec
.getBlockHeaderValidator() .getBlockHeaderValidator()
.validateHeader(ommer, context, HeaderValidationMode.FULL)) { .validateHeader(ommer, context, ommerValidationMode)) {
return false; return false;
} }

@ -37,7 +37,8 @@ public class MainnetBlockImporter<C> implements BlockImporter<C> {
public synchronized boolean importBlock( public synchronized boolean importBlock(
final ProtocolContext<C> context, final ProtocolContext<C> context,
final Block block, final Block block,
final HeaderValidationMode headerValidationMode) { final HeaderValidationMode headerValidationMode,
final HeaderValidationMode ommerValidationMode) {
final BlockHeader header = block.getHeader(); final BlockHeader header = block.getHeader();
final Optional<BlockHeader> maybeParentHeader = final Optional<BlockHeader> maybeParentHeader =
@ -65,7 +66,8 @@ public class MainnetBlockImporter<C> implements BlockImporter<C> {
} }
final List<TransactionReceipt> receipts = result.getReceipts(); final List<TransactionReceipt> receipts = result.getReceipts();
if (!blockBodyValidator.validateBody(context, block, receipts, worldState.rootHash())) { if (!blockBodyValidator.validateBody(
context, block, receipts, worldState.rootHash(), ommerValidationMode)) {
return false; return false;
} }
@ -86,7 +88,8 @@ public class MainnetBlockImporter<C> implements BlockImporter<C> {
return false; return false;
} }
if (!blockBodyValidator.validateBodyLight(context, block, receipts)) { if (!blockBodyValidator.validateBodyLight(
context, block, receipts, HeaderValidationMode.FULL)) {
return false; return false;
} }

@ -45,6 +45,7 @@ public class BlockchainReferenceTestCaseSpec {
private final WorldStateArchive worldStateArchive; private final WorldStateArchive worldStateArchive;
private final MutableBlockchain blockchain; private final MutableBlockchain blockchain;
private final String sealEngine;
private final ProtocolContext<Void> protocolContext; private final ProtocolContext<Void> protocolContext;
@ -78,7 +79,8 @@ public class BlockchainReferenceTestCaseSpec {
@JsonProperty("genesisBlockHeader") final BlockHeaderMock genesisBlockHeader, @JsonProperty("genesisBlockHeader") final BlockHeaderMock genesisBlockHeader,
@JsonProperty("genesisRLP") final String genesisRLP, @JsonProperty("genesisRLP") final String genesisRLP,
@JsonProperty("pre") final Map<String, WorldStateMock.AccountMock> accounts, @JsonProperty("pre") final Map<String, WorldStateMock.AccountMock> accounts,
@JsonProperty("lastblockhash") final String lastBlockHash) { @JsonProperty("lastblockhash") final String lastBlockHash,
@JsonProperty("sealEngine") final String sealEngine) {
this.network = network; this.network = network;
this.candidateBlocks = candidateBlocks; this.candidateBlocks = candidateBlocks;
this.genesisBlockHeader = genesisBlockHeader; this.genesisBlockHeader = genesisBlockHeader;
@ -86,6 +88,7 @@ public class BlockchainReferenceTestCaseSpec {
this.lastBlockHash = Hash.fromHexString(lastBlockHash); this.lastBlockHash = Hash.fromHexString(lastBlockHash);
this.worldStateArchive = buildWorldStateArchive(accounts); this.worldStateArchive = buildWorldStateArchive(accounts);
this.blockchain = buildBlockchain(genesisBlockHeader); this.blockchain = buildBlockchain(genesisBlockHeader);
this.sealEngine = sealEngine;
this.protocolContext = new ProtocolContext<>(this.blockchain, this.worldStateArchive, null); this.protocolContext = new ProtocolContext<>(this.blockchain, this.worldStateArchive, null);
} }
@ -117,6 +120,10 @@ public class BlockchainReferenceTestCaseSpec {
return lastBlockHash; return lastBlockHash;
} }
public String getSealEngine() {
return sealEngine;
}
public static class BlockHeaderMock extends BlockHeader { public static class BlockHeaderMock extends BlockHeader {
@JsonCreator @JsonCreator

@ -54,8 +54,17 @@ public class BlockchainReferenceTestTools {
params.blacklist("ChainAtoChainB_BlockHash_(Frontier|Homestead|EIP150|EIP158|Byzantium)"); params.blacklist("ChainAtoChainB_BlockHash_(Frontier|Homestead|EIP150|EIP158|Byzantium)");
// Known bad test. // Known bad test.
params.blacklist("RevertPrecompiledTouch_d0g0v0_(EIP158|Byzantium)"); params.blacklist("RevertPrecompiledTouch_d0g0v0_(EIP158|Byzantium)");
// Consumes a huge amount of memory // Consumes a huge amount of memory
params.blacklist("static_Call1MB1024Calldepth_d1g0v0_Byzantium"); params.blacklist("static_Call1MB1024Calldepth_d1g0v0_Byzantium");
// Pantheon is incorrectly rejecting Uncle block timestamps in the future
params.blacklist("futureUncleTimestampDifficultyDrop2");
params.blacklist("futureUncleTimestampDifficultyDrop");
// Needs investigation
params.blacklist("RevertInCreateInInit_d0g0v0_Byzantium");
params.blacklist("RevertInCreateInInit_d0g0v0_Constantinople");
} }
public static Collection<Object[]> generateTestParametersForConfig(final String[] filePath) { public static Collection<Object[]> generateTestParametersForConfig(final String[] filePath) {
@ -86,8 +95,12 @@ public class BlockchainReferenceTestTools {
final ProtocolSpec<Void> protocolSpec = final ProtocolSpec<Void> protocolSpec =
schedule.getByBlockNumber(block.getHeader().getNumber()); schedule.getByBlockNumber(block.getHeader().getNumber());
final BlockImporter<Void> blockImporter = protocolSpec.getBlockImporter(); final BlockImporter<Void> blockImporter = protocolSpec.getBlockImporter();
final HeaderValidationMode validationMode =
"NoProof".equalsIgnoreCase(spec.getSealEngine())
? HeaderValidationMode.LIGHT
: HeaderValidationMode.FULL;
final boolean imported = final boolean imported =
blockImporter.importBlock(context, block, HeaderValidationMode.FULL); blockImporter.importBlock(context, block, validationMode, validationMode);
assertThat(imported).isEqualTo(candidateBlock.isValid()); assertThat(imported).isEqualTo(candidateBlock.isValid());
} catch (final RLPException e) { } catch (final RLPException e) {

@ -71,6 +71,67 @@ public class GeneralStateReferenceTestTools {
params.blacklist("OverflowGasRequire"); params.blacklist("OverflowGasRequire");
// Consumes a huge amount of memory // Consumes a huge amount of memory
params.blacklist("static_Call1MB1024Calldepth-Byzantium"); params.blacklist("static_Call1MB1024Calldepth-Byzantium");
// Needs investigation (tests pass in other clients)
params.blacklist("createNameRegistratorPerTxsNotEnoughGas-Frontier\\[0\\]");
params.blacklist("NotEnoughCashContractCreation-Frontier");
params.blacklist("NotEnoughCashContractCreation-Homestead");
params.blacklist("NotEnoughCashContractCreation-EIP150");
params.blacklist("OutOfGasContractCreation-EIP150\\[0\\]");
params.blacklist("OutOfGasContractCreation-EIP150\\[2\\]");
params.blacklist("OutOfGasContractCreation-Homestead\\[0\\]");
params.blacklist("OutOfGasContractCreation-Homestead\\[2\\]");
params.blacklist("OutOfGasPrefundedContractCreation-EIP150");
params.blacklist("OutOfGasPrefundedContractCreation-Homestead");
params.blacklist("201503110226PYTHON_DUP6-EIP150");
params.blacklist("201503110226PYTHON_DUP6-Frontier");
params.blacklist("201503110226PYTHON_DUP6-Homestead");
params.blacklist("RevertOpcodeWithBigOutputInInit-EIP150\\[2\\]");
params.blacklist("RevertOpcodeWithBigOutputInInit-EIP150\\[3\\]");
params.blacklist("RevertOpcodeWithBigOutputInInit-Homestead\\[2\\]");
params.blacklist("RevertOpcodeWithBigOutputInInit-Homestead\\[3\\]");
params.blacklist("RevertInCreateInInit-Byzantium");
params.blacklist("RevertOpcodeInInit-EIP150\\[2\\]");
params.blacklist("RevertOpcodeInInit-EIP150\\[3\\]");
params.blacklist("RevertOpcodeInInit-Homestead\\[2\\]");
params.blacklist("RevertOpcodeInInit-Homestead\\[3\\]");
params.blacklist("suicideCoinbase-Frontier");
params.blacklist("suicideCoinbase-Homestead");
params.blacklist("TransactionNonceCheck-EIP150");
params.blacklist("TransactionNonceCheck-Frontier");
params.blacklist("TransactionNonceCheck-Homestead");
params.blacklist("EmptyTransaction-EIP150");
params.blacklist("EmptyTransaction-Frontier");
params.blacklist("EmptyTransaction-Homestead");
params.blacklist("RefundOverflow-EIP150");
params.blacklist("RefundOverflow-Frontier");
params.blacklist("RefundOverflow-Homestead");
params.blacklist("TransactionToItselfNotEnoughFounds-EIP150");
params.blacklist("TransactionToItselfNotEnoughFounds-Frontier");
params.blacklist("TransactionToItselfNotEnoughFounds-Homestead");
params.blacklist("TransactionNonceCheck2-EIP150");
params.blacklist("TransactionNonceCheck2-Frontier");
params.blacklist("TransactionNonceCheck2-Homestead");
params.blacklist("CreateTransactionReverted-EIP150");
params.blacklist("CreateTransactionReverted-Frontier");
params.blacklist("CreateTransactionReverted-Homestead");
params.blacklist("RefundOverflow2-EIP150");
params.blacklist("RefundOverflow2-Frontier");
params.blacklist("RefundOverflow2-Homestead");
params.blacklist("SuicidesMixingCoinbase-Frontier\\[0\\]");
params.blacklist("SuicidesMixingCoinbase-Frontier\\[1\\]");
params.blacklist("SuicidesMixingCoinbase-Homestead\\[0\\]");
params.blacklist("SuicidesMixingCoinbase-Homestead\\[1\\]");
params.blacklist("createNameRegistratorPerTxsNotEnoughGasBefore-EIP150");
params.blacklist("createNameRegistratorPerTxsNotEnoughGasBefore-Homestead");
params.blacklist("createNameRegistratorPerTxsNotEnoughGasAfter-EIP150");
params.blacklist("createNameRegistratorPerTxsNotEnoughGasAfter-Homestead");
params.blacklist("createNameRegistratorPerTxsNotEnoughGasAt-EIP150");
params.blacklist("createNameRegistratorPerTxsNotEnoughGasAt-Homestead");
params.blacklist("UserTransactionGasLimitIsTooLowWhenZeroCost-EIP150");
params.blacklist("UserTransactionGasLimitIsTooLowWhenZeroCost-Frontier");
params.blacklist("UserTransactionGasLimitIsTooLowWhenZeroCost-Homestead");
params.blacklist("ecmul_0-3_5616_28000_96-Byzantium\\[3\\]");
} }
public static Collection<Object[]> generateTestParametersForConfig(final String[] filePath) { public static Collection<Object[]> generateTestParametersForConfig(final String[] filePath) {

@ -31,7 +31,11 @@ public class ReferenceTestProtocolSchedules {
builder.put( builder.put(
"Byzantium", "Byzantium",
createSchedule( createSchedule(
protocolSpecLookup -> MainnetProtocolSpecs.byzantium(CHAIN_ID, protocolSpecLookup))); protocolSchedule -> MainnetProtocolSpecs.byzantium(CHAIN_ID, protocolSchedule)));
builder.put(
"Constantinople",
createSchedule(
protocolSchedule -> MainnetProtocolSpecs.constantinople(CHAIN_ID, protocolSchedule)));
return new ReferenceTestProtocolSchedules(builder.build()); return new ReferenceTestProtocolSchedules(builder.build());
} }

@ -26,7 +26,9 @@ public class %%TESTS_NAME%% {
private final String name; private final String name;
private final BlockchainReferenceTestCaseSpec spec; private final BlockchainReferenceTestCaseSpec spec;
public %%TESTS_NAME%%(String name, BlockchainReferenceTestCaseSpec spec) { public %%TESTS_NAME%%(
final String name,
final BlockchainReferenceTestCaseSpec spec) {
this.name = name; this.name = name;
this.spec = spec; this.spec = spec;
} }

@ -26,7 +26,9 @@ public class %%TESTS_NAME%% {
private final String name; private final String name;
private final GeneralStateTestCaseEipSpec spec; private final GeneralStateTestCaseEipSpec spec;
public %%TESTS_NAME%%(String name, GeneralStateTestCaseEipSpec spec) { public %%TESTS_NAME%%(
final String name,
final GeneralStateTestCaseEipSpec spec) {
this.name = name; this.name = name;
this.spec = spec; this.spec = spec;
} }

@ -1 +1 @@
Subproject commit 2bb0c3da3bbb15c528bcef2a7e5ac4bd73f81f87 Subproject commit 0c76bf7d18e63bb65e4b8ffdd20e602e97e1c83b
Loading…
Cancel
Save