diff --git a/app/build.gradle b/app/build.gradle index b60ec42e3..03d0f8f0d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,6 +126,7 @@ dependencies { implementation project(':atomic-swap') implementation project(':literaturedao') implementation project(':detoks') + implementation project(':detoks-engine') api(project(':common')) { exclude group: 'net.java.dev.jna' } diff --git a/app/src/main/java/nl/tudelft/trustchain/app/AppDefinition.kt b/app/src/main/java/nl/tudelft/trustchain/app/AppDefinition.kt index 1942e953c..1af9f9680 100644 --- a/app/src/main/java/nl/tudelft/trustchain/app/AppDefinition.kt +++ b/app/src/main/java/nl/tudelft/trustchain/app/AppDefinition.kt @@ -21,6 +21,7 @@ import nl.tudelft.trustchain.trader.ui.TrustChainTraderActivity import nl.tudelft.trustchain.valuetransfer.ValueTransferMainActivity import nl.tudelft.trustchain.voting.VotingActivity import nl.tudelft.trustchain.detoks.DeToksActivity +import nl.tudelft.trustchain.detoks_engine.DeToksEngineMainActivity enum class AppDefinition( @DrawableRes val icon: Int, @@ -36,6 +37,13 @@ enum class AppDefinition( DeToksActivity::class.java, true, ), + DETOKS_ENGINE( + R.drawable.ic_detox_logo, + "DeToksEngine", + R.color.purple, + DeToksEngineMainActivity::class.java, + true, + ), EIGHTEEN_PLUS( R.drawable.ic_18_plus, "18+", diff --git a/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt b/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt index a36465999..379b34df9 100644 --- a/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt +++ b/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt @@ -62,7 +62,10 @@ import nl.tudelft.trustchain.common.bitcoin.WalletService import nl.tudelft.trustchain.common.eurotoken.GatewayStore import nl.tudelft.trustchain.common.eurotoken.TransactionRepository import nl.tudelft.trustchain.currencyii.CoinCommunity +import nl.tudelft.trustchain.detoks_engine.TransactionCommunity import nl.tudelft.trustchain.detoks.DeToksCommunity +import nl.tudelft.trustchain.detoks_engine.trustchain.CommunityAdapter +import nl.tudelft.trustchain.detoks_engine.trustchain.TrustChainTransactionCommunity import nl.tudelft.trustchain.eurotoken.community.EuroTokenCommunity import nl.tudelft.trustchain.eurotoken.db.TrustStore import nl.tudelft.trustchain.gossipML.RecommenderCommunity @@ -107,6 +110,10 @@ class TrustChainApplication : Application() { createPeerChatCommunity(), createEuroTokenCommunity(), createTFTPCommunity(), + createIdentityCommunity(), + createDeToksCommunity(), + createTransactionCommunity(), + createTrustChainTransactionCommunity(), createDemoCommunity(), createWalletCommunity(), createAtomicSwapCommunity(), @@ -117,9 +124,7 @@ class TrustChainApplication : Application() { createMusicCommunity(), createLiteratureCommunity(), createRecommenderCommunity(), - createIdentityCommunity(), createFOCCommunity(), - createDeToksCommunity() ), walkerInterval = 5.0 ) @@ -283,6 +288,27 @@ class TrustChainApplication : Application() { ) } + private fun createTransactionCommunity(): OverlayConfiguration { + val randomWalk = RandomWalk.Factory() + return OverlayConfiguration( + Overlay.Factory(TransactionCommunity::class.java), + listOf(randomWalk) + ) + } + + private fun createTrustChainTransactionCommunity(): OverlayConfiguration { + val randomWalk = RandomWalk.Factory() + val blockTypesBcDisabled: Set = setOf(CommunityAdapter.TOKEN_BLOCK_TYPE) + val settings = TrustChainSettings(blockTypesBcDisabled) + val driver = AndroidSqliteDriver(Database.Schema, this, "trustchaindetoks.db") + val store = TrustChainSQLiteStore(Database(driver)) + + return OverlayConfiguration( + TrustChainTransactionCommunity.Factory(settings, store), + listOf(randomWalk) + ) + } + private fun createDiscoveryCommunity(): OverlayConfiguration { val randomWalk = RandomWalk.Factory() val randomChurn = RandomChurn.Factory() diff --git a/common-bitcoin/src/androidTest/java/com/example/common/bitcoin/.gitkeep b/common-bitcoin/src/androidTest/java/nl/tudelft/common/bitcoin/.gitkeep similarity index 100% rename from common-bitcoin/src/androidTest/java/com/example/common/bitcoin/.gitkeep rename to common-bitcoin/src/androidTest/java/nl/tudelft/common/bitcoin/.gitkeep diff --git a/common-bitcoin/src/test/java/com/example/common/bitcoin/.gitkeep b/common-bitcoin/src/test/java/nl/tudelft/common/bitcoin/.gitkeep similarity index 100% rename from common-bitcoin/src/test/java/com/example/common/bitcoin/.gitkeep rename to common-bitcoin/src/test/java/nl/tudelft/common/bitcoin/.gitkeep diff --git a/detoks-engine/.gitignore b/detoks-engine/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/detoks-engine/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/detoks-engine/README.md b/detoks-engine/README.md new file mode 100644 index 000000000..403128ee4 --- /dev/null +++ b/detoks-engine/README.md @@ -0,0 +1,141 @@ +# Blockchain Engineering 2023 - Token Transaction Engine II + +This folder contains our implemented and working token transaction engine. This engine allows for a fast and lightweight solution for fast token transactions based on the [Trustchain](https://github.com/Tribler/kotlin-ipv8/blob/master/doc/TrustChainCommunity.md). We make use of an [IPv8](https://github.com/Tribler/kotlin-ipv8) overlay on which we've built forward. The concept of Trustchain is one used for communicating and bookkeeping, to make sure the transactions are communicated properly, and that also the integrity can be maintained and ensured. + +## Content + +Our app has the following content: + +- **Token generation:** With our app it is possible to generate tokens on a large scale. The purpose of this is to create some form of currency to send to other peers currently active, you will also be able to see what tokens you have, and which ones you've recieved. To generate a token, simply use the button to generate one, then, select a peer in the current peers list, and then hit send to give them a token! If for some reason you don't like one of the tokens you've generated, you can select them and delete them too, or just delete them all. + +- **Visible peers:** We maintain a list of the peers currently in proximity. It's very easily possible to see what peers are currently around with their id's in a handy list. It's also possible to refresh, as other peers may have joined in as well during the usage. + +- **Sending tokens to peers:** It is possible to send many tokens at once to other peers in the vicinity, this can be done individually to a scale of a thousand tokens per second + +## The structure of the project + +The project and the code can be found in the current folder, the ```trustchain-superapp/detoks-engine``` folder. Our code is split up into multiple files, with each having their own specific role to play. Most of the related code to the transaction engine can be found in the TransactionCommunty file, where we handle tasks such as message grouping. In addition, we split up the code handling the SQLite database management and the token management code up as well. In ```db``` the tasks regarding the former are handled, while in the ```manage_tokens``` folder the latter is done. + +## Using the app + +Whilst first enabling our app in the trustchain-superapp, you'll be greeted with the entrance screen of the detoks engine. This screen has some video, and a button to take you to the sending tokens page. This app has a couple of important parts, which we will all discuss here to ensure proper app usage. Firstly, the numbers on the top show: + +- How many tokens you're currently in possesion of +- The percentage (%) of tokens recieved when sending +- The amount of tokens that are being sent per second, if applicable +- The latency between sending and recieving the tokens + +These statistics are an important part of knowing if your token may or may no have arrived, and also to know how well the performance of the app is, which may be key for benchmarking. In addition in the list below, you can see the fellow peers that you're able to send the tokens to if you wish so. These peers are selectable by ID. + +Lastly, the buttons on the bottom allow you to not only generate the tokens on a large scale (1000 or 100000), but also send them to a peer, this is done as follows: + +1) Ensure you have some tokens to send, use the generate token buttons at the bottom to do so +2) Next, select a peer from the list to send tokens to by tapping on them, you may need to go back one screen and enter again to force a refresh +3) Determine if you want to send many tokens at once or some per second, and press the button +4) Afterwards the statistics on the top of the screen will update accordingly + +## Design + +When opening the application, you see a page with two buttons. These buttons both bring you to a different page. Manage tokens, where you can send and generate tokens, and benchmarks, where you can also see some statistics. + + + +On the manage tokens page, you will find a list of all your tokens at the top, followed by a list of currently available peers. To create a new token, you can click on the "gen token" button located at the bottom of the page. Once you have accumulated enough tokens, you can send them to a selected peer by clicking on either the "send", "send 2", or "send 5" buttons, which will respectively send 1, 2, or 5 tokens. The application operates on the FIFO principle, meaning that the tokens generated first will be sent first. Lastly, you can refresh the page by clicking on the refresh button. + + + +The benchmarks page displays various statistics related to the application. At the top, you can view the number of tokens that you currently possess. Following that, you can find the percentage of sent packets that have successfully reached the designated peer. The throughput, measured in tokens per second, is displayed below the percentage. At the end of the statistics, you see the latency. After the statistics, a list of the currently available peers is displayed. At the bottom of the screen, you can find buttons specifically meant for generating or sending a large number of tokens. + + + +### Do you want to add your own app? + +- [Adding your own app to the TrustChain Super App](doc/AppTutorial.md) + +## Performance + +The statistics are all calculated for three different group sizes. These sizes are 1, 10 and 100. + +### Latency +Below, the average latency plotted over time is displayed. Here it is clear what the optimal group size is. The latency for groups of size 10 never rises above the latency of other group sizes. Groups of size 100 start with a latency that is better than the latency for groups of size 1 but rises above it for a while at the end. Finally, the latency for groups of size 1 starts high but spikes at the end. + + + +### Received packages +Below, you see the percentage of sent packets that have been received by a chosen peer over time. Depending on what you are looking for, there are two optimal sizes. The percentage of received packages for groups of size 1 gradually increases over time but only reaches 100% after significantly more time than the other sizes. For groups of size 100, the percentage of received packages does decrease for a while but reaches 100% the fastest out of all group sizes. + + + +### Throughput +Lastly, you see the throughput plotted over time. The optimal group size, in this case, is 100, as it results in a throughput that stays the highest out of all groups. Next is group size 10, and last is group size 1. + + + +### Flame chart +This flame chart shows the performance during sending 5000 tokens at a rate of 1000 tokens per second. + + +## Implementation + +The core of the application is the CommunityAdapter.kt file. Here all communication over the TrustChainCommunity is handled. The following functions are all public and can be used by anyone that wants to use our implementation: + +```sendTokens(amount: Int, peer: Peer)```: This function takes an integer and a peer as arguments. It then sends a number of tokens equal to that integer to the specified peer. + +```injectTokens(tokens: List)```: This function takes a list of tokens as an argument and adds a new token to it. In other words, this function handles token generation. + +```setReceiveTransactionHandler(handler: ((transaction: Transaction) -> Unit))```: This function takes a handler as an argument and makes sure that this handler gets called for every transaction that is received. + +```setReceiveAgreementHandler(handler: (transaction: Transaction) -> Unit)```: This function takes a handler as an argument and makes sure that this handler gets called for every agreement that is received. + +```getPeers()```: This function returns a list of all currently available peers that you can communicate with. + +```getTokens()```: This function returns a list of all your available tokens. + +```getTokenCount()```: Returns the amount of tokens available + +## Build + +If you want to build an APK, run the following command: + +``` +./gradlew :app:buildDebug +``` + +The resulting APK will be stored in `app/build/outputs/apk/debug/app-debug.apk`. + +## Install + +You can also build and automatically install the app on all connected Android devices with a single command: + +``` +./gradlew :app:installDebug +``` + +*Note: It is required to have an Android device connected with USB debugging enabled before running this command.* + +## Tests + +Run unit tests: +``` +./gradlew test +``` + +Run instrumented tests: +``` +./gradlew connectedAndroidTest +``` + +## Code style + +[Ktlint](https://ktlint.github.io/) is used to enforce a consistent code style across the whole project. + +Check code style: +``` +./gradlew ktlintCheck +``` + +Run code formatter: +``` +./gradlew ktlintFormat +``` + diff --git a/detoks-engine/Screenshot_20230417_182253.png b/detoks-engine/Screenshot_20230417_182253.png new file mode 100644 index 000000000..b725bc659 Binary files /dev/null and b/detoks-engine/Screenshot_20230417_182253.png differ diff --git a/detoks-engine/Screenshot_20230418_135848.png b/detoks-engine/Screenshot_20230418_135848.png new file mode 100644 index 000000000..627792563 Binary files /dev/null and b/detoks-engine/Screenshot_20230418_135848.png differ diff --git a/detoks-engine/Screenshot_20230418_145945.png b/detoks-engine/Screenshot_20230418_145945.png new file mode 100644 index 000000000..13f01054f Binary files /dev/null and b/detoks-engine/Screenshot_20230418_145945.png differ diff --git a/detoks-engine/Throughput.png b/detoks-engine/Throughput.png new file mode 100644 index 000000000..39783cb09 Binary files /dev/null and b/detoks-engine/Throughput.png differ diff --git a/detoks-engine/benchmark.gif b/detoks-engine/benchmark.gif new file mode 100644 index 000000000..fda82dd40 Binary files /dev/null and b/detoks-engine/benchmark.gif differ diff --git a/detoks-engine/build.gradle b/detoks-engine/build.gradle new file mode 100644 index 000000000..f55bd0ef9 --- /dev/null +++ b/detoks-engine/build.gradle @@ -0,0 +1,103 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'com.squareup.sqldelight' + +sqldelight { + Database { + packageName = "nl.tudelft.token_engine.sqldelight" + sourceFolders = ["sqldelight"] + schemaOutputDirectory = file("src/main/sqldelight/databases") + } +} + +android { + namespace 'nl.tudelft.trustchain.detoks_engine' + compileSdkVersion 33 + + defaultConfig { + minSdkVersion 22 + targetSdkVersion 33 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + allWarningsAsErrors = true + } + + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation project(':common') + + // AndroidX + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + implementation "androidx.fragment:fragment-ktx:$fragment_version" + implementation "androidx.preference:preference:1.2.0" + implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + + // Material + implementation 'com.google.android.material:material:1.8.0' + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + // Logging + implementation 'io.github.microutils:kotlin-logging:1.7.7' + implementation 'com.github.tony19:logback-android:2.0.0' + + implementation 'com.github.MattSkala:recyclerview-itemadapter:0.4' + + // Testing + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + testImplementation "io.mockk:mockk:1.9.3" + + // TODO fix direct import, this should rely on common. + // BitTorrent + implementation files('../common/libs/jlibtorrent-' + jlibtorrent_version + '.jar') + implementation files('../common/libs/jlibtorrent-android-arm64-' + jlibtorrent_version + '.jar') + implementation files('../common/libs/jlibtorrent-android-arm-' + jlibtorrent_version + '.jar') + implementation files('../common/libs/jlibtorrent-android-x86-' + jlibtorrent_version + '.jar') + implementation files('../common/libs/jlibtorrent-android-x86_64-' + jlibtorrent_version + '.jar') + + implementation 'com.devbrackets.android:exomedia:4.3.0' +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions.freeCompilerArgs += [ + "-opt-in=kotlin.RequiresOptIn", "-Werror" + ] +} diff --git a/detoks-engine/consumer-rules.pro b/detoks-engine/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/detoks-engine/flamechart.jpeg b/detoks-engine/flamechart.jpeg new file mode 100644 index 000000000..c863fba77 Binary files /dev/null and b/detoks-engine/flamechart.jpeg differ diff --git a/detoks-engine/latency.png b/detoks-engine/latency.png new file mode 100644 index 000000000..7bc2ed7cd Binary files /dev/null and b/detoks-engine/latency.png differ diff --git a/detoks-engine/packetsreceived.png b/detoks-engine/packetsreceived.png new file mode 100644 index 000000000..ba0f14bfe Binary files /dev/null and b/detoks-engine/packetsreceived.png differ diff --git a/detoks-engine/proguard-rules.pro b/detoks-engine/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/detoks-engine/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/detoks-engine/src/androidTest/java/nl/tudelft/detoks_engine/ExampleInstrumentedTest.kt b/detoks-engine/src/androidTest/java/nl/tudelft/detoks_engine/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..0c702c033 --- /dev/null +++ b/detoks-engine/src/androidTest/java/nl/tudelft/detoks_engine/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package nl.tudelft.detoks_engine + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.detoks_engine.test", appContext.packageName) + } +} diff --git a/detoks-engine/src/main/AndroidManifest.xml b/detoks-engine/src/main/AndroidManifest.xml new file mode 100644 index 000000000..639a42aee --- /dev/null +++ b/detoks-engine/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/detoks-engine/src/main/java/nl/tudelft/trustchain/detoks_engine/DeToksEngineFragment.kt b/detoks-engine/src/main/java/nl/tudelft/trustchain/detoks_engine/DeToksEngineFragment.kt new file mode 100644 index 000000000..16ab29a5f --- /dev/null +++ b/detoks-engine/src/main/java/nl/tudelft/trustchain/detoks_engine/DeToksEngineFragment.kt @@ -0,0 +1,126 @@ +package nl.tudelft.trustchain.detoks_engine + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.ViewPager2 +import kotlinx.android.synthetic.main.fragment_detoks2.* +import kotlinx.coroutines.launch +import mu.KotlinLogging +import nl.tudelft.ipv8.android.IPv8Android +import nl.tudelft.trustchain.common.ui.BaseFragment +import nl.tudelft.trustchain.detoks_engine.manage_tokens.TokenBenchmarkActivity +import nl.tudelft.trustchain.detoks_engine.manage_tokens.TokenManageActivity +import java.io.File +import java.io.FileOutputStream +import java.util.* + +class DeToksEngineFragment : BaseFragment(R.layout.fragment_detoks2) { + private lateinit var torrentManager: TorrentManager + private lateinit var transactionCommunity: TransactionCommunity + private val logger = KotlinLogging.logger {} + private var previousVideoAdapterIndex = 0 + private val torrentDir: String + get() = "${requireActivity().cacheDir.absolutePath}/torrent" + private val mediaCacheDir: String + get() = "${requireActivity().cacheDir.absolutePath}/media" + + private fun cacheDefaultTorrent() { + try { + val dir1 = File(mediaCacheDir) + if (!dir1.exists()) { + dir1.mkdirs() + } + val dir2 = File(torrentDir) + if (!dir2.exists()) { + dir2.mkdirs() + } + val file = File("$torrentDir/$DEFAULT_TORRENT_FILE") + if (!file.exists()) { + val outputStream = FileOutputStream(file) + val ins = requireActivity().resources.openRawResource(R.raw.detoks) + outputStream.write(ins.readBytes()) + ins.close() + outputStream.close() + } + } catch (e: Exception) { + Log.e("DeToks", "Failed to cache default torrent: $e") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + cacheDefaultTorrent() + + torrentManager = TorrentManager( + File("${requireActivity().cacheDir.absolutePath}/media"), + File("${requireActivity().cacheDir.absolutePath}/torrent"), + DEFAULT_CACHING_AMOUNT + ) + transactionCommunity = IPv8Android.getInstance().getOverlay()!! + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewPagerVideos.adapter = VideosAdapter(torrentManager) + viewPagerVideos.currentItem = 0 + + val button = view.findViewById