import { CeloTx } from '@celo/connect' ; import { ContractKit , newKitFromWeb3 } from '@celo/contractkit' ; import { privateKeyToAddress } from '@celo/utils/lib/address' ; import BigNumber from 'bignumber.js' ; import { assert } from 'chai' ; import ejsUtil from 'ethereumjs-util' ; import lodash from 'lodash' ; import Web3 from 'web3' ; import { AccountType , generatePrivateKey } from '../lib/generate_utils' ; import { GethRunConfig } from '../lib/interfaces/geth-run-config' ; import { getHooks , initAndSyncGethWithRetry , mnemonic , sleep } from './utils' ; const TMP_PATH = '/tmp/e2e' ; const validatorUrl = 'http://localhost:8545' ; const lightUrl = 'http://localhost:8546' ; const notYetActivatedError = 'support for eth-compatible transactions is not enabled' ; const notCompatibleError = 'ethCompatible is true, but non-eth-compatible fields are present' ; const noReplayProtectionError = 'only replay-protected (EIP-155) transactions allowed over RPC' ; const validatorPrivateKey = generatePrivateKey ( mnemonic , AccountType . VALIDATOR , 0 ) ; const validatorAddress = privateKeyToAddress ( validatorPrivateKey ) ; const toAddress = '0x8c36775E95A5f7FEf6894Ba658628352Ac58605B' ; const gatewayFeeRecipientAddress = '0xc77538d1e30C0e4ec44B0DcaD97FD3dc63fcaCC4' ; const bytecode = '0x608060405260008055348015601357600080fd5b5060358060216000396000f3006080604052600080fd00a165627a7a72305820c7f3f7c299940bb1d9b122d25e8f288817e45bbdeaccdd2f6e8801677ed934e70029' ; const verbose = false ; let replayProtectionTests = 'run' ; const devFilter = { cipIsActivated : undefined , lightNode : undefined , ethCompatible : undefined , contractCreation : undefined , useFeeCurrency : undefined , useGatewayFee : undefined , useGatewayFeeRecipient : undefined , sendRawTransaction : undefined , } ; type Filter = Partial < TestCase > ; interface TestCase { cipIsActivated : boolean ; lightNode : boolean ; ethCompatible : boolean ; contractCreation : boolean ; useFeeCurrency : boolean ; useGatewayFee : boolean ; useGatewayFeeRecipient : boolean ; sendRawTransaction : boolean ; errorString : string | null ; errorReason : string | null ; } function generateTestCases ( cipIsActivated ) { const cases = [ ] ; if ( devFilter . cipIsActivated !== undefined && devFilter . cipIsActivated !== cipIsActivated ) { return cases ; } const getValues = ( fieldFilter ) => { return fieldFilter === undefined ? [ false , true ] : [ fieldFilter ] ; } ; for ( const lightNode of getValues ( devFilter . lightNode ) ) { for ( const ethCompatible of getValues ( devFilter . ethCompatible ) ) { for ( const contractCreation of getValues ( devFilter . contractCreation ) ) { for ( const useFeeCurrency of getValues ( devFilter . useFeeCurrency ) ) { for ( const useGatewayFee of getValues ( devFilter . useGatewayFee ) ) { for ( const useGatewayFeeRecipient of getValues ( devFilter . useGatewayFeeRecipient ) ) { for ( const sendRawTransaction of getValues ( devFilter . sendRawTransaction ) ) { let errorString = null ; let errorReason = null ; const hasCeloFields = useFeeCurrency || useGatewayFee || useGatewayFeeRecipient ; if ( ethCompatible && hasCeloFields ) { errorString = notCompatibleError ; errorReason = 'transaction has celo-only fields' ; } else if ( ethCompatible && ! cipIsActivated ) { errorString = notYetActivatedError ; errorReason = 'Donut is not activated' ; } if ( sendRawTransaction && ethCompatible && hasCeloFields ) { continue ; } cases . push ( { cipIsActivated , lightNode , ethCompatible , contractCreation , useFeeCurrency , useGatewayFee , useGatewayFeeRecipient , sendRawTransaction , errorString , errorReason , } ) ; } } } } } } } return cases ; } function getGethRunConfig ( withDonut , withEspresso ) { console . log ( 'getGethRunConfig' , withDonut ) ; return { migrate : true , runPath : TMP_PATH , keepData : false , networkId : 1101 , network : 'local' , genesisConfig : { churritoBlock : 0 , donutBlock : withDonut ? 0 : null , espressoBlock : withEspresso ? 0 : null , } , instances : [ { name : 'validator' , validating : true , syncmode : 'full' , lightserv : true , port : 30303 , rpcport : 8545 , } , ] , } ; } param a return function stripZeros ( a ) { let first = a [ 0 ] ; while ( a . length > 0 && first . toString ( ) === '0' ) { a = a . slice ( 1 ) ; first = a [ 0 ] ; } return a ; } class TestEnv { testCases : TestCase [ ] ; gethConfig : GethRunConfig ; cipIsActivated : boolean ; replayProtectionIsNotMandatory : boolean ; hooks : ReturnType < typeof getHooks > ; stableTokenAddr : string = '' ; gasPrice : string = '' ; kit : ContractKit ; kitLight : ContractKit ; kitWithLocalWallet : ContractKit ; kitWithLocalWalletLight : ContractKit ; web3 : Web3 ; web3Light : Web3 ; constructor ( cipIsActivated , replayProtectionIsNotMandatory ) { this . gethConfig = getGethRunConfig ( cipIsActivated , replayProtectionIsNotMandatory ) ; this . hooks = getHooks ( this . gethConfig ) ; this . cipIsActivated = cipIsActivated ; this . replayProtectionIsNotMandatory = replayProtectionIsNotMandatory ; this . testCases = generateTestCases ( cipIsActivated ) ; this . kit = newKitFromWeb3 ( new Web3 ( validatorUrl ) ) ; this . kitLight = newKitFromWeb3 ( new Web3 ( lightUrl ) ) ; this . kitWithLocalWallet = newKitFromWeb3 ( new Web3 ( validatorUrl ) ) ; this . kitWithLocalWalletLight = newKitFromWeb3 ( new Web3 ( lightUrl ) ) ; this . web3 = new Web3 ( validatorUrl ) ; this . web3Light = new Web3 ( lightUrl ) ; } async before ( ) { await this . hooks . before ( ) ; await this . hooks . restart ( ) ; const lightNodeConfig = { name : 'light' , validating : false , syncmode : 'light' , port : 30305 , rpcport : 8546 , } ; await initAndSyncGethWithRetry ( this . gethConfig , this . hooks . gethBinaryPath , lightNodeConfig , [ ... this . gethConfig . instances , lightNodeConfig ] , verbose , 3 ) ; this . stableTokenAddr = ( await this . kit . contracts . getStableToken ( ) ) . address ; const gasPriceMinimum = await ( await this . kit . contracts . getGasPriceMinimum ( ) ) . gasPriceMinimum ( ) ; this . gasPrice = gasPriceMinimum . times ( 5 ) . toString ( ) ; await sleep ( 2 ) ; await this . kit . connection . web3 . eth . personal . unlockAccount ( validatorAddress , '' , 1000 ) ; await this . kitLight . connection . web3 . eth . personal . importRawKey ( validatorPrivateKey , '' ) ; await this . kitLight . connection . web3 . eth . personal . unlockAccount ( validatorAddress , '' , 1000 ) ; this . kitWithLocalWallet . connection . addAccount ( validatorPrivateKey ) ; this . kitWithLocalWalletLight . connection . addAccount ( validatorPrivateKey ) ; } async generateUnprotectedTransaction ( ethCompatible ) { const encode = ( ejsUtil as any ) . rlp . encode ; const numToHex = ( x ) => ejsUtil . bufferToHex ( ejsUtil . toBuffer ( x ) ) ; const nonce = await this . kit . connection . nonce ( validatorAddress ) ; const celoOnlyFields = ethCompatible ? [ ] : [ '0x' , '0x' , '0x' ] ; const arr = [ nonce > 0 ? numToHex ( nonce ) : '0x' , numToHex ( parseInt ( this . gasPrice , 10 ) ) , numToHex ( 1000000 ) , ... celoOnlyFields , toAddress , '0x05' , '0x' , ] ; const signingHash = ejsUtil . rlphash ( arr ) ; const pk = ejsUtil . addHexPrefix ( validatorPrivateKey ) ; const sig = ejsUtil . ecsign ( signingHash , ejsUtil . toBuffer ( pk ) ) ; arr . push ( ejsUtil . bufferToHex ( stripZeros ( sig . v ) ) , ejsUtil . bufferToHex ( stripZeros ( sig . r ) ) , ejsUtil . bufferToHex ( stripZeros ( sig . s ) ) ) ; return ejsUtil . bufferToHex ( encode ( arr ) ) ; } runReplayProtectionTests ( ) { for ( const ethCompatible of [ false , true ] ) { this . runReplayProtectionTest ( ethCompatible ) ; } } runReplayProtectionTest ( ethCompatible ) { describe ( `Transaction without replay protection, ethCompatible: ${ ethCompatible }` , ( ) => { let minedTx = null ; let error = null ; before ( async ( ) => { const tx = await this . generateUnprotectedTransaction ( ethCompatible ) ; try { const receipt = await ( await this . kit . connection . sendSignedTransaction ( tx ) ) . waitReceipt ( ) ; minedTx = await this . kit . web3 . eth . getTransaction ( receipt . transactionHash ) ; error = null ; } catch ( err ) { error = err . message ; } } ) ; if ( ethCompatible && ! this . cipIsActivated ) { it ( 'fails due to being ethereum-compatible' , ( ) => { assert . isNull ( minedTx , 'Transaction succeeded when it should have failed' ) ; assert . equal ( error ! , notYetActivatedError ) ; } ) ; } else if ( this . cipIsActivated ) { if ( this . replayProtectionIsNotMandatory ) { it ( 'succeeds' , ( ) => { assert . isNull ( error , 'Transaction failed when it should have succeeded' ) ; } ) ; } else { it ( 'fails due to replay protection being mandatory' , ( ) => { assert . isNull ( minedTx , 'Transaction succeeded when it should have failed' ) ; assert . equal ( error ! , noReplayProtectionError ) ; } ) ; } } else { it ( 'succeeds' , ( ) => { assert . isNull ( error , 'Transaction failed when it should have succeeded' ) ; assert . isFalse ( minedTx . ethCompatible , 'Transaction has wrong ethCompatible value' ) ; } ) ; } } ) ; } runTestCase ( testCase ) { const options = [ ] ; lodash . forEach ( testCase , ( value , key ) => { if ( value === true ) { options . push ( key ) ; } } ) ; describe ( `Testcase with: ${ options . join ( ', ' ) }` , ( ) => { let minedTx ; let error = null ; before ( async ( ) => { const tx = { from : validatorAddress , gas : 1000000 , gasPrice : this . gasPrice , chainId : this . gethConfig . networkId , nonce : await this . kit . connection . nonce ( validatorAddress ) , } ; if ( testCase . useFeeCurrency ) { tx . feeCurrency = this . stableTokenAddr ; } if ( testCase . useGatewayFee ) { tx . gatewayFee = '0x25' ; } if ( testCase . useGatewayFeeRecipient ) { tx . gatewayFeeRecipient = gatewayFeeRecipientAddress ; } if ( testCase . contractCreation ) { tx . data = bytecode ; } else { tx . to = await toAddress ; tx . value = 5 ; } try { let txHash ; const k = testCase . lightNode ? this . kitLight : this . kit ; const kLocal = testCase . lightNode ? this . kitWithLocalWalletLight : this . kitWithLocalWallet ; const w3 = testCase . lightNode ? this . web3Light : this . web3 ; if ( testCase . sendRawTransaction ) { let raw ; if ( testCase . ethCompatible ) { const signed = await w3 . eth . accounts . signTransaction ( tx , validatorPrivateKey ) ; raw = signed . rawTransaction ! ; } else { const signed = await kLocal . connection . wallet ! . signTransaction ( tx ) ; raw = signed . raw ; } txHash = ( await w3 . eth . sendSignedTransaction ( raw ) ) . transactionHash ; } else { tx . chainId = undefined ; const params = tx ; if ( testCase . ethCompatible ) { params . ethCompatible = true ; } const res = await k . sendTransaction ( params ) ; txHash = ( await res . waitReceipt ( ) ) . transactionHash ; } minedTx = await k . web3 . eth . getTransaction ( txHash ) ; error = null ; } catch ( err ) { error = err . message ; } } ) ; if ( testCase . errorString !== null ) { it ( `fails with the expected error (${ testCase . errorReason })` , ( ) => { assert . notEqual ( error , null , "Expected an error but didn't get one" ) ; assert . match ( error ! , new RegExp ( testCase . errorString ! , 'i' ) , `Got "${ error }", expected "${ testCase . errorString }"` ) ; } ) ; } else { it ( 'succeeds' , ( ) => { assert . equal ( error , null , 'Got an error but expected the transaction to succeed' ) ; } ) ; it ( `ethCompatible is ${ testCase . ethCompatible }` , ( ) => { assert . equal ( minedTx . ethCompatible , testCase . ethCompatible ) ; } ) ; } } ) ; } } describe ( 'CIP-35 >' , function ( this ) { this . timeout ( 0 ) ; describe ( 'before activation' , ( ) => { if ( devFilter . cipIsActivated === true ) { return ; } const testEnv = new TestEnv ( false , false ) ; before ( async function ( this ) { this . timeout ( 0 ) ; await testEnv . before ( ) ; } ) ; if ( replayProtectionTests !== 'only' ) { for ( const testCase of testEnv . testCases ) { testEnv . runTestCase ( testCase ) ; } } if ( replayProtectionTests !== 'skip' ) { testEnv . runReplayProtectionTests ( ) ; } after ( async function ( this ) { this . timeout ( 0 ) ; await testEnv . hooks . after ( ) ; } ) ; } ) ; describe ( 'after activation' , async ( ) => { if ( devFilter . cipIsActivated === false ) { return ; } const testEnv = new TestEnv ( true , false ) ; before ( async function ( this ) { this . timeout ( 0 ) ; await testEnv . before ( ) ; } ) ; if ( replayProtectionTests !== 'only' ) { for ( const testCase of testEnv . testCases ) { testEnv . runTestCase ( testCase ) ; } } if ( replayProtectionTests !== 'skip' ) { testEnv . runReplayProtectionTests ( ) ; } after ( async function ( this ) { this . timeout ( 0 ) ; await testEnv . hooks . after ( ) ; } ) ; } ) ; describe ( 'after cip50 (optional replay protection)' , async ( ) => { if ( devFilter . cipIsActivated === false ) { return ; } const testEnv = new TestEnv ( true , true ) ; before ( async function ( this ) { this . timeout ( 0 ) ; await testEnv . before ( ) ; } ) ; if ( replayProtectionTests !== 'only' ) { for ( const testCase of testEnv . testCases ) { testEnv . runTestCase ( testCase ) ; } } if ( replayProtectionTests !== 'skip' ) { testEnv . runReplayProtectionTests ( ) ; } after ( async function ( this ) { this . timeout ( 0 ) ; await testEnv . hooks . after ( ) ; } ) ; } ) ; } ) ;