From ec1785d2bf2b3c74ad6c1de0197804c794259fce Mon Sep 17 00:00:00 2001 From: Jeff Disher Date: Fri, 25 Oct 2019 14:53:31 -0400 Subject: [PATCH] AKI-467: Added a config option to capture deployed contracts -if set, this will capture all deployed contracts to the given directory -these can then be re-imported for offline analysis by the DirectoryDeployer tool --- .../org/aion/avm/core/AvmConfiguration.java | 11 ++- .../src/org/aion/avm/core/AvmImpl.java | 19 +++- .../avm/core/util/ContractCaptureTool.java | 46 ++++++++++ .../src/org/aion/cli/DirectoryDeployer.java | 89 +++++++++++++++++++ .../cli/DirectoryDeployerIntegrationTest.java | 73 +++++++++++++++ 5 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 org.aion.avm.core/src/org/aion/avm/core/util/ContractCaptureTool.java create mode 100644 org.aion.avm.embed/src/org/aion/cli/DirectoryDeployer.java create mode 100644 org.aion.avm.embed/test/org/aion/cli/DirectoryDeployerIntegrationTest.java diff --git a/org.aion.avm.core/src/org/aion/avm/core/AvmConfiguration.java b/org.aion.avm.core/src/org/aion/avm/core/AvmConfiguration.java index 1c2ca840b..d299e2461 100644 --- a/org.aion.avm.core/src/org/aion/avm/core/AvmConfiguration.java +++ b/org.aion.avm.core/src/org/aion/avm/core/AvmConfiguration.java @@ -1,5 +1,6 @@ package org.aion.avm.core; +import java.io.File; import java.io.PrintStream; /** @@ -38,9 +39,15 @@ public class AvmConfiguration { public boolean enableBlockchainPrintln; /** * If set to non-null, enables the collection of deployment data: various information collected about deployed contracts. - * When shutting down the AVM instance, a histgram of this data will be dumped to the given PrintStream. + * When shutting down the AVM instance, a histogram of this data will be dumped to the given PrintStream. */ public PrintStream deploymentDataHistorgramOutput; + /** + * If set to non-null, enables the capture of all deployed contracts into this directory, along with other information + * describing the context of the deployment transaction. + * This is useful for capturing the data from a network for offline analysis. + */ + public File contractCaptureDirectory; public AvmConfiguration() { // 4 threads is generally a safe, yet useful, number. @@ -54,5 +61,7 @@ public AvmConfiguration() { this.enableBlockchainPrintln = true; // This is not a cheap bit of instrumentation so we disable it, by default. this.deploymentDataHistorgramOutput = null; + // This is a very uncommon use-case so it defaults to off. + this.contractCaptureDirectory = null; } } diff --git a/org.aion.avm.core/src/org/aion/avm/core/AvmImpl.java b/org.aion.avm.core/src/org/aion/avm/core/AvmImpl.java index bd8efd3ca..ea7d5ed09 100644 --- a/org.aion.avm.core/src/org/aion/avm/core/AvmImpl.java +++ b/org.aion.avm.core/src/org/aion/avm/core/AvmImpl.java @@ -17,6 +17,7 @@ import org.aion.avm.core.persistence.LoadedDApp; import org.aion.avm.core.util.ByteArrayWrapper; +import org.aion.avm.core.util.ContractCaptureTool; import org.aion.avm.core.util.SoftCache; import i.IInstrumentation; import i.IInstrumentationFactory; @@ -54,6 +55,7 @@ public class AvmImpl implements AvmInternal { private final boolean enableVerboseConcurrentExecutor; private final boolean enableBlockchainPrintln; private final HistogramDataCollector histogramDataCollector; + private final ContractCaptureTool contractCaptureTool; public AvmImpl(IInstrumentationFactory instrumentationFactory, IExternalCapabilities capabilities, AvmConfiguration configuration) { this.instrumentationFactory = instrumentationFactory; @@ -71,6 +73,9 @@ public AvmImpl(IInstrumentationFactory instrumentationFactory, IExternalCapabili this.histogramDataCollector = (null != configuration.deploymentDataHistorgramOutput) ? new HistogramDataCollector(configuration.deploymentDataHistorgramOutput) : null; + this.contractCaptureTool = (null != configuration.contractCaptureDirectory) + ? new ContractCaptureTool(configuration.contractCaptureDirectory) + : null; } private class AvmExecutorThread extends Thread{ @@ -162,6 +167,11 @@ public void start() { RuntimeAssertionError.assertTrue(null == AvmImpl.currentAvm); AvmImpl.currentAvm = this; + // See if we need to enable the contract capture. + if (null != this.contractCaptureTool) { + this.contractCaptureTool.startup(); + } + RuntimeAssertionError.assertTrue(null == this.hotCache); RuntimeAssertionError.assertTrue(null == this.transformedCodeCache); this.hotCache = new SoftCache<>(); @@ -459,11 +469,16 @@ private AvmWrappedTransactionResult commonInvoke(IExternalState parentKernel // do nothing for balance transfers of which the recipient is not a DApp address. if (isCreate) { - if (null != this.histogramDataCollector) { + if ((null != this.histogramDataCollector) || (null != this.contractCaptureTool)) { CodeAndArguments codeAndArguments = CodeAndArguments.decodeFromBytes(transactionData); // If this data is invalid, we will get null. We don't bother tracking this. if (null != codeAndArguments) { - this.histogramDataCollector.collectDataFromJarBytes(codeAndArguments.code); + if (null != this.histogramDataCollector) { + this.histogramDataCollector.collectDataFromJarBytes(codeAndArguments.code); + } + if (null != this.contractCaptureTool) { + this.contractCaptureTool.captureDeployment(parentKernel.getBlockNumber(), senderAddress, recipient, nonce, codeAndArguments.code, codeAndArguments.arguments); + } } } result = DAppCreator.create(this.capabilities, thisTransactionKernel, this, task, senderAddress, recipient, effectiveTransactionOrigin, transactionData, transactionHash, energyLimit, energyPrice, transactionValue, result, this.preserveDebuggability, this.enableVerboseContractErrors, this.enableBlockchainPrintln); diff --git a/org.aion.avm.core/src/org/aion/avm/core/util/ContractCaptureTool.java b/org.aion.avm.core/src/org/aion/avm/core/util/ContractCaptureTool.java new file mode 100644 index 000000000..18e231e66 --- /dev/null +++ b/org.aion.avm.core/src/org/aion/avm/core/util/ContractCaptureTool.java @@ -0,0 +1,46 @@ +package org.aion.avm.core.util; + +import java.io.File; +import java.math.BigInteger; + +import org.aion.types.AionAddress; + +import i.RuntimeAssertionError; + + +/** + * Created as part of AKI-467 to capture contracts deployed on an AVM instance for offline analysis. + */ +public class ContractCaptureTool { + private static final String CODE_FILE_NAME = "code.jar"; + private static final String ARGUMENTS_FILE_NAME = "arguments.bin"; + private static final String CREATOR_FILE_NAME = "creator.bin"; + private static final String NONCE_FILE_NAME = "creator_nonce.bin"; + private static final String BLOCK_FILE_NAME = "block_height.txt"; + + private final File contractCaptureDirectory; + + public ContractCaptureTool(File contractCaptureDirectory) { + this.contractCaptureDirectory = contractCaptureDirectory; + } + + public void startup() { + if (!this.contractCaptureDirectory.exists()) { + this.contractCaptureDirectory.mkdirs(); + } + RuntimeAssertionError.assertTrue(this.contractCaptureDirectory.isDirectory()); + } + + public void captureDeployment(long blockNumber, AionAddress senderAddress, AionAddress newContractAddress, BigInteger senderNonce, byte[] code, byte[] arguments) { + String newContract = Helpers.bytesToHexString(newContractAddress.toByteArray()); + File thisDirectory = new File(this.contractCaptureDirectory, newContract); + thisDirectory.mkdirs(); + Helpers.writeBytesToFile(code, new File(thisDirectory, CODE_FILE_NAME).getAbsolutePath()); + if (null != arguments) { + Helpers.writeBytesToFile(arguments, new File(thisDirectory, ARGUMENTS_FILE_NAME).getAbsolutePath()); + } + Helpers.writeBytesToFile(senderAddress.toByteArray(), new File(thisDirectory, CREATOR_FILE_NAME).getAbsolutePath()); + Helpers.writeBytesToFile(senderNonce.toByteArray(), new File(thisDirectory, NONCE_FILE_NAME).getAbsolutePath()); + Helpers.writeBytesToFile(Long.toString(blockNumber).getBytes(), new File(thisDirectory, BLOCK_FILE_NAME).getAbsolutePath()); + } +} diff --git a/org.aion.avm.embed/src/org/aion/cli/DirectoryDeployer.java b/org.aion.avm.embed/src/org/aion/cli/DirectoryDeployer.java new file mode 100644 index 000000000..a3629e952 --- /dev/null +++ b/org.aion.avm.embed/src/org/aion/cli/DirectoryDeployer.java @@ -0,0 +1,89 @@ +package org.aion.cli; + +import org.aion.avm.core.AvmConfiguration; +import org.aion.avm.core.AvmImpl; +import org.aion.avm.core.AvmTransactionUtil; +import org.aion.avm.core.CommonAvmFactory; +import org.aion.avm.core.ExecutionType; +import org.aion.avm.core.FutureResult; +import org.aion.avm.core.IExternalCapabilities; +import org.aion.avm.core.IExternalState; +import org.aion.avm.core.util.Helpers; +import org.aion.avm.embed.StandardCapabilities; +import org.aion.avm.userlib.CodeAndArguments; +import org.aion.kernel.*; +import org.aion.types.AionAddress; +import org.aion.types.Transaction; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; + + +/** + * A tool to run our data collection histogram against pre-created contracts. + * Point this at a directory containing code and argument data and it will deploy every contract found, with corresponding argument data. + * The expected shape of the directory given is produced by the AvmConfiguration.contractCaptureDirectory option. + * + * NOTE: This currently deploys 1 contract at a time, in the order they are found in the directory, using the same emulated block. + * In the future, this may be changed to deploy them in the order and blocks defined by the other meta-data. + */ +public class DirectoryDeployer { + private static AionAddress DEPLOYER = Helpers.randomAddress(); + private static TestingBlock BLOCK = new TestingBlock(new byte[32], 1, DEPLOYER, System.currentTimeMillis(), new byte[0]); + private static long ENERGY_LIMIT = 5_000_000L; + private static long ENERGY_PRICE = 1L; + + public static void main(String[] args) { + TestingState kernel = new TestingState(BLOCK); + kernel.adjustBalance(DEPLOYER, new BigInteger("10000000000000000000000")); + IExternalCapabilities capabilities = new StandardCapabilities(); + AvmConfiguration config = new AvmConfiguration(); + config.deploymentDataHistorgramOutput = System.out; + AvmImpl avm = CommonAvmFactory.buildAvmInstanceForConfiguration(capabilities, config); + + File rootDirectory = new File(args[0]); + assertTrue(rootDirectory.isDirectory()); + int passCount = 0; + int failCount = 0; + for (String name : rootDirectory.list()) { + try { + Transaction transaction = createTransaction(kernel, new File(rootDirectory, name)); + FutureResult[] futures = avm.run(kernel, new Transaction[] { transaction }, ExecutionType.ASSUME_MAINCHAIN, 0); + boolean success = futures[0].getResult().transactionStatus.isSuccess(); + if (success) { + passCount += 1; + } else { + failCount += 1; + } + System.out.println(name + ": " + (success ? "PASS" : "FAIL")); + } catch (IOException e) { + System.err.println(name); + e.printStackTrace(); + System.exit(1); + } + } + System.out.println("Attempted " + (passCount + failCount) + " deployments (" + passCount + " passed, " + failCount + " failed)"); + avm.shutdown(); + } + + + private static Transaction createTransaction(IExternalState kernel, File directory) throws IOException { + // For now, we will only load the code and the arguments. + byte[] code = Files.readAllBytes(new File(directory, "code.jar").toPath()); + File argFile = new File(directory, "arguments.bin"); + byte[] args = argFile.exists() + ? Files.readAllBytes(argFile.toPath()) + : new byte[0]; + byte[] createData = new CodeAndArguments(code, args).encodeToBytes(); + return AvmTransactionUtil.create(DEPLOYER, kernel.getNonce(DEPLOYER), BigInteger.ZERO, createData, ENERGY_LIMIT, ENERGY_PRICE); + } + + private static void assertTrue(boolean flag) { + // We use a private helper to manage the assertions since the JDK default disables them. + if (!flag) { + throw new AssertionError("Case must be true"); + } + } +} diff --git a/org.aion.avm.embed/test/org/aion/cli/DirectoryDeployerIntegrationTest.java b/org.aion.avm.embed/test/org/aion/cli/DirectoryDeployerIntegrationTest.java new file mode 100644 index 000000000..0f0c03212 --- /dev/null +++ b/org.aion.avm.embed/test/org/aion/cli/DirectoryDeployerIntegrationTest.java @@ -0,0 +1,73 @@ +package org.aion.cli; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; + +import org.aion.kernel.TestingState; +import org.aion.types.AionAddress; +import org.aion.types.Transaction; +import org.aion.types.TransactionResult; +import org.aion.avm.core.AvmConfiguration; +import org.aion.avm.core.AvmImpl; +import org.aion.avm.core.AvmTransactionUtil; +import org.aion.avm.core.CommonAvmFactory; +import org.aion.avm.core.ExecutionType; +import org.aion.avm.core.IExternalCapabilities; +import org.aion.avm.core.dappreading.UserlibJarBuilder; +import org.aion.avm.core.util.Helpers; +import org.aion.avm.embed.StandardCapabilities; +import org.aion.avm.userlib.CodeAndArguments; +import org.aion.kernel.TestingBlock; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + + +public class DirectoryDeployerIntegrationTest { + private static AionAddress DEPLOYER = Helpers.randomAddress(); + private static TestingBlock BLOCK = new TestingBlock(new byte[32], 1, DEPLOYER, System.currentTimeMillis(), new byte[0]); + private static long ENERGY_LIMIT = 5_000_000L; + private static long ENERGY_PRICE = 1L; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void compare() throws Exception { + // The test we are interested in is is seeing if the histogram caused by deploying a basic contract is the same when in-process and out-of-process. + ByteArrayOutputStream captureStream = new ByteArrayOutputStream(); + TestingState kernel = new TestingState(BLOCK); + kernel.adjustBalance(DEPLOYER, new BigInteger("10000000000000000000000")); + IExternalCapabilities capabilities = new StandardCapabilities(); + AvmConfiguration config = new AvmConfiguration(); + config.deploymentDataHistorgramOutput = new PrintStream(captureStream); + config.contractCaptureDirectory = folder.newFolder(); + AvmImpl avm = CommonAvmFactory.buildAvmInstanceForConfiguration(capabilities, config); + byte[] deployment = new CodeAndArguments(UserlibJarBuilder.buildJarForMainAndClassesAndUserlib(SimpleStackDemo.class), null).encodeToBytes(); + Transaction transaction = AvmTransactionUtil.create(DEPLOYER, kernel.getNonce(DEPLOYER), BigInteger.ZERO, deployment, ENERGY_LIMIT, ENERGY_PRICE); + TransactionResult result = avm.run(kernel, new Transaction[] {transaction}, ExecutionType.ASSUME_MAINCHAIN, kernel.getBlockNumber() - 1)[0].getResult(); + Assert.assertTrue(result.transactionStatus.isSuccess()); + AionAddress contractAddress = new AionAddress(result.copyOfTransactionOutput().orElseThrow()); + String hexContract = Helpers.bytesToHexString(contractAddress.toByteArray()); + avm.shutdown(); + + // By this point, we should be able to capture the output. + String totalHistorgram = new String(captureStream.toByteArray(), StandardCharsets.UTF_8); + + // Make sure that we see the contract in that directory. + Assert.assertEquals(1, config.contractCaptureDirectory.listFiles((file) -> file.getName().equals(hexContract)).length); + + // Now, invoke the DirectoryDeployer on the captured directory and make sure its output has this histogram at the end. + PrintStream originalStdOut = System.out; + ByteArrayOutputStream fakeStdOutBuffer = new ByteArrayOutputStream(); + System.setOut(new PrintStream(fakeStdOutBuffer)); + DirectoryDeployer.main(new String[] { config.contractCaptureDirectory.getAbsolutePath() }); + System.setOut(originalStdOut); + + String fromCli = new String(fakeStdOutBuffer.toByteArray(), StandardCharsets.UTF_8); + Assert.assertTrue(fromCli.endsWith(totalHistorgram)); + } +}