diff --git a/rskj-core/src/main/java/co/rsk/core/bc/TransactionPoolImpl.java b/rskj-core/src/main/java/co/rsk/core/bc/TransactionPoolImpl.java index 967b8d49fd3..62205b00ab8 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/TransactionPoolImpl.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/TransactionPoolImpl.java @@ -26,6 +26,7 @@ import co.rsk.net.TransactionValidationResult; import co.rsk.net.handler.TxPendingValidator; import co.rsk.net.handler.quota.TxQuotaChecker; +import co.rsk.util.ContractUtil; import com.google.common.annotations.VisibleForTesting; import org.ethereum.core.*; import org.ethereum.db.BlockStore; @@ -477,7 +478,13 @@ private boolean senderCanPayPendingTransactionsAndNewTx(Transaction newTx, Repos } Coin costWithNewTx = accumTxCost.add(getTxBaseCost(newTx)); - return costWithNewTx.compareTo(currentRepository.getBalance(newTx.getSender(signatureCache))) <= 0; + boolean senderCanPay = costWithNewTx.compareTo(currentRepository.getBalance(newTx.getSender(signatureCache))) <= 0; + + if(senderCanPay) { + return true; + } + + return ContractUtil.isClaimTxAndValid(newTx, currentRepository, costWithNewTx, config.getNetworkConstants(), signatureCache); } private Coin getTxBaseCost(Transaction tx) { diff --git a/rskj-core/src/main/java/co/rsk/util/ContractUtil.java b/rskj-core/src/main/java/co/rsk/util/ContractUtil.java new file mode 100644 index 00000000000..efcdc9cf7e8 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/util/ContractUtil.java @@ -0,0 +1,96 @@ +package co.rsk.util; + +import co.rsk.core.Coin; +import co.rsk.core.RskAddress; +import co.rsk.db.RepositorySnapshot; +import org.ethereum.config.Constants; +import org.ethereum.core.CallTransaction; +import org.ethereum.core.SignatureCache; +import org.ethereum.core.Transaction; +import org.ethereum.crypto.HashUtil; +import org.ethereum.solidity.SolidityType; +import org.ethereum.util.ByteUtil; +import org.ethereum.vm.DataWord; + +import java.math.BigInteger; +import java.util.Arrays; + +public class ContractUtil { + public static byte[] calculateSwapHash(Transaction newTx, SignatureCache signatureCache) { + CallTransaction.Invocation invocation = getTxInvocation(newTx); + + // Get arguments from the invocation object + byte[] preimage = (byte[]) invocation.args[0]; + // Encode preimage with sha256, this is to imitate the encoding done in teh claim function before calling hashValues + byte[] preimageHash = HashUtil.sha256(preimage); + BigInteger amount = (BigInteger) invocation.args[1]; + DataWord refundAddress = (DataWord) invocation.args[2]; + BigInteger timeLock = (BigInteger) invocation.args[3]; + + + // Remove the leading zeroes from the arguments bytes and merge them + byte[] argumentsBytes = encodePacked( + preimageHash, + amount, + newTx.getSender(signatureCache), + new RskAddress(ByteUtil.toHexString(refundAddress.getLast20Bytes())), + timeLock + ); + + return HashUtil.keccak256(argumentsBytes); + } + + public static byte[] encodePacked(Object...arguments) { + byte[] encodedArguments = new byte[]{}; + + for(Object arg: arguments) { + byte[] encodedArg = new byte[]{}; + if(arg instanceof byte[]) { + SolidityType bytes32Type = SolidityType.getType(SolidityType.BYTES32); + encodedArg = bytes32Type.encode(arg); + } else if(arg instanceof RskAddress) { + SolidityType addressType = SolidityType.getType(SolidityType.ADDRESS); + byte[] encodedAddress = addressType.encode(((RskAddress) arg).toHexString()); + encodedArg = org.bouncycastle.util.Arrays.copyOfRange(encodedAddress, 12, encodedAddress.length); + } else if(arg instanceof BigInteger) { + SolidityType uint256Type = SolidityType.getType(SolidityType.UINT); + encodedArg = uint256Type.encode(arg); + } + + encodedArguments = ByteUtil.merge(encodedArguments, encodedArg); + } + + return encodedArguments; + } + + public static CallTransaction.Invocation getTxInvocation(Transaction newTx) { + String abi = "[{\"constant\":false,\"inputs\":[{\"name\":\"preimage\",\"type\":\"bytes32\"}, {\"name\":\"amount\",\"type\":\"uint256\"}, {\"name\":\"address\",\"type\":\"address\"}, {\"name\":\"timelock\",\"type\":\"uint256\"}],\"name\":\"claim\",\"outputs\":[],\"payable\":false,\"type\":\"function\"}]"; + CallTransaction.Contract contract = new CallTransaction.Contract(abi); + return contract.parseInvocation(newTx.getData()); + } + + public static boolean isClaimTxAndValid(Transaction newTx, + RepositorySnapshot currentRepository, + Coin txCost, + Constants constants, + SignatureCache signatureCache) { + byte[] functionSelector = Arrays.copyOfRange(newTx.getData(), 0, 4); + if(newTx.getReceiveAddress().toHexString().equalsIgnoreCase(constants.getEtherSwapContractAddress()) + && Arrays.equals(functionSelector, Constants.CLAIM_FUNCTION_SIGNATURE)) { + + byte[] swapHash = calculateSwapHash(newTx, signatureCache); + DataWord swapRecord = currentRepository.getStorageValue(newTx.getReceiveAddress(), DataWord.valueOf(swapHash)); + + if(swapRecord != null){ + CallTransaction.Invocation invocation = getTxInvocation(newTx); + BigInteger amount = (BigInteger) invocation.args[1]; + Coin balanceWithClaim = currentRepository.getBalance(newTx.getSender(signatureCache)) + .add(Coin.valueOf(amount.longValue())); + + return txCost.compareTo(balanceWithClaim) <= 0; + } + } + + return false; + } +} diff --git a/rskj-core/src/main/java/org/ethereum/config/Constants.java b/rskj-core/src/main/java/org/ethereum/config/Constants.java index dd4fcab9bf3..fb5a7855eaa 100644 --- a/rskj-core/src/main/java/org/ethereum/config/Constants.java +++ b/rskj-core/src/main/java/org/ethereum/config/Constants.java @@ -25,6 +25,7 @@ import org.bouncycastle.util.encoders.Hex; import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.config.blockchain.upgrades.ConsensusRule; +import org.ethereum.core.CallTransaction; import java.math.BigDecimal; import java.math.BigInteger; @@ -45,6 +46,11 @@ public class Constants { private static final BigInteger SECP256K1N = new BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16); private static final BigInteger TRANSACTION_GAS_CAP = BigDecimal.valueOf(Math.pow(2, 60)).toBigInteger(); private static final BigInteger RSKIP156_DIF_BOUND_DIVISOR = BigInteger.valueOf(400); + public static final byte[] CLAIM_FUNCTION_SIGNATURE = CallTransaction.Function.fromSignature( + "claim", + new String[]{"bytes32", "uint256", "address", "uint256"}, + new String[]{} + ).encodeSignature(); private static final long DEFAULT_MAX_TIMESTAMPS_DIFF_IN_SECS = 5L * 60; // 5 mins private static final long TESTNET_MAX_TIMESTAMPS_DIFF_IN_SECS = 120L * 60; // 120 mins @@ -59,6 +65,7 @@ public class Constants { private final int newBlockMaxSecondsInTheFuture; public final BridgeConstants bridgeConstants; private final ActivationConfig activationConfig; + private final String etherSwapContractAddress; public Constants( byte chainId, @@ -70,7 +77,8 @@ public Constants( int newBlockMaxSecondsInTheFuture, BridgeConstants bridgeConstants, ActivationConfig activationConfig, - BlockDifficulty minimumDifficultyForRskip290) { + BlockDifficulty minimumDifficultyForRskip290, + String etherSwapContractAddress) { this.chainId = chainId; this.seedCowAccounts = seedCowAccounts; this.durationLimit = durationLimit; @@ -81,6 +89,7 @@ public Constants( this.bridgeConstants = bridgeConstants; this.activationConfig = activationConfig; this.minimumDifficultyForRskip290 = minimumDifficultyForRskip290; + this.etherSwapContractAddress = etherSwapContractAddress; } public Constants( @@ -92,7 +101,8 @@ public Constants( BigInteger difficultyBoundDivisor, int newBlockMaxSecondsInTheFuture, BridgeConstants bridgeConstants, - BlockDifficulty minimumDifficultyForRskip290) { + BlockDifficulty minimumDifficultyForRskip290, + String etherSwapContractAddress) { this(chainId, seedCowAccounts, durationLimit, @@ -102,7 +112,8 @@ public Constants( newBlockMaxSecondsInTheFuture, bridgeConstants, null, - minimumDifficultyForRskip290 + minimumDifficultyForRskip290, + etherSwapContractAddress ); } @@ -225,6 +236,10 @@ public static int getMaxBitcoinMergedMiningMerkleProofLength() { return 960; } + public String getEtherSwapContractAddress() { + return etherSwapContractAddress; + } + public static Constants mainnet() { return new Constants( MAINNET_CHAIN_ID, @@ -235,7 +250,8 @@ public static Constants mainnet() { BigInteger.valueOf(50), 60, BridgeMainNetConstants.getInstance(), - new BlockDifficulty(new BigInteger("550000000")) + new BlockDifficulty(new BigInteger("550000000")), + null ); } @@ -249,7 +265,8 @@ public static Constants devnetWithFederation(List federationPublicKeys BigInteger.valueOf(50), 540, new BridgeDevNetConstants(federationPublicKeys), - new BlockDifficulty(new BigInteger("550000000")) + new BlockDifficulty(new BigInteger("550000000")), + null ); } @@ -264,7 +281,8 @@ public static Constants testnet(ActivationConfig activationConfig) { 540, BridgeTestNetConstants.getInstance(), activationConfig, - new BlockDifficulty(new BigInteger("550000000")) + new BlockDifficulty(new BigInteger("550000000")), + null ); } @@ -278,7 +296,8 @@ public static Constants regtest() { BigInteger.valueOf(2048), 0, BridgeRegTestConstants.getInstance(), - new BlockDifficulty(new BigInteger("550000000")) + new BlockDifficulty(new BigInteger("550000000")), + "77045E71a7A2c50903d88e564cD72fab11e82051" ); } @@ -292,7 +311,8 @@ public static Constants regtestWithFederation(List genesisFederationPu BigInteger.valueOf(2048), 0, new BridgeRegTestConstants(genesisFederationPublicKeys), - new BlockDifficulty(new BigInteger("550000000")) + new BlockDifficulty(new BigInteger("550000000")), + null ); } } diff --git a/rskj-core/src/main/java/org/ethereum/core/TransactionExecutor.java b/rskj-core/src/main/java/org/ethereum/core/TransactionExecutor.java index b0883a63821..bad58151dac 100644 --- a/rskj-core/src/main/java/org/ethereum/core/TransactionExecutor.java +++ b/rskj-core/src/main/java/org/ethereum/core/TransactionExecutor.java @@ -26,6 +26,7 @@ import co.rsk.metrics.profilers.ProfilerFactory; import co.rsk.panic.PanicProcessor; import co.rsk.rpc.modules.trace.ProgramSubtrace; +import co.rsk.util.ContractUtil; import org.ethereum.config.Constants; import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.config.blockchain.upgrades.ConsensusRule; @@ -99,6 +100,7 @@ public class TransactionExecutor { private final SignatureCache signatureCache; private boolean localCall = false; + private boolean txWasPaid = false; public TransactionExecutor( Constants constants, ActivationConfig activationConfig, Transaction tx, int txindex, RskAddress coinbase, @@ -167,17 +169,11 @@ private boolean init() { } - Coin totalCost = tx.getValue(); - - if (basicTxCost > 0 ) { - // add gas cost only for priced transactions - Coin txGasCost = tx.getGasPrice().multiply(BigInteger.valueOf(txGasLimit)); - totalCost = totalCost.add(txGasCost); - } + Coin totalCost = getTxTotalCost(); Coin senderBalance = track.getBalance(tx.getSender(signatureCache)); - if (!isCovers(senderBalance, totalCost)) { + if (!isCovers(senderBalance, totalCost) && !ContractUtil.isClaimTxAndValid(tx, track, totalCost,constants, signatureCache)) { logger.warn("Not enough cash: Require: {}, Sender cash: {}, tx {}", totalCost, senderBalance, tx.getHash()); logger.warn("Transaction Data: {}", tx); @@ -271,6 +267,14 @@ private void execute() { track.increaseNonce(tx.getSender(signatureCache)); + Coin totalCost = getTxTotalCost(); + Coin senderBalance = track.getBalance(tx.getSender(signatureCache)); + + if(isCovers(senderBalance, totalCost)) { + payTransaction(); + txWasPaid = true; + } + long txGasLimit = GasCost.toGas(tx.getGasLimit()); Coin txGasCost = tx.getGasPrice().multiply(BigInteger.valueOf(txGasLimit)); track.addBalance(tx.getSender(signatureCache), txGasCost.negate()); @@ -502,6 +506,9 @@ public TransactionReceipt getReceipt() { } private void finalization() { + if(!txWasPaid) { + payTransaction(); + } // RSK if local call gas balances must not be changed if (localCall) { // there's no need to save any change @@ -682,4 +689,24 @@ public long getGasUsed() { } public Coin getPaidFees() { return paidFees; } + + private void payTransaction() { + long txGasLimit = GasCost.toGas(tx.getGasLimit()); + Coin txGasCost = tx.getGasPrice().multiply(BigInteger.valueOf(txGasLimit)); + track.addBalance(tx.getSender(signatureCache), txGasCost.negate()); + + logger.trace("Paying: txGasCost: [{}], gasPrice: [{}], gasLimit: [{}]", txGasCost, tx.getGasPrice(), txGasLimit); + } + + private Coin getTxTotalCost() { + long txGasLimit = GasCost.toGas(tx.getGasLimit()); + Coin totalCost = tx.getValue(); + + if (basicTxCost > 0 ) { + Coin txGasCost = tx.getGasPrice().multiply(BigInteger.valueOf(txGasLimit)); + totalCost = totalCost.add(txGasCost); + } + + return totalCost; + } } diff --git a/rskj-core/src/test/java/co/rsk/util/ContractUtilTest.java b/rskj-core/src/test/java/co/rsk/util/ContractUtilTest.java new file mode 100644 index 00000000000..462fd68014a --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/util/ContractUtilTest.java @@ -0,0 +1,83 @@ +package co.rsk.util; + +import co.rsk.core.Coin; +import co.rsk.core.RskAddress; +import co.rsk.db.RepositorySnapshot; +import org.bouncycastle.util.encoders.Hex; +import org.ethereum.config.Constants; +import org.ethereum.core.CallTransaction; +import org.ethereum.core.ReceivedTxSignatureCache; +import org.ethereum.core.SignatureCache; +import org.ethereum.core.Transaction; +import org.ethereum.util.ByteUtil; +import org.ethereum.vm.DataWord; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static org.junit.jupiter.api.Assertions.*; + +public class ContractUtilTest { + + private final CallTransaction.Function CLAIM_FUNCTION = CallTransaction.Function.fromSignature( + "claim", + new String[]{"bytes32", "uint256", "address", "uint256"}, + new String[]{} + ); + + private final Constants testConstans = Constants.regtest(); + + private final String EXPECTED_HASH = "0x3dc21e0a710489c951f29205f9961b2c311d48fdf5a35545469d7b43e88f7624"; + + @Test + public void testCalculateSwapHash() { + Transaction mockedTx = mockTx(); + + byte[] result = ContractUtil.calculateSwapHash(mockedTx, new ReceivedTxSignatureCache()); + + assertEquals(EXPECTED_HASH, HexUtils.toJsonHex(result)); + } + + @Test + public void testIsClaimTxAndValid() { + Transaction mockedTx = mockTx(); + Coin txCost = Coin.valueOf(5); + SignatureCache signatureCache = new ReceivedTxSignatureCache(); + RepositorySnapshot mockedRepository = mock(RepositorySnapshot.class); + when(mockedRepository.getStorageValue(mockedTx.getReceiveAddress(), DataWord.valueOf(HexUtils.stringHexToByteArray(EXPECTED_HASH)))) + .thenReturn(DataWord.valueOf(1)); + when(mockedRepository.getBalance(any(RskAddress.class))).thenReturn(Coin.valueOf(3)); + + boolean result = ContractUtil.isClaimTxAndValid(mockedTx, mockedRepository, txCost, testConstans, signatureCache); + + assertTrue(result); + } + + private Transaction mockTx() { + byte[] senderBytes = Hex.decode("0000000000000000000000000000000001000001"); + RskAddress claimAddress = new RskAddress(senderBytes); + + byte[] preimage = "preimage".getBytes(StandardCharsets.UTF_8); + byte[] callData = CLAIM_FUNCTION.encode(preimage, + 10, + "0000000000000000000000000000000001000002", + 1000000); + + Transaction mockedTx = mock(Transaction.class); + + when(mockedTx.getSender(any(SignatureCache.class))).thenReturn(claimAddress); + when(mockedTx.getNonce()).thenReturn(ByteUtil.cloneBytes(BigInteger.ZERO.toByteArray())); + when(mockedTx.getGasPrice()).thenReturn(Coin.valueOf(1)); + when(mockedTx.getGasLimit()).thenReturn(BigInteger.valueOf(1).toByteArray()); + when(mockedTx.getReceiveAddress()).thenReturn(new RskAddress(testConstans.getEtherSwapContractAddress())); + when(mockedTx.getData()).thenReturn(callData); + when(mockedTx.getValue()).thenReturn(Coin.ZERO); + + return mockedTx; + } +} diff --git a/rskj-core/src/test/resources/dsl/etherswap.txt b/rskj-core/src/test/resources/dsl/etherswap.txt new file mode 100644 index 00000000000..e826849670a --- /dev/null +++ b/rskj-core/src/test/resources/dsl/etherswap.txt @@ -0,0 +1,220 @@ +comment + +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TransferHelper.sol"; + +contract EtherSwap { + uint8 constant public version = 3; + + bytes32 immutable public DOMAIN_SEPARATOR; + bytes32 immutable public TYPEHASH_REFUND; + + mapping (bytes32 => bool) public swaps; + + event Lockup( + bytes32 indexed preimageHash, + uint amount, + address claimAddress, + address indexed refundAddress, + uint timelock + ); + + event Claim(bytes32 indexed preimageHash, bytes32 preimage); + event Refund(bytes32 indexed preimageHash); + + constructor() { + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("EtherSwap"), + keccak256("3"), + block.chainid, + address(this) + ) + ); + TYPEHASH_REFUND = keccak256( + "Refund(bytes32 preimageHash,uint256 amount,address claimAddress,uint256 timeout)" + ); + } + + function lock( + bytes32 preimageHash, + address claimAddress, + uint timelock + ) external payable { + lockEther(preimageHash, msg.value, claimAddress, timelock); + } + + function lockPrepayMinerfee( + bytes32 preimageHash, + address payable claimAddress, + uint timelock, + uint prepayAmount + ) external payable { + require(msg.value > prepayAmount, "EtherSwap: sent amount must be greater than the prepay amount"); + + lockEther(preimageHash, msg.value - prepayAmount, claimAddress, timelock); + + TransferHelper.transferEther(claimAddress, prepayAmount); + } + + function claim( + bytes32 preimage, + uint amount, + address refundAddress, + uint timelock + ) external { + claim(preimage, amount, msg.sender, refundAddress, timelock); + } + + function claim( + bytes32 preimage, + uint amount, + address claimAddress, + address refundAddress, + uint timelock + ) public { + bytes32 preimageHash = sha256(abi.encodePacked(preimage)); + + bytes32 hash = hashValues( + preimageHash, + amount, + claimAddress, + refundAddress, + timelock + ); + + checkSwapIsLocked(hash); + delete swaps[hash]; + + emit Claim(preimageHash, preimage); + + TransferHelper.transferEther(payable(claimAddress), amount); + } + + function refund( + bytes32 preimageHash, + uint amount, + address claimAddress, + uint timelock + ) external { + require(timelock <= block.number, "EtherSwap: swap has not timed out yet"); + refundInternal(preimageHash, amount, claimAddress, timelock); + } + + function refundCooperative( + bytes32 preimageHash, + uint amount, + address claimAddress, + uint timelock, + uint8 v, + bytes32 r, + bytes32 s + ) external { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + TYPEHASH_REFUND, + preimageHash, + amount, + claimAddress, + timelock + ) + ) + ) + ), + v, + r, + s + ); + require(recoveredAddress != address(0) && recoveredAddress == claimAddress, "EtherSwap: invalid signature"); + + refundInternal(preimageHash, amount, claimAddress, timelock); + } + + function hashValues( + bytes32 preimageHash, + uint amount, + address claimAddress, + address refundAddress, + uint timelock + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked( + preimageHash, + amount, + claimAddress, + refundAddress, + timelock + )); + } + + function lockEther(bytes32 preimageHash, uint amount, address claimAddress, uint timelock) private { + require(amount > 0, "EtherSwap: locked amount must not be zero"); + + bytes32 hash = hashValues( + preimageHash, + amount, + claimAddress, + msg.sender, + timelock + ); + + require(swaps[hash] == false, "EtherSwap: swap exists already"); + + swaps[hash] = true; + + emit Lockup(preimageHash, amount, claimAddress, msg.sender, timelock); + } + + function refundInternal(bytes32 preimageHash, uint amount, address claimAddress, uint timelock) private { + bytes32 hash = hashValues( + preimageHash, + amount, + claimAddress, + msg.sender, + timelock + ); + + checkSwapIsLocked(hash); + delete swaps[hash]; + + emit Refund(preimageHash); + + TransferHelper.transferEther(payable(msg.sender), amount); + } + + function checkSwapIsLocked(bytes32 hash) private view { + require(swaps[hash] == true, "EtherSwap: swap has no Ether locked in the contract"); + } +} + +end + +account_new acc1 20000000 + +# Deploy EtherSwap +transaction_build tx01 + sender acc1 + nonce 0 + receiverAddress 00 + value 0 + data 60c06040523480156200001157600080fd5b507f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f7f0f435efbd794bc89b372127f0e96dcc927b729642a259c9b4dcbab48b107caec7f2a80e1ef1d7842f27f2e6be0972bb708b9a135c38860dbe73c27c3486c34f4de46306040516020016200008d95949392919062000153565b60405160208183030381529060405280519060200120608081815250507f9e9b7110907661cf70aeef31f56954d657589e65e35455acf5737535f33a234160a08181525050620001b0565b6000819050919050565b620000ed81620000d8565b82525050565b6000819050919050565b6200010881620000f3565b82525050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006200013b826200010e565b9050919050565b6200014d816200012e565b82525050565b600060a0820190506200016a6000830188620000e2565b620001796020830187620000e2565b620001886040830186620000e2565b620001976060830185620000fd565b620001a6608083018462000142565b9695505050505050565b60805160a051611591620001e4600039600081816103ae01526105240152600081816102e0015261050301526115916000f3fe60806040526004361061009c5760003560e01c80638b2f8f82116100645780638b2f8f8214610158578063a9ab4d5b14610195578063c3c37fbc146101c0578063cd413efa146101e9578063eb84e7f214610212578063fe237d451461024f5761009c565b80630899146b146100a157806335cd4ccb146100bd5780633644e515146100e657806354fd4d50146101115780636fa4ae601461013c575b600080fd5b6100bb60048036038101906100b69190610a45565b610278565b005b3480156100c957600080fd5b506100e460048036038101906100df9190610a98565b610289565b005b3480156100f257600080fd5b506100fb6102de565b6040516101089190610b0e565b60405180910390f35b34801561011d57600080fd5b50610126610302565b6040516101339190610b45565b60405180910390f35b61015660048036038101906101519190610b9e565b610307565b005b34801561016457600080fd5b5061017f600480360381019061017a9190610c05565b610370565b60405161018c9190610b0e565b60405180910390f35b3480156101a157600080fd5b506101aa6103ac565b6040516101b79190610b0e565b60405180910390f35b3480156101cc57600080fd5b506101e760048036038101906101e29190610a98565b6103d0565b005b3480156101f557600080fd5b50610210600480360381019061020b9190610c05565b6103e3565b005b34801561021e57600080fd5b5061023960048036038101906102349190610c80565b6104dd565b6040516102469190610cc8565b60405180910390f35b34801561025b57600080fd5b5061027660048036038101906102719190610d0f565b6104fd565b005b610284833484846106a3565b505050565b438111156102cc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102c390610e34565b60405180910390fd5b6102d8848484846107e2565b50505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b600381565b803411610349576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161034090610ec6565b60405180910390fd5b6103608482346103599190610f15565b85856106a3565b61036a838261085c565b50505050565b6000858585858560405160200161038b959493929190610fd3565b60405160208183030381529060405280519060200120905095945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b6103dd84843385856103e3565b50505050565b60006002866040516020016103f89190611032565b60405160208183030381529060405260405161041491906110be565b602060405180830381855afa158015610431573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061045491906110ea565b905060006104658287878787610370565b90506104708161090d565b60008082815260200190815260200160002060006101000a81549060ff0219169055817f5664142af3dcfc3dc3de45a43f75c746bd1d8c11170a5037fdf98bdb35775137886040516104c29190610b0e565b60405180910390a26104d4858761085c565b50505050505050565b60006020528060005260406000206000915054906101000a900460ff1681565b600060017f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000008a8a8a8a60405160200161055b959493929190611135565b604051602081830303815290604052805190602001206040516020016105829291906111df565b60405160208183030381529060405280519060200120858585604051600081526020016040526040516105b89493929190611216565b6020604051602081039080840390855afa1580156105da573d6000803e3d6000fd5b505050602060405103519050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415801561064e57508573ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16145b61068d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610684906112a7565b60405180910390fd5b610699888888886107e2565b5050505050505050565b600083116106e6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106dd90611339565b60405180910390fd5b60006106f58585853386610370565b90506000151560008083815260200190815260200160002060009054906101000a900460ff1615151461075d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610754906113a5565b60405180910390fd5b600160008083815260200190815260200160002060006101000a81548160ff0219169083151502179055503373ffffffffffffffffffffffffffffffffffffffff16857f15b4b8206809535e547317cd5cedc86cff6e7d203551f93701786ddaf14fd9f98686866040516107d3939291906113c5565b60405180910390a35050505050565b60006107f18585853386610370565b90506107fc8161090d565b60008082815260200190815260200160002060006101000a81549060ff0219169055847f3fbd469ec3a5ce074f975f76ce27e727ba21c99176917b97ae2e713695582a1260405160405180910390a2610855338561085c565b5050505050565b60008273ffffffffffffffffffffffffffffffffffffffff168260405161088290611422565b60006040518083038185875af1925050503d80600081146108bf576040519150601f19603f3d011682016040523d82523d6000602084013e6108c4565b606091505b5050905080610908576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108ff906114a9565b60405180910390fd5b505050565b6001151560008083815260200190815260200160002060009054906101000a900460ff16151514610973576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161096a9061153b565b60405180910390fd5b50565b600080fd5b6000819050919050565b61098e8161097b565b811461099957600080fd5b50565b6000813590506109ab81610985565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006109dc826109b1565b9050919050565b6109ec816109d1565b81146109f757600080fd5b50565b600081359050610a09816109e3565b92915050565b6000819050919050565b610a2281610a0f565b8114610a2d57600080fd5b50565b600081359050610a3f81610a19565b92915050565b600080600060608486031215610a5e57610a5d610976565b5b6000610a6c8682870161099c565b9350506020610a7d868287016109fa565b9250506040610a8e86828701610a30565b9150509250925092565b60008060008060808587031215610ab257610ab1610976565b5b6000610ac08782880161099c565b9450506020610ad187828801610a30565b9350506040610ae2878288016109fa565b9250506060610af387828801610a30565b91505092959194509250565b610b088161097b565b82525050565b6000602082019050610b236000830184610aff565b92915050565b600060ff82169050919050565b610b3f81610b29565b82525050565b6000602082019050610b5a6000830184610b36565b92915050565b6000610b6b826109b1565b9050919050565b610b7b81610b60565b8114610b8657600080fd5b50565b600081359050610b9881610b72565b92915050565b60008060008060808587031215610bb857610bb7610976565b5b6000610bc68782880161099c565b9450506020610bd787828801610b89565b9350506040610be887828801610a30565b9250506060610bf987828801610a30565b91505092959194509250565b600080600080600060a08688031215610c2157610c20610976565b5b6000610c2f8882890161099c565b9550506020610c4088828901610a30565b9450506040610c51888289016109fa565b9350506060610c62888289016109fa565b9250506080610c7388828901610a30565b9150509295509295909350565b600060208284031215610c9657610c95610976565b5b6000610ca48482850161099c565b91505092915050565b60008115159050919050565b610cc281610cad565b82525050565b6000602082019050610cdd6000830184610cb9565b92915050565b610cec81610b29565b8114610cf757600080fd5b50565b600081359050610d0981610ce3565b92915050565b600080600080600080600060e0888a031215610d2e57610d2d610976565b5b6000610d3c8a828b0161099c565b9750506020610d4d8a828b01610a30565b9650506040610d5e8a828b016109fa565b9550506060610d6f8a828b01610a30565b9450506080610d808a828b01610cfa565b93505060a0610d918a828b0161099c565b92505060c0610da28a828b0161099c565b91505092959891949750929550565b600082825260208201905092915050565b7f4574686572537761703a207377617020686173206e6f742074696d6564206f7560008201527f7420796574000000000000000000000000000000000000000000000000000000602082015250565b6000610e1e602583610db1565b9150610e2982610dc2565b604082019050919050565b60006020820190508181036000830152610e4d81610e11565b9050919050565b7f4574686572537761703a2073656e7420616d6f756e74206d757374206265206760008201527f726561746572207468616e207468652070726570617920616d6f756e74000000602082015250565b6000610eb0603d83610db1565b9150610ebb82610e54565b604082019050919050565b60006020820190508181036000830152610edf81610ea3565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610f2082610a0f565b9150610f2b83610a0f565b9250828203905081811115610f4357610f42610ee6565b5b92915050565b6000819050919050565b610f64610f5f8261097b565b610f49565b82525050565b6000819050919050565b610f85610f8082610a0f565b610f6a565b82525050565b60008160601b9050919050565b6000610fa382610f8b565b9050919050565b6000610fb582610f98565b9050919050565b610fcd610fc8826109d1565b610faa565b82525050565b6000610fdf8288610f53565b602082019150610fef8287610f74565b602082019150610fff8286610fbc565b60148201915061100f8285610fbc565b60148201915061101f8284610f74565b6020820191508190509695505050505050565b600061103e8284610f53565b60208201915081905092915050565b600081519050919050565b600081905092915050565b60005b83811015611081578082015181840152602081019050611066565b60008484015250505050565b60006110988261104d565b6110a28185611058565b93506110b2818560208601611063565b80840191505092915050565b60006110ca828461108d565b915081905092915050565b6000815190506110e481610985565b92915050565b600060208284031215611100576110ff610976565b5b600061110e848285016110d5565b91505092915050565b61112081610a0f565b82525050565b61112f816109d1565b82525050565b600060a08201905061114a6000830188610aff565b6111576020830187610aff565b6111646040830186611117565b6111716060830185611126565b61117e6080830184611117565b9695505050505050565b600081905092915050565b7f1901000000000000000000000000000000000000000000000000000000000000600082015250565b60006111c9600283611188565b91506111d482611193565b600282019050919050565b60006111ea826111bc565b91506111f68285610f53565b6020820191506112068284610f53565b6020820191508190509392505050565b600060808201905061122b6000830187610aff565b6112386020830186610b36565b6112456040830185610aff565b6112526060830184610aff565b95945050505050565b7f4574686572537761703a20696e76616c6964207369676e617475726500000000600082015250565b6000611291601c83610db1565b915061129c8261125b565b602082019050919050565b600060208201905081810360008301526112c081611284565b9050919050565b7f4574686572537761703a206c6f636b656420616d6f756e74206d757374206e6f60008201527f74206265207a65726f0000000000000000000000000000000000000000000000602082015250565b6000611323602983610db1565b915061132e826112c7565b604082019050919050565b6000602082019050818103600083015261135281611316565b9050919050565b7f4574686572537761703a20737761702065786973747320616c72656164790000600082015250565b600061138f601e83610db1565b915061139a82611359565b602082019050919050565b600060208201905081810360008301526113be81611382565b9050919050565b60006060820190506113da6000830186611117565b6113e76020830185611126565b6113f46040830184611117565b949350505050565b50565b600061140c600083611058565b9150611417826113fc565b600082019050919050565b600061142d826113ff565b9150819050919050565b7f5472616e7366657248656c7065723a20636f756c64206e6f74207472616e736660008201527f6572204574686572000000000000000000000000000000000000000000000000602082015250565b6000611493602883610db1565b915061149e82611437565b604082019050919050565b600060208201905081810360008301526114c281611486565b9050919050565b7f4574686572537761703a207377617020686173206e6f204574686572206c6f6360008201527f6b656420696e2074686520636f6e747261637400000000000000000000000000602082015250565b6000611525603383610db1565b9150611530826114c9565b604082019050919050565b6000602082019050818103600083015261155481611518565b905091905056fea2646970667358221220521990c57fce8c6ba9b7c7cf550d50fb69f717ab41f4047528082b658e040ec464736f6c63430008180033 + gas 2000000 + build + +block_build b01 + parent g00 + transactions tx01 + build + +block_connect b01 + +# Assert best block +assert_best b01 \ No newline at end of file