Skip to content

Commit

Permalink
Implement persistent Allow State (#125)
Browse files Browse the repository at this point in the history
* bump the proto version

* dump the updated rust bindings

* add the allow list implementation

* update the parsing

* write tests for it

* update the way the client is created

* update the topic to the new agreed upon topic name

* try to update docker compose

* fix up the unit tests

* it is decrypt not encrypt

* fix up the linter issues

* more fixes for contact

* more fixes for linter

* fix up test not running properly

* make it easier to check

* fix linter again
  • Loading branch information
nplasterer authored Oct 26, 2023
1 parent 9561552 commit ad4884d
Show file tree
Hide file tree
Showing 16 changed files with 539 additions and 65 deletions.
20 changes: 4 additions & 16 deletions dev/local/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "3.8"
services:
wakunode:
waku-node:
image: xmtp/node-go:latest
platform: linux/amd64
environment:
- GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67
command:
Expand All @@ -12,22 +12,10 @@ services:
- --api.authn.enable
ports:
- 9001:9001
- 5555:5555 # http message API
- 5556:5556 # grpc message API
- 5555:5555
depends_on:
- db
healthcheck:
test: [ "CMD", "lsof", "-i", ":5556" ]
interval: 3s
timeout: 10s
retries: 5
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: xmtp
js:
restart: always
depends_on:
wakunode:
condition: service_healthy
build: ./test
POSTGRES_PASSWORD: xmtp
8 changes: 7 additions & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ dokkaGfmPartial {
outputDirectory.set(file("build/docs/partial"))
}

ktlint {
filter {
exclude { it.file.path.contains("xmtp_dh") }
}
}

android {
namespace 'org.xmtp.android.library'
compileSdk 33
Expand Down Expand Up @@ -80,7 +86,7 @@ dependencies {
implementation 'org.web3j:crypto:5.0.0'
implementation "net.java.dev.jna:jna:5.13.0@aar"
api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
api 'org.xmtp:proto-kotlin:3.24.1'
api 'org.xmtp:proto-kotlin:3.31.0'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'app.cash.turbine:turbine:0.12.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package org.xmtp.android.library

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder
import org.xmtp.android.library.messages.generate
import org.xmtp.proto.message.contents.PrivateKeyOuterClass

@RunWith(AndroidJUnit4::class)
class ClientTest {
@Test
fun testTakesAWallet() {
Expand All @@ -20,6 +26,20 @@ class ClientTest {
assert(preKey?.publicKey?.hasSignature() ?: false)
}

@Test
fun testSerialization() {
val wallet = PrivateKeyBuilder()
val v1 =
PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet)
val encodedData = PrivateKeyBundleV1Builder.encodeData(v1)
val v1Copy = PrivateKeyBundleV1Builder.fromEncodedData(encodedData)
val client = Client().buildFrom(v1Copy)
assertEquals(
wallet.address,
client.address
)
}

@Test
fun testCanBeCreatedWithBundle() {
val fakeWallet = PrivateKeyBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.xmtp.android.library

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.xmtp.android.library.messages.walletAddress

@RunWith(AndroidJUnit4::class)
class ContactsTest {

@Test
Expand Down Expand Up @@ -35,4 +37,34 @@ class ContactsTest {
}
assert(fixtures.aliceClient.contacts.has(fixtures.bob.walletAddress))
}

@Test
fun testAllowAddress() {
val fixtures = fixtures()

val contacts = fixtures.bobClient.contacts
var result = contacts.isAllowed(fixtures.alice.walletAddress)

assert(!result)

contacts.allow(listOf(fixtures.alice.walletAddress))

result = contacts.isAllowed(fixtures.alice.walletAddress)
assert(result)
}

@Test
fun testBlockAddress() {
val fixtures = fixtures()

val contacts = fixtures.bobClient.contacts
var result = contacts.isAllowed(fixtures.alice.walletAddress)

assert(!result)

contacts.block(listOf(fixtures.alice.walletAddress))

result = contacts.isBlocked(fixtures.alice.walletAddress)
assert(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -713,4 +713,34 @@ class ConversationTest {
assertEquals(1, messages.size)
assertEquals("hi", messages[0].content())
}

@Test
fun testCanHaveAllowState() {
val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null)
val isAllowed = bobConversation.allowState() == AllowState.ALLOW

// Conversations you start should start as allowed
assertTrue(isAllowed)

val aliceConversation = aliceClient.conversations.list()[0]
val isUnknown = aliceConversation.allowState() == AllowState.UNKNOWN

// Conversations started with you should start as unknown
assertTrue(isUnknown)

aliceClient.contacts.allow(listOf(bob.walletAddress))

val isBobAllowed = aliceConversation.allowState() == AllowState.ALLOW
assertTrue(isBobAllowed)

val aliceClient2 = Client().create(aliceWallet, fakeApiClient)
val aliceConversation2 = aliceClient2.conversations.list()[0]

aliceClient2.contacts.refreshAllowList()

// Allow state should sync across clients
val isBobAllowed2 = aliceConversation2.allowState() == AllowState.ALLOW

assertTrue(isBobAllowed2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ class LocalInstrumentedTest {

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testStreamAllMessagesWorksWithIntros() = runBlocking {
fun testStreamAllMessagesWorksWithIntros() {
val bob = PrivateKeyBuilder()
val alice = PrivateKeyBuilder()
val clientOptions =
Expand Down
148 changes: 147 additions & 1 deletion library/src/main/java/org/xmtp/android/library/Contacts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,161 @@ package org.xmtp.android.library
import kotlinx.coroutines.runBlocking
import org.xmtp.android.library.messages.ContactBundle
import org.xmtp.android.library.messages.ContactBundleBuilder
import org.xmtp.android.library.messages.EnvelopeBuilder
import org.xmtp.android.library.messages.Topic
import org.xmtp.android.library.messages.walletAddress
import org.xmtp.proto.message.contents.PrivatePreferences.PrivatePreferencesAction
import java.util.Date

typealias MessageType = PrivatePreferencesAction.MessageTypeCase

enum class AllowState {
ALLOW,
BLOCK,
UNKNOWN
}
data class AllowListEntry(
val value: String,
val entryType: EntryType,
val permissionType: AllowState,
) {
enum class EntryType {
ADDRESS
}

companion object {
fun address(
address: String,
type: AllowState = AllowState.UNKNOWN,
): AllowListEntry {
return AllowListEntry(address, EntryType.ADDRESS, type)
}
}

val key: String
get() = "${entryType.name}-$value"
}

class AllowList(val client: Client) {
private val entries: MutableMap<String, AllowState> = mutableMapOf()
private val publicKey =
client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
private val privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes

@OptIn(ExperimentalUnsignedTypes::class)
private val identifier: String = uniffi.xmtp_dh.generatePrivatePreferencesTopicIdentifier(
privateKey.toByteArray().toUByteArray().toList()
)

@OptIn(ExperimentalUnsignedTypes::class)
suspend fun load(): AllowList {
val envelopes = client.query(Topic.preferenceList(identifier))
val allowList = AllowList(client)
val preferences: MutableList<PrivatePreferencesAction> = mutableListOf()

for (envelope in envelopes.envelopesList) {
val payload = uniffi.xmtp_dh.eciesDecryptK256Sha3256(
publicKey.toByteArray().toUByteArray().toList(),
privateKey.toByteArray().toUByteArray().toList(),
envelope.message.toByteArray().toUByteArray().toList()
)

preferences.add(
PrivatePreferencesAction.parseFrom(
payload.toUByteArray().toByteArray()
)
)
}

preferences.iterator().forEach { preference ->
preference.allow?.walletAddressesList?.forEach { address ->
allowList.allow(address)
}
preference.block?.walletAddressesList?.forEach { address ->
allowList.block(address)
}
}
return allowList
}

@OptIn(ExperimentalUnsignedTypes::class)
fun publish(entry: AllowListEntry) {
val payload = PrivatePreferencesAction.newBuilder().also {
when (entry.permissionType) {
AllowState.ALLOW -> it.setAllow(PrivatePreferencesAction.Allow.newBuilder().addWalletAddresses(entry.value))
AllowState.BLOCK -> it.setBlock(PrivatePreferencesAction.Block.newBuilder().addWalletAddresses(entry.value))
AllowState.UNKNOWN -> it.clearMessageType()
}
}.build()

val message = uniffi.xmtp_dh.eciesEncryptK256Sha3256(
publicKey.toByteArray().toUByteArray().toList(),
privateKey.toByteArray().toUByteArray().toList(),
payload.toByteArray().toUByteArray().toList()
)

val envelope = EnvelopeBuilder.buildFromTopic(
Topic.preferenceList(identifier),
Date(),
ByteArray(message.size) { message[it].toByte() }
)

client.publish(listOf(envelope))
}

fun allow(address: String): AllowListEntry {
entries[AllowListEntry.address(address).key] = AllowState.ALLOW

return AllowListEntry.address(address, AllowState.ALLOW)
}

fun block(address: String): AllowListEntry {
entries[AllowListEntry.address(address).key] = AllowState.BLOCK

return AllowListEntry.address(address, AllowState.BLOCK)
}

fun state(address: String): AllowState {
val state = entries[AllowListEntry.address(address).key]

return state ?: AllowState.UNKNOWN
}
}

data class Contacts(
var client: Client,
val knownBundles: MutableMap<String, ContactBundle> = mutableMapOf(),
val hasIntroduced: MutableMap<String, Boolean> = mutableMapOf()
val hasIntroduced: MutableMap<String, Boolean> = mutableMapOf(),
) {

var allowList: AllowList = AllowList(client)

fun refreshAllowList() {
runBlocking {
allowList = AllowList(client).load()
}
}

fun isAllowed(address: String): Boolean {
return allowList.state(address) == AllowState.ALLOW
}

fun isBlocked(address: String): Boolean {
return allowList.state(address) == AllowState.BLOCK
}

fun allow(addresses: List<String>) {
for (address in addresses) {
AllowList(client).publish(allowList.allow(address))
}
}

fun block(addresses: List<String>) {
for (address in addresses) {
AllowList(client).publish(allowList.block(address))
}
}

fun has(peerAddress: String): Boolean =
knownBundles[peerAddress] != null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ sealed class Conversation {
}
}

fun allowState(): AllowState {
val client: Client = when (this) {
is V1 -> conversationV1.client
is V2 -> conversationV2.client
}
return client.contacts.allowList.state(address = peerAddress)
}

fun toTopicData(): TopicData {
val data = TopicData.newBuilder()
.setCreatedNs(createdAt.time * 1_000_000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ data class Conversations(
invitation = invitation,
header = sealedInvitation.v1.header
)
client.contacts.allow(addresses = listOf(peerAddress))
val conversation = Conversation.V2(conversationV2)
conversationsByTopic[conversation.topic] = conversation
return conversation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sealed class Topic {
data class userInvite(val address: String?) : Topic()
data class directMessageV1(val address1: String?, val address2: String?) : Topic()
data class directMessageV2(val addresses: String?) : Topic()
data class groupInvite(val address: String?) : Topic()
data class preferenceList(val identifier: String?) : Topic()

val description: String
get() {
Expand All @@ -22,7 +22,7 @@ sealed class Topic {
wrap("dm-${addresses.joinToString(separator = "-")}")
}
is directMessageV2 -> wrap("m-$addresses")
is groupInvite -> wrap("groupInvite-$address")
is preferenceList -> wrap("pppp-$identifier")
}
}

Expand Down
Loading

0 comments on commit ad4884d

Please sign in to comment.