content: package org.aion.precompiled.contracts.ATB;
  
  import static com.google.common.truth.Truth.assertThat;
  import static org.aion.precompiled.contracts.ATB.BridgeTestUtils.dummyContext;
  
  import java.math.BigInteger;
  import java.security.SecureRandom;
  import java.util.List;
  import java.util.Random;
  import org.aion.precompiled.ExternalCapabilitiesForTesting;
  import org.aion.precompiled.RepositoryForPrecompiled;
  import org.aion.precompiled.type.CapabilitiesProvider;
  import org.aion.precompiled.PrecompiledUtilities;
  import org.aion.precompiled.ExternalStateForTests;
  import org.aion.precompiled.type.PrecompiledTransactionContext;
  import org.aion.types.AionAddress;
  import org.aion.types.Log;
  import org.junit.AfterClass;
  import org.junit.Before;
  import org.junit.BeforeClass;
  import org.junit.Test;
  
  public class BridgeTransferTest {
  
      private RepositoryForPrecompiled repo;
      private BridgeController controller;
      private PrecompiledTransactionContext context;
  
      private static byte[][] members = new byte[5][32];
  
      private static ExternalCapabilitiesForTesting capabilities;
  
      private static AionAddress CONTRACT_ADDR;
      private static AionAddress OWNER_ADDR;
  
      @BeforeClass
      public static void setupCapabilities() {
          capabilities = new ExternalCapabilitiesForTesting();
          CapabilitiesProvider.installExternalCapabilities(capabilities);
          CONTRACT_ADDR = new AionAddress(capabilities.blake2b("contractAddress".getBytes()));
          OWNER_ADDR = new AionAddress(capabilities.blake2b("ownerAddress".getBytes()));
  
          Random r = new Random();
          for (int i = 0; i < members.length; i++) {
              byte[] addr = new byte[32];
              r.nextBytes(addr);
              members[i] = addr;
          }
      }
  
      @AfterClass
      public static void teardownCapabilities() {
          CapabilitiesProvider.removeExternalCapabilities();
      }
  
      @Before
      public void beforeEach() {
          repo = new RepositoryForPrecompiled();
          resetContext();
      }
  
      private void resetContext() {
          this.context = dummyContext();
          TokenBridgeContract contract = new TokenBridgeContract(context, ExternalStateForTests.usingRepository(repo), OWNER_ADDR, CONTRACT_ADDR);
          this.controller = contract.getController();
          this.controller.initialize();
  
          this.controller.ringInitialize(OWNER_ADDR.toByteArray(), members);
      }
  
      @Test
      public void testBridgeTransferOne() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] sourceTransactionHash = capabilities.blake2b("transaction".getBytes());
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransactionHash".getBytes());
  
          // ensure we have enough balance
          this.repo.addBalance(CONTRACT_ADDR, BigInteger.ONE);
  
          BridgeTransfer bundle =
                  BridgeTransfer.getInstance(BigInteger.valueOf(1), recipient, sourceTransactionHash);
          byte[] bundleHash =
                  BridgeUtilities.computeBundleHash(blockHash, new BridgeTransfer[] {bundle});
  
          byte[][] signatures = new byte[members.length][];
          for (int i = 0; i < members.length; i++) {
              signatures[i] = capabilities.sign(members[i], bundleHash);
          }
  
          ErrCode code = this.controller.setRelayer(OWNER_ADDR.toByteArray(), senderAddress);
          assertThat(code).isEqualTo(ErrCode.NO_ERROR);
  
          BridgeController.ProcessedResults results =
                  this.controller.processBundles(
                          senderAddress,
                          aionTransactionHash,
                          blockHash,
                          new BridgeTransfer[] {bundle},
                          signatures);
          assertThat(results).isNotNull();
          assertThat(results.controllerResult).isEqualTo(ErrCode.NO_ERROR);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ZERO);
      }
  
      @Test
      public void testBridgeNotEnoughSignatures() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] sourceTransactionHash = capabilities.blake2b("transaction".getBytes());
  
          // ATB-4, this is the hash of the aion transaction which submitted
          // this bundle, this is different from the source transaction hash
          // which is transferred from another blockchain network.
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransaction".getBytes());
  
          // ensure we have enough balance
          this.repo.addBalance(CONTRACT_ADDR, BigInteger.ONE);
  
          BridgeTransfer bundle =
                  BridgeTransfer.getInstance(BigInteger.valueOf(1), recipient, sourceTransactionHash);
          byte[] bundleHash =
                  BridgeUtilities.computeBundleHash(blockHash, new BridgeTransfer[] {bundle});
  
          byte[][] signatures = new byte[2][];
          for (int i = 0; i < 2; i++) {
              signatures[i] = capabilities.sign(members[i], bundleHash);
          }
  
          ErrCode code = this.controller.setRelayer(OWNER_ADDR.toByteArray(), senderAddress);
          assertThat(code).isEqualTo(ErrCode.NO_ERROR);
  
          BridgeController.ProcessedResults results =
                  this.controller.processBundles(
                          senderAddress,
                          aionTransactionHash,
                          blockHash,
                          new BridgeTransfer[] {bundle},
                          signatures);
  
          assertThat(results).isNotNull();
          assertThat(results.controllerResult).isEqualTo(ErrCode.INVALID_SIGNATURE_BOUNDS);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ONE);
          assertThat(this.repo.getBalance(new AionAddress(recipient))).isEqualTo(BigInteger.ZERO);
      }
  
      @Test
      public void testLowerBoundSignature() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] sourceTransactionHash = capabilities.blake2b("transaction".getBytes());
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransaction".getBytes());
  
          // ensure we have enough balance
          this.repo.addBalance(CONTRACT_ADDR, BigInteger.ONE);
  
          BridgeTransfer bundle =
                  BridgeTransfer.getInstance(BigInteger.valueOf(1), recipient, sourceTransactionHash);
          byte[] bundleHash =
                  BridgeUtilities.computeBundleHash(blockHash, new BridgeTransfer[] {bundle});
  
          byte[][] signatures = new byte[3][];
          for (int i = 0; i < 3; i++) {
              signatures[i] = capabilities.sign(members[i], bundleHash);
          }
  
          ErrCode code = this.controller.setRelayer(OWNER_ADDR.toByteArray(), senderAddress);
          assertThat(code).isEqualTo(ErrCode.NO_ERROR);
  
          BridgeController.ProcessedResults results =
                  this.controller.processBundles(
                          senderAddress,
                          aionTransactionHash,
                          blockHash,
                          new BridgeTransfer[] {bundle},
                          signatures);
  
          assertThat(results).isNotNull();
          assertThat(results.controllerResult).isEqualTo(ErrCode.NO_ERROR);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ZERO);
          assertThat(this.repo.getBalance(new AionAddress(recipient))).isEqualTo(BigInteger.ONE);
      }
  
      @Test
      public void testBeyondUpperBoundSignatures() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] sourceTransactionHash = capabilities.blake2b("transaction".getBytes());
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransaction".getBytes());
  
          // ensure we have enough balance
          this.repo.addBalance(CONTRACT_ADDR, BigInteger.ONE);
  
          BridgeTransfer bundle =
                  BridgeTransfer.getInstance(BigInteger.valueOf(1), recipient, sourceTransactionHash);
          byte[] bundleHash =
                  BridgeUtilities.computeBundleHash(blockHash, new BridgeTransfer[] {bundle});
  
          byte[][] signatures = new byte[6][];
          for (int i = 0; i < 5; i++) {
              signatures[i] = capabilities.sign(members[i], bundleHash);
          }
  
          ErrCode code = this.controller.setRelayer(OWNER_ADDR.toByteArray(), senderAddress);
          assertThat(code).isEqualTo(ErrCode.NO_ERROR);
  
          BridgeController.ProcessedResults results =
                  this.controller.processBundles(
                          senderAddress,
                          aionTransactionHash,
                          blockHash,
                          new BridgeTransfer[] {bundle},
                          signatures);
  
          assertThat(results).isNotNull();
          assertThat(results.controllerResult).isEqualTo(ErrCode.INVALID_SIGNATURE_BOUNDS);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ONE);
          assertThat(this.repo.getBalance(new AionAddress(recipient))).isEqualTo(BigInteger.ZERO);
      }
  
      @Test
      public void testTransferZeroBundle() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] sourceTransactionHash = capabilities.blake2b("transaction".getBytes());
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransaction".getBytes());
  
          this.repo.addBalance(CONTRACT_ADDR, BigInteger.ONE);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ONE);
          BridgeTransfer bundle =
                  BridgeTransfer.getInstance(BigInteger.ZERO, recipient, sourceTransactionHash);
          byte[] bundleHash =
                  BridgeUtilities.computeBundleHash(blockHash, new BridgeTransfer[] {bundle});
  
          byte[][] signatures = new byte[5][];
          for (int i = 0; i < 5; i++) {
              signatures[i] = capabilities.sign(members[i], bundleHash);
          }
  
          ErrCode code = this.controller.setRelayer(OWNER_ADDR.toByteArray(), senderAddress);
          assertThat(code).isEqualTo(ErrCode.NO_ERROR);
  
          BridgeController.ProcessedResults results =
                  this.controller.processBundles(
                          senderAddress,
                          aionTransactionHash,
                          blockHash,
                          new BridgeTransfer[] {bundle},
                          signatures);
  
          assertThat(results).isNotNull();
          assertThat(results.controllerResult).isEqualTo(ErrCode.INVALID_TRANSFER);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ONE);
          assertThat(this.repo.getBalance(new AionAddress(recipient))).isEqualTo(BigInteger.ZERO);
      }
  
      static class ResultHashTuple {
          byte[] bundleHash;
          BridgeController.ProcessedResults results;
      }
  
      private ResultHashTuple executeSignController(
              byte[] senderAddress,
              byte[] blockHash,
              byte[] aionTransactionHash,
              BridgeTransfer[] transfers) {
  
          resetContext();
          byte[] bundleHash = BridgeUtilities.computeBundleHash(blockHash, transfers);
          byte[][] signatures = new byte[members.length][];
          for (int i = 0; i < members.length; i++) {
              signatures[i] = capabilities.sign(members[i], bundleHash);
          }
  
          ErrCode code = this.controller.setRelayer(OWNER_ADDR.toByteArray(), senderAddress);
          assertThat(code).isEqualTo(ErrCode.NO_ERROR);
  
          BridgeController.ProcessedResults results =
                  this.controller.processBundles(
                          senderAddress, aionTransactionHash, blockHash, transfers, signatures);
          ResultHashTuple tuple = new ResultHashTuple();
          tuple.bundleHash = bundleHash;
          tuple.results = results;
          return tuple;
      }
  
      @Test
      public void testDoubleBundleSend() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] sourceTransactionHash = capabilities.blake2b("transaction".getBytes());
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransactionHash".getBytes());
  
          // ensure we have enough balance
          this.repo.addBalance(CONTRACT_ADDR, BigInteger.TWO);
  
          BridgeTransfer transfer =
                  BridgeTransfer.getInstance(BigInteger.ONE, recipient, sourceTransactionHash);
          executeSignController(
                  senderAddress, blockHash, aionTransactionHash, new BridgeTransfer[] {transfer});
  
          final byte[] secondAionTransactionHash =
                  capabilities.blake2b("secondAionTransactionHash".getBytes());
  
          // try second time, should still be succesful
          ResultHashTuple tuple =
                  executeSignController(
                          senderAddress,
                          blockHash,
                          secondAionTransactionHash,
                          new BridgeTransfer[] {transfer});
  
          // check status of result
          assertThat(tuple.results.controllerResult).isEqualTo(ErrCode.NO_ERROR);
          assertThat(this.context.getLogs().size()).isEqualTo(1);
          assertThat(this.context.getLogs().get(0).copyOfTopics().get(0))
                  .isEqualTo(BridgeEventSig.SUCCESSFUL_TXHASH.getHashed());
          assertThat(this.context.getLogs().get(0).copyOfTopics().get(1))
                  .isEqualTo(aionTransactionHash);
  
          // one transfer should have gone through, second shouldn't
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ONE);
          assertThat(this.repo.getBalance(new AionAddress(recipient))).isEqualTo(BigInteger.ONE);
      }
  
      // Transfer 511 times, ONE per transfer
      // Also thoroughly checks event signatures
      @Test
      public void testBundleMultipleTransferSameRecipient() {
          final byte[] senderAddress = members[0];
          final byte[] blockHash = capabilities.blake2b("blockHash".getBytes());
          final byte[] recipient = capabilities.blake2b("recipient".getBytes());
          final byte[] aionTransactionHash = capabilities.blake2b("aionTransactionHash".getBytes());
  
          int transferTotal = 511;
          final BigInteger transferTotalBigInteger = BigInteger.valueOf(transferTotal);
  
          this.repo.addBalance(CONTRACT_ADDR, transferTotalBigInteger);
  
          BridgeTransfer[] transfers = new BridgeTransfer[transferTotal];
          for (int i = 0; i < transferTotal; i++) {
              byte[] randomBytes = new byte[32];
              new SecureRandom().nextBytes(randomBytes);
              transfers[i] = BridgeTransfer.getInstance(BigInteger.ONE, recipient, randomBytes);
          }
  
          ResultHashTuple tuple =
                  executeSignController(senderAddress, blockHash, aionTransactionHash, transfers);
  
          assertThat(tuple.results.controllerResult).isEqualTo(ErrCode.NO_ERROR);
          assertThat(this.repo.getBalance(CONTRACT_ADDR)).isEqualTo(BigInteger.ZERO);
          assertThat(this.repo.getBalance(new AionAddress(recipient)))
                  .isEqualTo(transferTotalBigInteger);
  
          // 511 transfer events + 1 distributed event
          assertThat(this.context.getLogs().size()).isEqualTo(512);
  
          List<Log> logs = this.context.getLogs();
          for (int i = 0; i < 511; i++) {
              List<byte[]> topics = logs.get(i).copyOfTopics();
              assertThat(topics.get(0)).isEqualTo(BridgeEventSig.DISTRIBUTED.getHashed());
              assertThat(topics.get(1)).isEqualTo(transfers[i].getSourceTransactionHash());
              assertThat(topics.get(2)).isEqualTo(transfers[i].getRecipient());
              assertThat(topics.get(3))
                      .isEqualTo(
                              PrecompiledUtilities.pad(transfers[i].getTransferValueByteArray(), 32));
          }
  
          // for the last element
          {
              List<byte[]> topics = logs.get(511).copyOfTopics();
              assertThat(topics.get(0)).isEqualTo(BridgeEventSig.PROCESSED_BUNDLE.getHashed());
              assertThat(topics.get(1)).isEqualTo(blockHash);
              assertThat(topics.get(2)).isEqualTo(tuple.bundleHash);
          }
      }
  }
  