Skip to content

Commit

Permalink
AKI-467: Added a config option to capture deployed contracts
Browse files Browse the repository at this point in the history
-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
  • Loading branch information
jeff-aion committed Oct 27, 2019
1 parent 347e348 commit ec1785d
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 3 deletions.
11 changes: 10 additions & 1 deletion org.aion.avm.core/src/org/aion/avm/core/AvmConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.aion.avm.core;

import java.io.File;
import java.io.PrintStream;

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}
}
19 changes: 17 additions & 2 deletions org.aion.avm.core/src/org/aion/avm/core/AvmImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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{
Expand Down Expand Up @@ -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<>();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
89 changes: 89 additions & 0 deletions org.aion.avm.embed/src/org/aion/cli/DirectoryDeployer.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit ec1785d

Please sign in to comment.