From f4c050dea4e67644988163a219ab7d9661d8ea4f Mon Sep 17 00:00:00 2001 From: Cryptoloveer Date: Mon, 14 Sep 2026 10:51:43 +0200 Subject: [PATCH 1/5] feat: include configured account heads in discovery broadcasts --- .../discovery/DiscoveryProperties.kt | 2 ++ .../bootstrap/discovery/LastDiscoverer.kt | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/cash/atto/node/bootstrap/discovery/DiscoveryProperties.kt b/src/main/kotlin/cash/atto/node/bootstrap/discovery/DiscoveryProperties.kt index c7f9f7d7..6bbd3ce9 100644 --- a/src/main/kotlin/cash/atto/node/bootstrap/discovery/DiscoveryProperties.kt +++ b/src/main/kotlin/cash/atto/node/bootstrap/discovery/DiscoveryProperties.kt @@ -1,5 +1,6 @@ package cash.atto.node.bootstrap.discovery +import cash.atto.commons.AttoAddress import jakarta.annotation.PostConstruct import kotlinx.coroutines.channels.Channel import org.springframework.boot.context.properties.ConfigurationProperties @@ -12,6 +13,7 @@ class DiscoveryProperties { var headroom: Int = 2_000 var batchSize: Int = 1_000 var persistenceTargetPerSecond: Long = 1_000 + var hintedAddresses: Set = emptySet() @PostConstruct fun validate() { diff --git a/src/main/kotlin/cash/atto/node/bootstrap/discovery/LastDiscoverer.kt b/src/main/kotlin/cash/atto/node/bootstrap/discovery/LastDiscoverer.kt index eee22c60..ed93d2d5 100644 --- a/src/main/kotlin/cash/atto/node/bootstrap/discovery/LastDiscoverer.kt +++ b/src/main/kotlin/cash/atto/node/bootstrap/discovery/LastDiscoverer.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -48,6 +48,7 @@ import java.util.concurrent.TimeUnit @Component class LastDiscoverer( private val thisNode: AttoNode, + private val discoveryProperties: DiscoveryProperties, private val accountRepository: AccountRepository, private val transactionRepository: TransactionRepository, private val uncheckedTransactionRepository: UncheckedTransactionRepository, @@ -102,12 +103,21 @@ class LastDiscoverer( return } - val transactions = transactionRepository.getLastSample(10) + val transactions = transactionRepository.getLastSample(RANDOM_SAMPLE_SIZE).toList().toMutableList() + for (address in discoveryProperties.hintedAddresses) { + val account = accountRepository.findById(address.publicKey) + if (account == null || account.algorithm != address.algorithm) { + continue + } + + transactionRepository.findById(account.lastTransactionHash)?.let(transactions::add) + } transactions + .distinctBy { it.hash } .map { AttoBootstrapTransactionPush(it.toAttoTransaction()) } .map { BroadcastNetworkMessage(BroadcastStrategy.EVERYONE, setOf(), it) } - .collect { nodeConnectionManager.send(it) } + .forEach { nodeConnectionManager.send(it) } } } @@ -221,6 +231,10 @@ class LastDiscoverer( private fun startElection(transaction: Transaction) { eventPublisher.publish(TransactionReceived(transaction)) } + + private companion object { + const val RANDOM_SAMPLE_SIZE = 10L + } } private data class PendingHeadAdmission( From e87b9a791b2f0bc4f4e28979a32b9c975afea077 Mon Sep 17 00:00:00 2001 From: Cryptoloveer Date: Mon, 14 Sep 2026 10:54:59 +0200 Subject: [PATCH 2/5] test: update LastDiscoverer fixture for discovery properties --- .../atto/node/bootstrap/discovery/LastDiscovererTest.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt index 5514496b..d92a498f 100644 --- a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt +++ b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt @@ -55,17 +55,14 @@ class LastDiscovererTest { @Test fun `capacity prevents a new head election and vote request`() = runTest { - // Given val fixture = fixture(initiallyAtCapacity = true) val transaction = transaction(1) val response = fixture.voteResponse(transaction) try { - // When fixture.discoverer.processPush(fixture.push(transaction)) fixture.discoverer.processVoteResponse(response) - // Then assertTrue(fixture.messages.none { it.payload is AttoVoteStreamRequest }) coVerify(exactly = 0) { fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) @@ -78,7 +75,6 @@ class LastDiscovererTest { @Test fun `consensus at capacity retains the head election when admission is cancelled`() = runTest { - // Given val fixture = fixture(initiallyAtCapacity = false) val transaction = transaction(2) val response = fixture.voteResponse(transaction) @@ -91,14 +87,12 @@ class LastDiscovererTest { } try { - // When val admission = launch { fixture.discoverer.processVoteResponse(response) } runCurrent() - // Then assertFalse(admission.isCompleted) assertTrue(fixture.atCapacity.get()) coVerify(exactly = 1) { @@ -109,14 +103,12 @@ class LastDiscovererTest { } assertEquals(0, fixture.messages.count { it.payload is AttoVoteStreamCancel }) - // When admission.cancelAndJoin() coEvery { fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) } returns true fixture.discoverer.processVoteResponse(response) - // Then coVerify(exactly = 2) { fixture.discoveryQueue.queue( match { it.transaction.hash == transaction.hash }, @@ -153,6 +145,7 @@ class LastDiscovererTest { val discoverer = LastDiscoverer( thisNode = mockk(relaxed = true), + discoveryProperties = DiscoveryProperties(), accountRepository = accountRepository, transactionRepository = mockk(), uncheckedTransactionRepository = mockk(), From 66b9b927cae40c8c768700d6d82773ddbcedeb3c Mon Sep 17 00:00:00 2001 From: Cryptoloveer Date: Mon, 14 Sep 2026 10:55:59 +0200 Subject: [PATCH 3/5] test: preserve existing discovery test structure --- .../bootstrap/discovery/LastDiscovererTest.kt | 93 ++++++------------- 1 file changed, 30 insertions(+), 63 deletions(-) diff --git a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt index d92a498f..a017b9eb 100644 --- a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt +++ b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt @@ -55,14 +55,17 @@ class LastDiscovererTest { @Test fun `capacity prevents a new head election and vote request`() = runTest { + // Given val fixture = fixture(initiallyAtCapacity = true) val transaction = transaction(1) val response = fixture.voteResponse(transaction) try { + // When fixture.discoverer.processPush(fixture.push(transaction)) fixture.discoverer.processVoteResponse(response) + // Then assertTrue(fixture.messages.none { it.payload is AttoVoteStreamRequest }) coVerify(exactly = 0) { fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) @@ -75,6 +78,7 @@ class LastDiscovererTest { @Test fun `consensus at capacity retains the head election when admission is cancelled`() = runTest { + // Given val fixture = fixture(initiallyAtCapacity = false) val transaction = transaction(2) val response = fixture.voteResponse(transaction) @@ -87,12 +91,11 @@ class LastDiscovererTest { } try { - val admission = - launch { - fixture.discoverer.processVoteResponse(response) - } + // When + val admission = launch { fixture.discoverer.processVoteResponse(response) } runCurrent() + // Then assertFalse(admission.isCompleted) assertTrue(fixture.atCapacity.get()) coVerify(exactly = 1) { @@ -103,12 +106,12 @@ class LastDiscovererTest { } assertEquals(0, fixture.messages.count { it.payload is AttoVoteStreamCancel }) + // When admission.cancelAndJoin() - coEvery { - fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) - } returns true + coEvery { fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) } returns true fixture.discoverer.processVoteResponse(response) + // Then coVerify(exactly = 2) { fixture.discoveryQueue.queue( match { it.transaction.hash == transaction.hash }, @@ -135,9 +138,7 @@ class LastDiscovererTest { val voteConverter = mockk() val messages = mutableListOf>() val networkMessagePublisher = mockk() - every { networkMessagePublisher.publish(any()) } answers { - messages += firstArg>() - } + every { networkMessagePublisher.publish(any()) } answers { messages += firstArg>() } val accountRepository = mockk() coEvery { accountRepository.findById(any()) } returns null @@ -157,13 +158,7 @@ class LastDiscovererTest { voteWeighter = voteWeighter, ) - return Fixture( - discoverer = discoverer, - discoveryQueue = discoveryQueue, - voteConverter = voteConverter, - messages = messages, - atCapacity = atCapacity, - ) + return Fixture(discoverer, discoveryQueue, voteConverter, messages, atCapacity) } private data class Fixture( @@ -177,56 +172,28 @@ class LastDiscovererTest { private val socketAddress = InetSocketAddress("127.0.0.1", 8080) fun push(transaction: AttoTransaction): InboundNetworkMessage = - InboundNetworkMessage( - source = MessageSource.WEBSOCKET, - publicUri = publicUri, - socketAddress = socketAddress, - payload = AttoBootstrapTransactionPush(transaction), - ) + InboundNetworkMessage(MessageSource.WEBSOCKET, publicUri, socketAddress, AttoBootstrapTransactionPush(transaction)) fun voteResponse(transaction: AttoTransaction): InboundNetworkMessage { - val attoVote = - AttoVote( - version = 0U.toAttoVersion(), - algorithm = AttoAlgorithm.V1, - publicKey = AttoPublicKey(ByteArray(32) { 10 }), - blockAlgorithm = AttoAlgorithm.V1, - blockHash = transaction.hash, - timestamp = AttoVote.finalTimestamp, - ) - val signedVote = - AttoSignedVote( - vote = attoVote, - signature = AttoSignature(ByteArray(64) { 11 }), - ) - every { voteConverter.convert(signedVote) } returns - Vote.from(ElectionVoter.MIN_WEIGHT, signedVote) - - return InboundNetworkMessage( - source = MessageSource.WEBSOCKET, - publicUri = publicUri, - socketAddress = socketAddress, - payload = AttoVoteStreamResponse(signedVote), + val attoVote = AttoVote( + version = 0U.toAttoVersion(), algorithm = AttoAlgorithm.V1, + publicKey = AttoPublicKey(ByteArray(32) { 10 }), blockAlgorithm = AttoAlgorithm.V1, + blockHash = transaction.hash, timestamp = AttoVote.finalTimestamp, ) + val signedVote = AttoSignedVote(attoVote, AttoSignature(ByteArray(64) { 11 })) + every { voteConverter.convert(signedVote) } returns Vote.from(ElectionVoter.MIN_WEIGHT, signedVote) + return InboundNetworkMessage(MessageSource.WEBSOCKET, publicUri, socketAddress, AttoVoteStreamResponse(signedVote)) } } - private fun transaction(marker: Byte): AttoTransaction = - AttoTransaction( - block = - AttoReceiveBlock( - version = 0U.toAttoVersion(), - network = AttoNetwork.LOCAL, - algorithm = AttoAlgorithm.V1, - publicKey = AttoPublicKey(ByteArray(32) { marker }), - height = 2U.toAttoHeight(), - balance = AttoAmount.MAX, - timestamp = AttoInstant.now(), - previous = AttoHash(ByteArray(32) { (marker + 1).toByte() }), - sendHashAlgorithm = AttoAlgorithm.V1, - sendHash = AttoHash(ByteArray(32) { (marker + 2).toByte() }), - ), - signature = AttoSignature(ByteArray(64) { (marker + 3).toByte() }), - work = AttoWork(ByteArray(8) { (marker + 4).toByte() }), - ) + private fun transaction(marker: Byte): AttoTransaction = AttoTransaction( + block = AttoReceiveBlock( + version = 0U.toAttoVersion(), network = AttoNetwork.LOCAL, algorithm = AttoAlgorithm.V1, + publicKey = AttoPublicKey(ByteArray(32) { marker }), height = 2U.toAttoHeight(), balance = AttoAmount.MAX, + timestamp = AttoInstant.now(), previous = AttoHash(ByteArray(32) { (marker + 1).toByte() }), + sendHashAlgorithm = AttoAlgorithm.V1, sendHash = AttoHash(ByteArray(32) { (marker + 2).toByte() }), + ), + signature = AttoSignature(ByteArray(64) { (marker + 3).toByte() }), + work = AttoWork(ByteArray(8) { (marker + 4).toByte() }), + ) } From b445dbbf841722d174f2b0c2ccb24f85a2b8c2e8 Mon Sep 17 00:00:00 2001 From: Cryptoloveer Date: Mon, 14 Sep 2026 10:56:32 +0200 Subject: [PATCH 4/5] test: keep address hinting fixture change focused --- .../bootstrap/discovery/LastDiscovererTest.kt | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt index a017b9eb..6b5f2287 100644 --- a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt +++ b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererTest.kt @@ -92,7 +92,10 @@ class LastDiscovererTest { try { // When - val admission = launch { fixture.discoverer.processVoteResponse(response) } + val admission = + launch { + fixture.discoverer.processVoteResponse(response) + } runCurrent() // Then @@ -108,7 +111,9 @@ class LastDiscovererTest { // When admission.cancelAndJoin() - coEvery { fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) } returns true + coEvery { + fixture.discoveryQueue.queue(any(), DiscoverySource.HEAD) + } returns true fixture.discoverer.processVoteResponse(response) // Then @@ -138,7 +143,9 @@ class LastDiscovererTest { val voteConverter = mockk() val messages = mutableListOf>() val networkMessagePublisher = mockk() - every { networkMessagePublisher.publish(any()) } answers { messages += firstArg>() } + every { networkMessagePublisher.publish(any()) } answers { + messages += firstArg>() + } val accountRepository = mockk() coEvery { accountRepository.findById(any()) } returns null @@ -158,7 +165,13 @@ class LastDiscovererTest { voteWeighter = voteWeighter, ) - return Fixture(discoverer, discoveryQueue, voteConverter, messages, atCapacity) + return Fixture( + discoverer = discoverer, + discoveryQueue = discoveryQueue, + voteConverter = voteConverter, + messages = messages, + atCapacity = atCapacity, + ) } private data class Fixture( @@ -172,28 +185,56 @@ class LastDiscovererTest { private val socketAddress = InetSocketAddress("127.0.0.1", 8080) fun push(transaction: AttoTransaction): InboundNetworkMessage = - InboundNetworkMessage(MessageSource.WEBSOCKET, publicUri, socketAddress, AttoBootstrapTransactionPush(transaction)) + InboundNetworkMessage( + source = MessageSource.WEBSOCKET, + publicUri = publicUri, + socketAddress = socketAddress, + payload = AttoBootstrapTransactionPush(transaction), + ) fun voteResponse(transaction: AttoTransaction): InboundNetworkMessage { - val attoVote = AttoVote( - version = 0U.toAttoVersion(), algorithm = AttoAlgorithm.V1, - publicKey = AttoPublicKey(ByteArray(32) { 10 }), blockAlgorithm = AttoAlgorithm.V1, - blockHash = transaction.hash, timestamp = AttoVote.finalTimestamp, + val attoVote = + AttoVote( + version = 0U.toAttoVersion(), + algorithm = AttoAlgorithm.V1, + publicKey = AttoPublicKey(ByteArray(32) { 10 }), + blockAlgorithm = AttoAlgorithm.V1, + blockHash = transaction.hash, + timestamp = AttoVote.finalTimestamp, + ) + val signedVote = + AttoSignedVote( + vote = attoVote, + signature = AttoSignature(ByteArray(64) { 11 }), + ) + every { voteConverter.convert(signedVote) } returns + Vote.from(ElectionVoter.MIN_WEIGHT, signedVote) + + return InboundNetworkMessage( + source = MessageSource.WEBSOCKET, + publicUri = publicUri, + socketAddress = socketAddress, + payload = AttoVoteStreamResponse(signedVote), ) - val signedVote = AttoSignedVote(attoVote, AttoSignature(ByteArray(64) { 11 })) - every { voteConverter.convert(signedVote) } returns Vote.from(ElectionVoter.MIN_WEIGHT, signedVote) - return InboundNetworkMessage(MessageSource.WEBSOCKET, publicUri, socketAddress, AttoVoteStreamResponse(signedVote)) } } - private fun transaction(marker: Byte): AttoTransaction = AttoTransaction( - block = AttoReceiveBlock( - version = 0U.toAttoVersion(), network = AttoNetwork.LOCAL, algorithm = AttoAlgorithm.V1, - publicKey = AttoPublicKey(ByteArray(32) { marker }), height = 2U.toAttoHeight(), balance = AttoAmount.MAX, - timestamp = AttoInstant.now(), previous = AttoHash(ByteArray(32) { (marker + 1).toByte() }), - sendHashAlgorithm = AttoAlgorithm.V1, sendHash = AttoHash(ByteArray(32) { (marker + 2).toByte() }), - ), - signature = AttoSignature(ByteArray(64) { (marker + 3).toByte() }), - work = AttoWork(ByteArray(8) { (marker + 4).toByte() }), - ) + private fun transaction(marker: Byte): AttoTransaction = + AttoTransaction( + block = + AttoReceiveBlock( + version = 0U.toAttoVersion(), + network = AttoNetwork.LOCAL, + algorithm = AttoAlgorithm.V1, + publicKey = AttoPublicKey(ByteArray(32) { marker }), + height = 2U.toAttoHeight(), + balance = AttoAmount.MAX, + timestamp = AttoInstant.now(), + previous = AttoHash(ByteArray(32) { (marker + 1).toByte() }), + sendHashAlgorithm = AttoAlgorithm.V1, + sendHash = AttoHash(ByteArray(32) { (marker + 2).toByte() }), + ), + signature = AttoSignature(ByteArray(64) { (marker + 3).toByte() }), + work = AttoWork(ByteArray(8) { (marker + 4).toByte() }), + ) } From d752b9814c3b0de27b40073f74c77558798ca256 Mon Sep 17 00:00:00 2001 From: Cryptoloveer Date: Mon, 14 Sep 2026 10:57:11 +0200 Subject: [PATCH 5/5] test: cover configured discovery address hints --- .../discovery/LastDiscovererBroadcastTest.kt | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererBroadcastTest.kt diff --git a/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererBroadcastTest.kt b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererBroadcastTest.kt new file mode 100644 index 00000000..aa9b0b62 --- /dev/null +++ b/src/test/kotlin/cash/atto/node/bootstrap/discovery/LastDiscovererBroadcastTest.kt @@ -0,0 +1,128 @@ +package cash.atto.node.bootstrap.discovery + +import cash.atto.commons.AttoAddress +import cash.atto.commons.AttoAlgorithm +import cash.atto.commons.AttoAmount +import cash.atto.commons.AttoHash +import cash.atto.commons.AttoInstant +import cash.atto.commons.AttoNetwork +import cash.atto.commons.AttoPublicKey +import cash.atto.commons.AttoReceiveBlock +import cash.atto.commons.AttoSignature +import cash.atto.commons.AttoTransaction +import cash.atto.commons.AttoWork +import cash.atto.commons.toAttoHeight +import cash.atto.commons.toAttoVersion +import cash.atto.node.EventPublisher +import cash.atto.node.account.Account +import cash.atto.node.account.AccountRepository +import cash.atto.node.bootstrap.unchecked.UncheckedTransactionRepository +import cash.atto.node.network.BroadcastNetworkMessage +import cash.atto.node.network.NetworkMessagePublisher +import cash.atto.node.network.NodeConnectionManager +import cash.atto.node.transaction.Transaction +import cash.atto.node.transaction.TransactionRepository +import cash.atto.node.transaction.toTransaction +import cash.atto.node.vote.convertion.VoteConverter +import cash.atto.node.vote.weight.VoteWeighter +import cash.atto.protocol.AttoBootstrapTransactionPush +import cash.atto.protocol.AttoNode +import io.mockk.coEvery +import io.mockk.every +import io.mockk.firstArg +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.Instant + +class LastDiscovererBroadcastTest { + @Test + fun `configured account head is included once alongside random sample`() = + runTest { + val random = transaction(1) + val hinted = transaction(2) + val hintedAddress = AttoAddress(hinted.algorithm, hinted.publicKey) + val properties = DiscoveryProperties().apply { hintedAddresses = setOf(hintedAddress) } + + val accountRepository = mockk() + coEvery { accountRepository.findById(hinted.publicKey) } returns account(hinted) + + val transactionRepository = mockk() + coEvery { transactionRepository.getLastSample(10L) } returns flowOf(random, hinted) + coEvery { transactionRepository.findById(hinted.hash) } returns hinted + + val uncheckedRepository = mockk() + coEvery { uncheckedRepository.count() } returns 0 + + val sent = mutableListOf>() + val connectionManager = mockk() + coEvery { connectionManager.send(any>()) } answers { + sent += firstArg>() + } + + val thisNode = mockk() + every { thisNode.isNotHistorical() } returns false + + val discoverer = + LastDiscoverer( + thisNode = thisNode, + discoveryProperties = properties, + accountRepository = accountRepository, + transactionRepository = transactionRepository, + uncheckedTransactionRepository = uncheckedRepository, + nodeConnectionManager = connectionManager, + networkMessagePublisher = mockk(relaxed = true), + eventPublisher = mockk(relaxed = true), + discoveryQueue = mockk(relaxed = true), + voteConverter = mockk(relaxed = true), + voteWeighter = mockk(relaxed = true), + ) + + try { + discoverer.broadcastSample() + + val hashes = + sent.map { + (it.payload as AttoBootstrapTransactionPush).transaction.hash + } + assertEquals(listOf(random.hash, hinted.hash), hashes) + } finally { + discoverer.close() + } + } + + private fun account(transaction: Transaction): Account = + Account( + publicKey = transaction.publicKey, + network = transaction.block.network, + version = 0U.toAttoVersion(), + algorithm = transaction.algorithm, + height = transaction.height.value.toLong(), + balance = transaction.block.balance, + lastTransactionTimestamp = Instant.now(), + lastTransactionHash = transaction.hash, + representativeAlgorithm = transaction.algorithm, + representativePublicKey = transaction.publicKey, + ) + + private fun transaction(marker: Byte): Transaction = + AttoTransaction( + block = + AttoReceiveBlock( + version = 0U.toAttoVersion(), + network = AttoNetwork.LOCAL, + algorithm = AttoAlgorithm.V1, + publicKey = AttoPublicKey(ByteArray(32) { marker }), + height = 2U.toAttoHeight(), + balance = AttoAmount.MAX, + timestamp = AttoInstant.now(), + previous = AttoHash(ByteArray(32) { (marker + 1).toByte() }), + sendHashAlgorithm = AttoAlgorithm.V1, + sendHash = AttoHash(ByteArray(32) { (marker + 2).toByte() }), + ), + signature = AttoSignature(ByteArray(64) { (marker + 3).toByte() }), + work = AttoWork(ByteArray(8) { (marker + 4).toByte() }), + ).toTransaction() +}