mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
feat: draw cyberspace avatars, and only the ones that paid
Kind 11333 carries the same SNO payload in a replaceable container, so it now renders like any other object — except that §8.10 gates the drawing rather than the parsing: an avatar that has not paid for its reach and its detail, or whose content cannot be read, MUST NOT be drawn. An avatar is the one thing in cyberspace that lands on other people's screens whether they asked for it or not. SnoAvatarEvent extends BaseReplaceableEvent rather than plain Event, which is both what §8.10 means by moving the kind out of the addressable range and what LocalCache needs to key it. The payment check keeps both of §8.10's conditions separate, and the test pins the case a shortcut gets wrong: an avatar owing 16 bits, committed to 30 and mined to 20 reads as 20 through Event.pow(), which clears the 16 it owes while plainly falling short of the 30 its publisher committed to. Not wired: replacing a user's NIP-01 profile picture with their cyberspace avatar across the app. That is a product decision about every feed row, not plumbing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwXApJjoZYtkD3sPRWbPNa
This commit is contained in:
co-authored by
Claude Opus 5
parent
e792a2228b
commit
e18d176481
@@ -145,6 +145,7 @@ import com.vitorpamplona.quartz.buzz.wpWorkspaceProfile.SetWorkspaceProfileEvent
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChatEditEvent
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoAvatarEvent
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoObjectEvent
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
@@ -3806,6 +3807,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
is GeocacheCurationListEvent,
|
||||
is GeohashListEvent,
|
||||
is SnoObjectEvent,
|
||||
is SnoAvatarEvent,
|
||||
is GitRepositoryEvent,
|
||||
is GitRepositoryStateEvent,
|
||||
is UserGraspListEvent,
|
||||
|
||||
@@ -213,6 +213,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventConfirmation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventReport
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRootNappletEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSnoAvatar
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSnoObject
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareApplication
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareAsset
|
||||
@@ -258,6 +259,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent
|
||||
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoAvatarEvent
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoObjectEvent
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
@@ -1373,6 +1375,10 @@ private fun RenderNoteRow(
|
||||
RenderSnoObject(baseNote)
|
||||
}
|
||||
|
||||
is SnoAvatarEvent -> {
|
||||
RenderSnoAvatar(baseNote)
|
||||
}
|
||||
|
||||
is ChessGameEvent -> {
|
||||
RenderChessGame(
|
||||
baseNote,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.sno.ui.SnoObjectViewer
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.SnoAvatarCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.SnoAvatarUnpaidCard
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoAvatarEvent
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoPayload
|
||||
|
||||
private val VIEWER_HEIGHT = 360.dp
|
||||
|
||||
/**
|
||||
* Entry for a cyberspace avatar (kind 11333).
|
||||
*
|
||||
* §8.10 gates the drawing rather than the parsing: an avatar that has not paid
|
||||
* for its reach and its detail, or whose content cannot be read, MUST NOT be
|
||||
* drawn. Empty content is the default avatar and owes no work, so there is
|
||||
* nothing to show for it either.
|
||||
*/
|
||||
@Composable
|
||||
fun RenderSnoAvatar(baseNote: Note) {
|
||||
val noteEvent = baseNote.event as? SnoAvatarEvent ?: return
|
||||
if (noteEvent.isDefaultAvatar()) return
|
||||
|
||||
val shape: SnoPayload? = remember(noteEvent) { if (noteEvent.isPaid()) noteEvent.sno().payloadOrNull() else null }
|
||||
|
||||
if (shape == null) {
|
||||
SnoAvatarUnpaidCard()
|
||||
return
|
||||
}
|
||||
|
||||
var turning by remember(noteEvent) { mutableStateOf(false) }
|
||||
|
||||
SnoAvatarCard(
|
||||
payload = shape,
|
||||
eventId = noteEvent.id,
|
||||
name = noteEvent.nameTag(),
|
||||
onClick = { turning = true },
|
||||
)
|
||||
|
||||
if (turning) {
|
||||
Dialog(onDismissRequest = { turning = false }) {
|
||||
SnoObjectViewer(
|
||||
payload = shape,
|
||||
eventId = noteEvent.id,
|
||||
contentDescription = noteEvent.nameTag() ?: shape.name.ifBlank { null },
|
||||
modifier = Modifier.fillMaxWidth().height(VIEWER_HEIGHT),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -225,6 +225,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderRelayReview
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventConfirmation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventReport
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSnoAvatar
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSnoObject
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareApplication
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareAsset
|
||||
@@ -265,6 +266,7 @@ import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoAvatarEvent
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoObjectEvent
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
@@ -1068,6 +1070,8 @@ private fun FullBleedNoteCompose(
|
||||
RenderBirdDetection(baseNote)
|
||||
} else if (noteEvent is SnoObjectEvent) {
|
||||
RenderSnoObject(baseNote)
|
||||
} else if (noteEvent is SnoAvatarEvent) {
|
||||
RenderSnoAvatar(baseNote)
|
||||
} else if (noteEvent is Ps1SaveEvent) {
|
||||
RenderPs1Save(baseNote)
|
||||
} else if (noteEvent is GeocacheListingEvent) {
|
||||
|
||||
@@ -3084,4 +3084,6 @@
|
||||
<string name="sno_object_details">%1$d vertices · %2$d faces</string>
|
||||
<string name="sno_object_unreadable">This 3D object could not be read (rule %1$s)</string>
|
||||
<string name="sno_object_turn_hint">Drag to turn</string>
|
||||
<string name="sno_avatar_title">Cyberspace avatar</string>
|
||||
<string name="sno_avatar_unpaid">This avatar has not paid for its size, so it is not drawn</string>
|
||||
</resources>
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.sno_avatar_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.sno_avatar_unpaid
|
||||
import com.vitorpamplona.amethyst.commons.resources.sno_object_details
|
||||
import com.vitorpamplona.amethyst.commons.sno.ui.SnoThumbnail
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoPayload
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
private val AVATAR_THUMBNAIL_SIZE = 96.dp
|
||||
|
||||
/**
|
||||
* A cyberspace avatar (`CYBERSPACE_V2.md` §8.10 kind 11333), which is an SNO
|
||||
* payload in a replaceable container.
|
||||
*/
|
||||
@Composable
|
||||
fun SnoAvatarCard(
|
||||
payload: SnoPayload,
|
||||
eventId: String,
|
||||
name: String?,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp)
|
||||
|
||||
Row(
|
||||
modifier = if (onClick != null) modifier.clickable(onClick = onClick) else modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SnoThumbnail(
|
||||
payload = payload,
|
||||
eventId = eventId,
|
||||
size = AVATAR_THUMBNAIL_SIZE,
|
||||
contentDescription = name ?: payload.name.ifBlank { null },
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = name ?: payload.name.ifBlank { stringResource(Res.string.sno_avatar_title) },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.sno_object_details, payload.vertexCount, payload.faceCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What stands in for an avatar a client may not draw.
|
||||
*
|
||||
* `CYBERSPACE_V2.md` §8.10 is normative here: "a client MUST NOT draw an avatar
|
||||
* event that is not paid, or that carries content it cannot read". An avatar is
|
||||
* the one thing in cyberspace that lands on other people's screens whether they
|
||||
* asked for it or not, so its size and its detail are paid for in proof of work
|
||||
* on the event that publishes it, and a client draws nothing it cannot verify
|
||||
* has paid.
|
||||
*/
|
||||
@Composable
|
||||
fun SnoAvatarUnpaidCard() {
|
||||
Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.sno_avatar_unpaid),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.cyberspace.deck0003Sno
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator
|
||||
import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag
|
||||
@@ -32,7 +32,9 @@ import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag
|
||||
* default avatar.
|
||||
*
|
||||
* Replaceable, so relays keep the newest per `(pubkey, kind)` and an identity
|
||||
* has exactly one avatar. This kind **was** 33331 with a `d` fixed at
|
||||
* has exactly one avatar — which is why this extends [BaseReplaceableEvent]
|
||||
* rather than plain `Event`, giving it the fixed-empty-`d` address the local
|
||||
* store keys replaceables on. This kind **was** 33331 with a `d` fixed at
|
||||
* `"avatar"` before the spec moved it here, and 33331 was then handed to the
|
||||
* standalone objects of DECK-0003 §3.1 — so an old 33331 whose `d` is literally
|
||||
* `avatar` is not a surprise, merely stale.
|
||||
@@ -51,7 +53,7 @@ class SnoAvatarEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
/** True when this identity asked for the default avatar, which owes no work. */
|
||||
fun isDefaultAvatar(): Boolean = content.isBlank()
|
||||
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.cyberspace.deck0003Sno
|
||||
|
||||
import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* The drawing gate of `CYBERSPACE_V2.md` §8.10: "a client MUST NOT draw an
|
||||
* avatar event that is not paid, or that carries content it cannot read".
|
||||
*
|
||||
* Both conditions are required — the committed target must cover the work the
|
||||
* payload owes, AND the id must carry that many leading zero bits — and the
|
||||
* interesting case is the one where only the second fails.
|
||||
*/
|
||||
class SnoAvatarPaidTest {
|
||||
/** A shape that owes exactly the 16-bit floor. */
|
||||
private val floorShape =
|
||||
"""{"v":2,"name":"dot","unit":0,"mode":"points","vertices":[[0,0,0],[1,0,0]],"colors":[225,225],"faces":[]}"""
|
||||
|
||||
private fun idWithRank(bits: Int): String {
|
||||
val zeros = bits / 4
|
||||
val remainder = bits % 4
|
||||
val stopper =
|
||||
when (remainder) {
|
||||
0 -> "f"
|
||||
1 -> "4"
|
||||
2 -> "2"
|
||||
else -> "1"
|
||||
}
|
||||
val head = "0".repeat(zeros) + stopper
|
||||
return head + "f".repeat(64 - head.length)
|
||||
}
|
||||
|
||||
private fun avatar(
|
||||
content: String,
|
||||
committed: Int?,
|
||||
idBits: Int,
|
||||
) = SnoAvatarEvent(
|
||||
idWithRank(idBits),
|
||||
"11".repeat(32),
|
||||
0L,
|
||||
committed?.let { arrayOf(arrayOf("nonce", "1", it.toString())) } ?: emptyArray(),
|
||||
content,
|
||||
"22".repeat(64),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun theIdHelperProducesTheRankItClaims() {
|
||||
listOf(12, 16, 20, 30).forEach {
|
||||
assertEquals(it, PoWRankEvaluator.calculatePowRankOf(idWithRank(it)), "rank for $it bits")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theFloorShapeOwesSixteenBits() {
|
||||
assertEquals(16, SnoAvatarWork.required(SnoParser.parse(floorShape).payloadOrNull()!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anAvatarMinedToItsCommitmentIsPaid() {
|
||||
assertTrue(avatar(floorShape, committed = 16, idBits = 16).isPaid())
|
||||
assertTrue(avatar(floorShape, committed = 16, idBits = 24).isPaid(), "over-mining is fine")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyContentIsTheDefaultAvatarAndOwesNothing() {
|
||||
val default = avatar("", committed = null, idBits = 0)
|
||||
assertTrue(default.isDefaultAvatar())
|
||||
assertTrue(default.isPaid())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anAvatarWithoutANonceTagIsNotPaid() {
|
||||
// §8.10 requires the tag: committing the target before mining is what
|
||||
// stops a lucky id being claimed against a lower bar than it was mined
|
||||
// for, so an uncommitted avatar has not paid however long its id is.
|
||||
assertFalse(avatar(floorShape, committed = null, idBits = 32).isPaid())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anAvatarCommittingLessThanItOwesIsNotPaid() {
|
||||
assertFalse(avatar(floorShape, committed = 8, idBits = 32).isPaid())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anIdShortOfItsOwnCommitmentIsNotPaid() {
|
||||
// The case a naive check gets wrong. Event.pow() is
|
||||
// PoWRankEvaluator.compute(id, committed), which returns
|
||||
// min(actualRank, committed) — here min(20, 30) = 20, which clears the
|
||||
// 16 bits the shape owes while the id plainly falls short of the 30 the
|
||||
// publisher committed to.
|
||||
val event = avatar(floorShape, committed = 30, idBits = 20)
|
||||
|
||||
assertEquals(20, PoWRankEvaluator.compute(event.id, 30), "the shortcut would say 20...")
|
||||
assertTrue(20 >= SnoAvatarWork.required(event.sno().payloadOrNull()!!), "...which clears the required 16...")
|
||||
assertFalse(event.isPaid(), "...but §8.10 needs the id to carry the committed 30")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anAvatarWhoseContentCannotBeReadIsNotPaid() {
|
||||
assertFalse(avatar("not json at all", committed = 32, idBits = 32).isPaid())
|
||||
assertFalse(avatar("""{"v":9}""", committed = 32, idBits = 32).isPaid())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user