diff --git a/AGENTS.md b/AGENTS.md index 6284195..9d922d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,22 @@ This document describes the agent-based architecture of the ChainCash protocol - ChainCash implements a decentralized monetary system where different agents manage the lifecycle of digital notes backed by collateral and trust. The system enables self-sovereign banking where each participant can issue, transfer, and redeem digital currency. -## Core On-Chain Agents +## Current Lifecycle Boundary + +Only the Basis v2 and Basis-token v2 candidate sources remain in the production +contract surface. Basis v1, the original `contracts/onchain` family, the +`contracts/layer2-old` experiments, their transaction builders, participant +secret helpers, and their address/scan-rule printers are retired. Exact old +source snapshots now live under `src/test/resources` solely for historical +regression tests; never use them to derive an address, build a transaction, or +infer deployment status. The removed Scala helpers remain available in Git +history at the pinned commit below. + +The sections describing the original ChainCash on-chain agents and flows below +are historical design context. Their old paths refer to the source layout at +commit `78475e30362571acf56e4e38276a9d6c0a84ce0c`, not active production files. + +## Historical On-Chain Agents ### Reserve Contract Agent **File**: `contracts/onchain/reserve.es` @@ -57,6 +72,10 @@ ChainCash implements a decentralized monetary system where different agents mana ## Off-Chain Management Agents +The legacy reserve/note construction helpers described in this section are +historical and are no longer compiled. Read-only tracking code remains only to +support a future, separately reviewed inventory of any old state. + ### Wallet Agent **File**: `src/main/scala/chaincash/offchain/WalletUtils.scala` @@ -114,7 +133,10 @@ ChainCash implements a decentralized monetary system where different agents mana - Event processing and persistence ### Basis Tracker Agent -**File**: `contracts/offchain/tracker.md` (documentation), `demo/basis/simple/src/TrackerBoxSetup.scala` (setup) +**Historical files**: +`src/test/resources/contracts/historical/offchain/tracker.md`. The former +`demo/basis/simple/src/TrackerBoxSetup.scala` walkthrough is available only in +Git history at commit `78475e30362571acf56e4e38276a9d6c0a84ce0c`. **Responsibilities**: - Tracks complete state of debt for all issuers (with or without on-chain reserves) @@ -141,7 +163,9 @@ ChainCash implements a decentralized monetary system where different agents mana - Enables true escape from tracker unavailability - Anti-censorship protection (previously witnessed notes can still be redeemed) -**See Also**: `contracts/offchain/basis.md` for Basis protocol design, `contracts/offchain/tracker.md` for detailed tracker architecture +**See Also**: the test-only `offchain/basis.md` and `offchain/tracker.md` +fixtures for the historical v1 design; use `contracts/offchain/basis-v2.md` for +the active candidate ABI. ## Server Agent @@ -227,8 +251,9 @@ Creditor → Reserve Agent (after timeout) ``` ### Reserve Owner Refund (Censorship-Resistant Exit) -Implemented in both `contracts/offchain/basis.es` (ERG reserve) and -`contracts/offchain/basis-token.es` (token reserve). +Historically implemented in the test-only fixtures +`src/test/resources/contracts/historical/offchain/basis.es` (ERG reserve) and +`src/test/resources/contracts/historical/offchain/basis-token.es` (token reserve). ``` Reserve Owner (two-phase, unilateral) 1. Initiate refund (action #2): owner signs tx setting R7 to initiation height @@ -307,4 +332,4 @@ See test files in `src/test/scala/chaincash/` for detailed agent testing. --- -*This architecture enables a global monetary system with decentralized issuance where each participant can define their own acceptance rules while maintaining collective backing through the spending chain.* \ No newline at end of file +*This architecture enables a global monetary system with decentralized issuance where each participant can define their own acceptance rules while maintaining collective backing through the spending chain.* diff --git a/README.md b/README.md index 0b3d8fc..e890ba0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ This repository contains whitepaper and some prototyping code for ChainCash, a protocol to create money in self-sovereign way via trust or collateral, with collective backing and individual acceptance. +## Protocol Lifecycle + +The original on-chain reserve/note/receipt prototype and the experimental +Layer-2 prototype are historical, test-only sources. They are not compiled, +addressed, or activated by production code. See +[`docs/legacy-contract-retirement.md`](docs/legacy-contract-retirement.md). + +The production-code contract surface contains only the reviewed Basis v2 +candidate sources and their frozen full-ErgoTree goldens. Basis v1 is also +retired to test-only fixtures. No deployment, signing, submission, broadcast +or in-place migration path is provided. + ## Intro We consider money as a set of digital notes, and every note is collectively backed @@ -47,21 +59,12 @@ More introductory materials: * [Video presentation from Ergo Summit](https://www.youtube.com/watch?v=NxIlIpO6ZVI) * [Video: ChainCash, part two](https://www.youtube.com/watch?v=fk8ZFvNFDYc) -## Basis Demo - -A simple demonstration of the Basis reserve protocol is available in `demo/basis/simple/`: +## Retired Basis v1 demos -```bash -cd demo/basis/simple - -# Generate redemption transaction -sbt "runMain chaincash.contracts.BasisNoteRedeemer --note-json note.json --reserve-box auto --tracker-box auto --fee-box ,,, --output sign_request.json" - -# Sign with Ergo node -curl -X POST "http://localhost:9053/wallet/transaction/sign" -H "api_key: hello" -d @sign_request.json -``` - -See `demo/basis/simple/README.md` for detailed instructions. +The former Basis v1 examples and operational walkthroughs are removed from the +working tree so they cannot be mistaken for supported execution or deployment +paths. They remain recoverable from Git history at commit +`78475e30362571acf56e4e38276a9d6c0a84ce0c` for historical research. ## ChainCash Server @@ -74,15 +77,19 @@ Initial version of [design document](docs/server.md) is also available. The serv * Whitepaper - https://github.com/ChainCashLabs/chaincash/blob/master/docs/conf/conf.pdf High-level description of ChainCash protocol and its implementation -* Contracts - https://github.com/kushti/chaincash/tree/master/contracts - note and reserve contracts in ErgoScript +* Contracts - `contracts/offchain/` contains only the Basis v2 candidate family + and its exact `.p2s` goldens. Basis v1 and the former on-chain and Layer-2 + prototypes are pinned historical fixtures under + `src/test/resources/contracts/historical/`. * Modelling - https://github.com/kushti/chaincash/tree/master/src/main/scala/chaincash/model Contract-less and blockchain-less models of ChainCash entities and one of notes collateralization estimation options. -* Tests - https://github.com/kushti/chaincash/blob/master/src/test/scala/kiosk/ChainCashSpec.scala - Kiosk-based tests for transactions involving note - contracts (note creation, spending, redemption) -* Offchain part - https://github.com/kushti/chaincash/tree/master/src/main/scala/chaincash/offchain - on-chain data tracking, - persistence, transaction builders. This is very rough prototype, at the moment better to look into ChainCash Server which +* Tests - `src/test/scala/chaincash/ChainCashSpec.scala` replays the historical + note-contract transitions through test-only fixtures. +* Offchain part - `src/main/scala/chaincash/offchain/` retains rough prototype + tracking and persistence code, but no production legacy transaction builder. + For application development, prefer ChainCash Server, which is available at [https://github.com/BetterMoneyLabs/chaincash-rs](https://github.com/BetterMoneyLabs/chaincash-rs) . ## Communications @@ -91,33 +98,16 @@ Join discussion groups for developers and users: * Telegram: [https://t.me/chaincashtalks](https://t.me/chaincashtalks) -## Deployment Utilities - -### Basis Reserve Contract Deployment +## Candidate-byte inspection -The repository includes deployment utilities for the Basis reserve contract: +The production commands print only the v2 candidate addresses and the +source-to-ErgoTree receipt material: ```scala -// Run deployment utility -sbt 'runMain chaincash.contracts.BasisDeployer' - -// Or use the contract printer -sbt 'runMain chaincash.contracts.Constants$Printer' +sbt 'runMain chaincash.contracts.Printer' +sbt 'runMain chaincash.contracts.BasisV2ReceiptPrinter' ``` -This generates deployment requests for the Basis reserve contract, which supports: -- Off-chain payments with credit creation -- Redemption with 2% fee -- Emergency redemption after 7 days -- Tracker-based debt tracking - -See `src/main/scala/chaincash/contracts/README.md` for detailed usage. - -## TODO - -* update ReserveData.liabilities and reserveKeys in offchain code -* offchain code for note redemption -* support few spendings of a note in the same block (offchain tracking of it) -* support other tokens in reserves, e.g. SigUSD -* efficient persistence for own notes (currently, all the notes in the system are iterated over) -* check ERG preservation in note contracts +These commands do not generate transactions. The printer emits only entries in +`Constants.publishedContracts`; all v1 and legacy addresses and builders are +deliberately unavailable from production tooling. diff --git a/build.sbt b/build.sbt index dcc7aac..8d6b7fd 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,11 @@ +import scala.collection.JavaConverters._ + name := "chaincash" version := "0.2.1" organization := "org.ergoplatform" scalaVersion := "2.12.17" -Compile / unmanagedClasspath += baseDirectory.value / "contracts" - resolvers ++= Seq( "Sonatype Releases" at "https://oss.sonatype.org/content/repositories/releases/", "SonaType" at "https://oss.sonatype.org/content/groups/public", @@ -39,3 +39,55 @@ libraryDependencies ++= Seq( ) dependencyOverrides += "org.ergoplatform" %% "ergo-appkit" % "6.0.0" + +lazy val verifyMainJarRetirement = taskKey[Unit]( + "Fail if the production JAR contains retired ChainCash or Basis v1 classes/resources" +) + +verifyMainJarRetirement := { + val jarPath = (Compile / packageBin).value + val log = streams.value.log + val jar = new java.util.jar.JarFile(jarPath) + try { + val entries = jar.entries().asScala.map(_.getName).toVector + val forbiddenClassPrefixes = Vector( + "chaincash/contracts/BasisDeployer", + "chaincash/contracts/BasisConstants", + "chaincash/contracts/BasisNoteCreator", + "chaincash/contracts/ContractsPrinter", + "chaincash/contracts/ParticipantKeys", + "chaincash/contracts/ParticipantSecretsReader", + "chaincash/offchain/NoteUtils", + "chaincash/offchain/ReserveUtils", + "chaincash/offchain/Tester" + ) + val forbiddenResources = Set( + "contracts/offchain/basis.es", + "contracts/offchain/basis-token.es" + ) + val forbiddenResourcePrefixes = Vector( + "contracts/historical/", + "contracts/onchain/", + "contracts/layer2-old/" + ) + + val forbiddenEntries = entries.filter { entry => + forbiddenClassPrefixes.exists(entry.startsWith) || + forbiddenResources.contains(entry) || + forbiddenResourcePrefixes.exists(entry.startsWith) + } + + if (forbiddenEntries.nonEmpty) { + sys.error( + "Retired ChainCash/Basis v1 entries leaked into the production JAR: " + + forbiddenEntries.sorted.mkString(", ") + ) + } + + log.info(s"Verified ${entries.size} production JAR entries: no retired ChainCash/Basis v1 surface") + } finally { + jar.close() + } +} + +Test / test := (Test / test).dependsOn(verifyMainJarRetirement).value diff --git a/contracts/offchain/README.md b/contracts/offchain/README.md index 106d97a..d854445 100644 --- a/contracts/offchain/README.md +++ b/contracts/offchain/README.md @@ -1,4 +1,24 @@ -Different ChainCash variants for offchain applications. +# ChainCash off-chain contract candidates +## Active source family -In most cases, reserves are on-chain, notes are created and making progress offchain. +`basis-v2.es` and `basis-token-v2.es` are the only contract sources exposed by +production compilation and address tooling. Their committed `.p2s` files are +the exact full-ErgoTree goldens for this candidate generation. + +See `basis-v2.md` for the register and transition ABI, and +`basis-v2-reproducibility.md` for the source, compiler and full-byte receipt. +The repository provides no v2 deployment, signing, submission or migration +builder. A compiled address is not evidence of deployment or production +readiness. + +## Historical sources + +The exact Basis v1 contract sources are retained only under +`src/test/resources/contracts/historical/` for regression tests. Its former +demos and operational walkthroughs are removed from the working tree and remain +recoverable at commit `78475e30362571acf56e4e38276a9d6c0a84ce0c`. +Production `Constants`, printers, classes and the main JAR cannot load or expose +that generation. Moving the sources does not change the authorization of any +immutable v1 box; recovery would require a separate inventory and an exit +already authorized by its exact old ErgoTree. diff --git a/contracts/offchain/basis-token-v2.es b/contracts/offchain/basis-token-v2.es new file mode 100644 index 0000000..48a00b6 --- /dev/null +++ b/contracts/offchain/basis-token-v2.es @@ -0,0 +1,337 @@ +{ + // Basis token reserve, ABI generation v2. + // See basis-v2.md. V1 contracts are intentionally left unchanged. + + val packedOpt = getVar[Byte](0) + val selfRegistersDefined = + SELF.R4[GroupElement].isDefined && + SELF.R5[AvlTree].isDefined && + SELF.R6[Coll[Byte]].isDefined && + SELF.R7[Long].isDefined && + SELF.R8[Long].isDefined && + SELF.R9[Coll[Byte]].isDefined + + if (!packedOpt.isDefined || !selfRegistersDefined) { + sigmaProp(false) + } else { + val packed = packedOpt.get + val action = packed / 10 + val outputIndex = packed % 10 + + val ownerKey = SELF.R4[GroupElement].get + val identity = groupGenerator.exp(byteArrayToBigInt(fromBase16("00"))) + val redeemedTree = SELF.R5[AvlTree].get + val trackerNftId = SELF.R6[Coll[Byte]].get + val refundHeight = SELF.R7[Long].get + val emergencyHeight = SELF.R8[Long].get + val predecessorId = SELF.R9[Coll[Byte]].get + + val reserveTokensShape = + SELF.tokens.size == 2 && + SELF.tokens(0)._2 == 1L && + SELF.tokens(0)._1 != SELF.tokens(1)._1 && + SELF.tokens(1)._2 > 0L + val redeemedTreeShape = + redeemedTree.keyLength == 32 && + redeemedTree.valueLengthOpt.isDefined && + redeemedTree.valueLengthOpt.get == 24 && + redeemedTree.isInsertAllowed && + redeemedTree.isUpdateAllowed && + !redeemedTree.isRemoveAllowed + val selfShape = + packed >= 0 && + reserveTokensShape && + trackerNftId.size == 32 && + predecessorId.size == 32 && + emergencyHeight > 0L && + refundHeight >= 0L && + ownerKey != identity && + redeemedTreeShape + + if (!selfShape) { + sigmaProp(false) + } else if (action == 0) { + val receiverOpt = getVar[GroupElement](1) + val reserveSigOpt = getVar[Coll[Byte]](2) + val totalDebtOpt = getVar[Long](3) + val timestampOpt = getVar[Long](4) + val updateProofOpt = getVar[Coll[Byte]](5) + val priorProofOpt = getVar[Coll[Byte]](7) + val outputShape = + outputIndex >= 0 && + OUTPUTS.size > outputIndex + 1 + val contextShape = + receiverOpt.isDefined && + reserveSigOpt.isDefined && + totalDebtOpt.isDefined && + timestampOpt.isDefined && + updateProofOpt.isDefined && + priorProofOpt.isDefined + + if (!outputShape || !contextShape) { + sigmaProp(false) + } else { + val selfOut = OUTPUTS(outputIndex) + val payout = OUTPUTS(outputIndex + 1) + val successorRegistersDefined = + selfOut.R4[GroupElement].isDefined && + selfOut.R5[AvlTree].isDefined && + selfOut.R6[Coll[Byte]].isDefined && + selfOut.R7[Long].isDefined && + selfOut.R8[Long].isDefined && + selfOut.R9[Coll[Byte]].isDefined + val payoutLineageDefined = payout.R4[Coll[Byte]].isDefined + + if (!successorRegistersDefined || !payoutLineageDefined) { + sigmaProp(false) + } else { + val receiver = receiverOpt.get + val reserveSig = reserveSigOpt.get + val totalDebt = totalDebtOpt.get + val timestamp = timestampOpt.get + val updateProof = updateProofOpt.get + val priorProof = priorProofOpt.get + val reserveNftId = SELF.tokens(0)._1 + val reserveTokenId = SELF.tokens(1)._1 + + // "BASIS" || ABI 2 || Ergo-mainnet domain 0 || token kind 1. + val domainTag = fromBase16("4241534953020001") + val claimKey = blake2b256( + domainTag ++ + reserveNftId ++ + reserveTokenId ++ + trackerNftId ++ + ownerKey.getEncoded ++ + receiver.getEncoded + ) + val message = + claimKey ++ + longToByteArray(totalDebt) ++ + longToByteArray(timestamp) + + val reserveSigShape = reserveSig.size >= 64 && reserveSig.size <= 66 + val properReserveSignature = if (reserveSigShape) { + val aBytes = reserveSig.slice(0, 33) + val zBytes = reserveSig.slice(33, reserveSig.size) + val a = decodePoint(aBytes) + val z = byteArrayToBigInt(zBytes) + val e = byteArrayToBigInt( + blake2b256(aBytes ++ message ++ ownerKey.getEncoded) + ) + a != identity && + groupGenerator.exp(z) == a.multiply(ownerKey.exp(e)) + } else { + false + } + + // A proof is mandatory for both membership and non-membership. + val priorOpt = redeemedTree.get(claimKey, priorProof) + val zeroState = + longToByteArray(0L) ++ + longToByteArray(0L) ++ + longToByteArray(0L) + val priorRaw = priorOpt.getOrElse(zeroState) + val priorShape = !priorOpt.isDefined || priorRaw.size == 24 + val priorBytes = if (priorRaw.size == 24) priorRaw else zeroState + val storedTimestamp = byteArrayToLong(priorBytes.slice(0, 8)) + val storedTotalDebt = byteArrayToLong(priorBytes.slice(8, 16)) + val storedRedeemed = byteArrayToLong(priorBytes.slice(16, 24)) + val storedStateValid = + storedTimestamp >= 0L && + storedTotalDebt >= 0L && + storedRedeemed >= 0L && + storedRedeemed <= storedTotalDebt + val claimProgressValid = if (priorOpt.isDefined) { + (timestamp == storedTimestamp && totalDebt == storedTotalDebt) || + (timestamp > storedTimestamp && totalDebt >= storedTotalDebt) + } else { + timestamp > 0L && totalDebt > 0L + } + + val successorTokenShape = + selfOut.tokens.size == 2 && + selfOut.tokens(0)._1 == reserveNftId && + selfOut.tokens(0)._2 == 1L && + selfOut.tokens(1)._1 == reserveTokenId && + selfOut.tokens(1)._2 > 0L + val successorCommon = + selfOut.propositionBytes == SELF.propositionBytes && + successorTokenShape && + selfOut.R4[GroupElement].get == ownerKey && + selfOut.R6[Coll[Byte]].get == trackerNftId && + selfOut.R7[Long].get == refundHeight && + selfOut.R8[Long].get == emergencyHeight && + selfOut.R9[Coll[Byte]].get == SELF.id + val receiverCondition = proveDlog(receiver) + val receiverValid = receiver != identity + val amount = SELF.tokens(1)._2 - selfOut.tokens(1)._2 + val payoutBound = + payout.propositionBytes == receiverCondition.propBytes && + payout.value > 0L && + payout.tokens.size == 1 && + payout.tokens(0)._1 == reserveTokenId && + payout.tokens(0)._2 == amount && + payout.R4[Coll[Byte]].get == SELF.id + val valueFlowValid = + selfOut.value >= SELF.value && + amount > 0L + val available = if ( + storedRedeemed >= 0L && totalDebt >= storedRedeemed + ) { + totalDebt - storedRedeemed + } else { + 0L + } + val amountValid = amount > 0L && amount <= available + val newRedeemed = if (amountValid) { + storedRedeemed + amount + } else { + storedRedeemed + } + val nextValue = + longToByteArray(timestamp) ++ + longToByteArray(totalDebt) ++ + longToByteArray(newRedeemed) + val nextTree = redeemedTree + .insertOrUpdate(Coll((claimKey, nextValue)), updateProof) + .get + val stateTransitionValid = + nextTree == selfOut.R5[AvlTree].get + + val emergency = HEIGHT >= emergencyHeight + val trackerEvidenceValid = if (emergency) { + true + } else { + val trackerSigOpt = getVar[Coll[Byte]](6) + val trackerProofOpt = getVar[Coll[Byte]](8) + val trackerInputPresent = CONTEXT.dataInputs.size > 0 + if ( + !trackerSigOpt.isDefined || + !trackerProofOpt.isDefined || + !trackerInputPresent + ) { + false + } else { + val tracker = CONTEXT.dataInputs(0) + val trackerRegistersDefined = + tracker.R4[GroupElement].isDefined && + tracker.R5[AvlTree].isDefined + val trackerTokenShape = + tracker.tokens.size == 1 && + tracker.tokens(0)._1 == trackerNftId && + tracker.tokens(0)._2 == 1L + if (!trackerRegistersDefined || !trackerTokenShape) { + false + } else { + val trackerKey = tracker.R4[GroupElement].get + val trackerTree = tracker.R5[AvlTree].get + val trackerTreeShape = + trackerTree.keyLength == 32 && + trackerTree.valueLengthOpt.isDefined && + trackerTree.valueLengthOpt.get == 8 && + trackerTree.isInsertAllowed && + trackerTree.isUpdateAllowed && + !trackerTree.isRemoveAllowed + val trackerDebtOpt = trackerTree.get(claimKey, trackerProofOpt.get) + val trackerDebtValid = if (trackerDebtOpt.isDefined) { + val debtBytes = trackerDebtOpt.get + debtBytes.size == 8 && + byteArrayToLong(debtBytes) >= totalDebt + } else { + false + } + val trackerSig = trackerSigOpt.get + val trackerSigShape = trackerSig.size >= 64 && trackerSig.size <= 66 + val properTrackerSignature = if (trackerSigShape) { + val aBytes = trackerSig.slice(0, 33) + val zBytes = trackerSig.slice(33, trackerSig.size) + val a = decodePoint(aBytes) + val z = byteArrayToBigInt(zBytes) + val e = byteArrayToBigInt( + blake2b256(aBytes ++ message ++ trackerKey.getEncoded) + ) + a != identity && + groupGenerator.exp(z) == a.multiply(trackerKey.exp(e)) + } else { + false + } + trackerKey != identity && + trackerTreeShape && trackerDebtValid && properTrackerSignature + } + } + } + + sigmaProp( + successorCommon && + receiverValid && + payoutBound && + valueFlowValid && + priorShape && + storedStateValid && + claimProgressValid && + amountValid && + stateTransitionValid && + properReserveSignature && + trackerEvidenceValid + ) && receiverCondition + } + } + } else if (action == 1 || action == 2) { + val outputShape = outputIndex >= 0 && OUTPUTS.size > outputIndex + if (!outputShape) { + sigmaProp(false) + } else { + val selfOut = OUTPUTS(outputIndex) + val successorRegistersDefined = + selfOut.R4[GroupElement].isDefined && + selfOut.R5[AvlTree].isDefined && + selfOut.R6[Coll[Byte]].isDefined && + selfOut.R7[Long].isDefined && + selfOut.R8[Long].isDefined && + selfOut.R9[Coll[Byte]].isDefined + val successorTokenShape = + selfOut.tokens.size == 2 && + selfOut.tokens(0)._1 == SELF.tokens(0)._1 && + selfOut.tokens(0)._2 == 1L && + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && + selfOut.tokens(1)._2 > 0L + if (!successorRegistersDefined || !successorTokenShape) { + sigmaProp(false) + } else { + val successorCommon = + selfOut.propositionBytes == SELF.propositionBytes && + selfOut.R4[GroupElement].get == ownerKey && + selfOut.R5[AvlTree].get == redeemedTree && + selfOut.R6[Coll[Byte]].get == trackerNftId && + selfOut.R8[Long].get == emergencyHeight && + selfOut.R9[Coll[Byte]].get == SELF.id + if (action == 1) { + sigmaProp( + successorCommon && + selfOut.R7[Long].get == refundHeight && + selfOut.value >= SELF.value && + selfOut.tokens(1)._2 - SELF.tokens(1)._2 >= 1L + ) + } else { + sigmaProp( + successorCommon && + refundHeight == 0L && + selfOut.R7[Long].get >= HEIGHT.toLong && + selfOut.R7[Long].get <= HEIGHT.toLong + 30L && + selfOut.value >= SELF.value && + selfOut.tokens(1)._2 >= SELF.tokens(1)._2 + ) && proveDlog(ownerKey) + } + } + } + } else if (action == 3) { + sigmaProp( + refundHeight > 0L && + HEIGHT.toLong >= 43200L && + refundHeight <= HEIGHT.toLong - 43200L + ) && proveDlog(ownerKey) + } else { + sigmaProp(false) + } + } +} diff --git a/contracts/offchain/basis-token-v2.p2s b/contracts/offchain/basis-token-v2.p2s new file mode 100644 index 0000000..debc245 --- /dev/null +++ b/contracts/offchain/basis-token-v2.p2s @@ -0,0 +1 @@ +1ba80f6d01000e0100041404140400040404000502040004020402050004400440050005000440043001000400040004020100040201000400040204020e08424153495302000105000430040004100410042004200430050005000500040404000400050204020402050005000402040004000500043005000500050005000500048001048401040004420442010001010400010004000402040004000502010004400410041001000480010484010400044204420100040204040400010004040400040004000502040204020402050001000402040205020500053c04020402040605000580a3050580a3050100d802d601e30002d602c6a7040795ecefe67201efededededede67202e6c6a70564e6c6a7060ee6c6a70705e6c6a70805e6c6a7090ed17300d80bd6037ee4720104d604db6308a7d605e4c6a7060ed606e4c6a70805d607e4c6a70705d608e47202d609db6a01ddd60a9f72097b7301d60be4c6a70564d60c9d72037302d60d9e7203730395efededededededed9272037304ededed93b172047305938cb27204730600027307948cb27204730800018cb2720473090001918cb27204730a0002730b93b17205730c93b1e4c6a7090e730d917206730e927207730f947208720aededededed93db6403720b7310e6db6404720b93e4db6404720b7311db6405720bdb6406720befdb6407720bd173129593720c7313d806d60ee30107d60fe3020ed610e30305d611e30405d612e3050ed613e3070e95ecefed92720d731491b1a59a720d7315efededededede6720ee6720fe67210e67211e67212e67213d17316d804d614b2a5720d00d615c672140407d616b2a59a720d731700d617c67216040e95ecefededededede67215e6c672140564e6c67214060ee6c672140705e6c672140805e6c67214090eefe67217d17318d81ad618db63087214d6198cb2720473190001d61ab27204731a00d61b8c721a01d61ce4720ed61dcd721cd61e998c721a028cb27218731b0002d61fdb07027208d620cbb3b3b3b3b3731c7219721b7205721fdb0702721cd621dc640a720b027220e47213d622e67221d6237a731dd624b3b3722372237223d625e572217224d6269593b17225731e72257224d6277cb47226731f7320d6287cb4722673217322d6297cb4722673237324d62ae47211d62be47210d62ced91721e732590721e95ed927229732692722b722999722b72297327d62d7a722ad62e7a722bd62fe4720fd630b1722fd631b3b37220722e722dea02d1edededededededededededededededed93c27214c2a7edededed93b172187328938cb27218732900017219938cb27218732a0002732b938cb27218732c0001721b918cb27218732d0002732e93e47215720893e4c67214060e720593e4c672140705720793e4c672140805720693e4c67214090ec5a794721c720aededededed93c27216d0721d91c17216732f93b1db630872167330938cb2db6308721673310001721b938cb2db6308721673320002721e93e47217c5a7ed92c17214c1a791721e7333ecef722293b172257334ededed9272277335927228733692722973379072297228957222eced93722a722793722b7228ed91722a722792722b7228ed91722a733891722b7339722c93e4dc6410720b0283013c0e0e86027220b3b3722d722e7a95722c9a7229721e7229e47212e4c67214056495ed927230733a907230733bd802d632b4722f733c733dd633ee7232ed947233720a939f72097bb4722f733e7230a072339f72087bcbb3b372327231721f733f95927ea30572067340d803d632e3060ed633e3080ed634db6501fe95ececefe67232efe6723390b1723473417342d803d635b27234734300d636c672350407d637db6308723595ecefede67236e6c672350564efeded93b172377344938cb27237734500017205938cb272377346000273477348d805d638e47236d639e4c672350564d63adc640a7239027220e47233d63be47232d63cb1723bededed947238720aededededed93db640372397349e6db6404723993e4db64047239734adb64057239db64067239efdb6407723995e6723ad801d63de4723aed93b1723d734b927c723d722b734c95ed92723c734d90723c734ed802d63db4723b734f7350d63eee723ded94723e720a939f72097bb4723b7351723ca0723e9f72387bcbb3b3723d7231db070272387352721dd801d60e93720c735395ec720e93720c735495efed92720d735591b1a5720dd17356d803d60fb2a5720d00d610c6720f0407d611db6308720f95ecefededededede67210e6c6720f0564e6c6720f060ee6c6720f0705e6c6720f0805e6c6720f090eefedededed93b172117357938cb27211735800018cb2720473590001938cb27211735a0002735b938cb27211735c00018cb27204735d0001918cb27211735e0002735fd17360d801d612ededededed93c2720fc2a793e47210720893e4c6720f0564720b93e4c6720f060e720593e4c6720f0805720693e4c6720f090ec5a795720ed1ededed721293e4c6720f0705720792c1720fc1a792998cb27211736100028cb27204736200027363ea02d1ededededed7212937207736492e4c6720f07057ea30590e4c6720f07059a7ea305736592c1720fc1a7928cb27211736600028cb2720473670002cd72089593720c7368ea02d1eded9172077369927ea305736a907207997ea305736bcd7208d1736c diff --git a/contracts/offchain/basis-v2-reproducibility.md b/contracts/offchain/basis-v2-reproducibility.md new file mode 100644 index 0000000..0f9f437 --- /dev/null +++ b/contracts/offchain/basis-v2-reproducibility.md @@ -0,0 +1,87 @@ +# Basis v2 source-to-ErgoTree receipt + +This receipt binds the local Basis v2 candidate to exact compiler inputs and +full ErgoTree bytes. It is a build-parity record, not a security, deployment or +activation claim. + +## Source identity + +- Repository base commit: + `78475e30362571acf56e4e38276a9d6c0a84ce0c` +- Immutable contract source and golden commit: + `9a274396d5f78f7be5ed76bacee5329c42570317` + +| Path | Exact candidate Git blob id | +| --- | --- | +| `contracts/offchain/basis-v2.es` | `47b3e0647f90db2dd8e667641294d66e287d8f6a` | +| `contracts/offchain/basis-token-v2.es` | `48a00b612fe6f4345b322d22408c48100f2cd442` | +| `contracts/offchain/basis-v2.p2s` | `f806b34edab4b18b89a395625db753d7acdf94db` | +| `contracts/offchain/basis-token-v2.p2s` | `debc24531f0adc26292f897fff6b53ed06ff4374` | +| `src/test/scala/chaincash/BasisV2Spec.scala` | `10613d30d499a02941f8b8a9c8b55c06568e796b` | + +The contract sources and golden byte files above are immutable at the named +commit. The test blob is pinned independently because later closeout commits +may strengthen the negative matrix without changing either contract or golden. + +- The compiler reads UTF-8 with `getLines.mkString("\n")`. It therefore uses LF + separators and removes the final line terminator independently of checkout + line endings. + +| Compiler input | SHA-256 of normalized UTF-8 bytes | +| --- | --- | +| `basis-v2.es` | `31ff4271b1c79302064df83a6bfa3d4f6f5f153747002c1c0206b2f3d88b507a` | +| `basis-token-v2.es` | `c7e5a0cf6a12aaedefba79ecd012ac7839a48f683931d1d4e10371d287c58573` | + +## Compiler closure + +| Input | Version / SHA-256 | +| --- | --- | +| Scala | 2.12.17 | +| SBT | 1.5.2; launcher `b4c0c55d68f11b1510d884641cb1b1456191dac40ddc958bf86c825adc344e16` | +| JDK | Microsoft OpenJDK 17.0.19+10-LTS, 64-bit | +| Ergo Appkit | `org.ergoplatform:ergo-appkit_2.12:6.0.0`; jar `258e2c8e1d3f8d68c82cdb36126d7b1bd5100a70aadf9e4c46dc67fadaf9822b` | +| Sigma State | `org.scorexfoundation:sigma-state_2.12:6.0.2`; jar `0dbd3b31ef94affec83f8f0f6c5a9891c45da1e975ff6016a0574fc5aa1418e6` | +| `build.sbt` | `806c9e6a228f08ef9f1a3c6fb9b11c0aac097157b03925bfbfeaf1e203420ca0` | +| `project/build.properties` | `cb740d250483ffcfe325411426588065e377c7e4ce7a5faf6cfdde97f390ada1` | +| `project/plugins.sbt` | `e1188d30689ed8c50d7e154158aabdac75b5d91612a3f61b89b65fc3a6b88296` | + +Compilation uses `AppkitHelpers.compile` through `Constants.compile`, mainnet +network prefix, and block version 4 (the v6 language lane required by +`AvlTree.insertOrUpdate`). No source substitution parameters are used. + +Commands: + +```text +sbt -batch "runMain chaincash.contracts.BasisV2ReceiptPrinter" +sbt -batch "testOnly chaincash.BasisV2Spec" +``` + +## Full output bindings + +The full serialized bytes, not only their digests, are committed in these +single-line lowercase-hex files: + +| Contract | Full-byte file | Byte length | ErgoTree SHA-256 | +| --- | --- | ---: | --- | +| ERG reserve | `contracts/offchain/basis-v2.p2s` | 1,682 | `2690634924efb22359a776f89f5274d77e067bd8ad0619a6e358a2f96697a0c2` | +| token reserve | `contracts/offchain/basis-token-v2.p2s` | 1,963 | `ba1df64e7d95ecffc4f3d49fcada8baebe59a676eb617737cd010bdb52381cb3` | + +`BasisV2Spec` recompiles both normalized sources, compares the complete byte +arrays to `Constants`, then decodes and compares each committed golden file. +`BasisV2ReceiptPrinter` prints the source hashes, full-byte hashes and complete +hex without updating any expected file. + +## Consumer linkage + +`Constants.basisV2ErgoTree` and `Constants.basisTokenV2ErgoTree` are the named +builder/compiler consumers. V1 constants remain separate. Runtime builders, +tracker state, deployment manifests and observed boxes must fail closed unless +their exact bytes match one of these v2 outputs; those cross-repository and +live-state checks are outside this receipt. + +## Evidence ceiling + +One local matching build plus mockchain fixtures establishes parity for the +recorded closure. It does not establish an independent clean-room rebuild, +compiler trust, target-node acceptance, released-wallet compatibility, current +deployment, live-box lineage, economic safety or production readiness. diff --git a/contracts/offchain/basis-v2-review.md b/contracts/offchain/basis-v2-review.md new file mode 100644 index 0000000..1b8d02d --- /dev/null +++ b/contracts/offchain/basis-v2-review.md @@ -0,0 +1,98 @@ +# Basis v2 review packet + +This packet describes the exact local candidate implemented by +`basis-v2.es` and `basis-token-v2.es`. It is intended to make contract and +runtime review reproducible without treating a green mockchain suite as a +deployment claim. + +## Box and branch topology + +The reserve input is the sole authority for its continuing state. Redemption +reads tracker data input 0 only before the immutable R8 boundary. A continuing +branch creates one successor at the context-selected output index. Redemption +also creates one creditor payout immediately after that successor. Miner fees +and wallet change are outside the reserve accounting equations. + +| Action | Authorization | Continuing state | Released value | +| --- | --- | --- | --- | +| 0 redemption before R8 | debtor claim signature, tracker NFT/tree/signature, creditor transaction proof | exact v2 successor and authenticated R5 update | exact P2PK payout bound to `SELF.id` | +| 0 redemption at/after R8 | debtor claim signature and creditor transaction proof; no tracker input/proof/signature | same successor and R5 rules | same exact payout | +| 1 top-up | permissionless | exact state preservation plus input-specific R9 | ERG or reserve tokens may only increase | +| 2 refund initiation | reserve-owner transaction proof | exact state, R7 set within the current 30-block construction window | none | +| 3 refund completion | reserve-owner transaction proof after 43,200 blocks | terminal; no successor required | owner controls the signed terminal transaction | + +## Invariant and consumer matrix + +| ID | Invariant / enforcement | Downstream consumer | Relaxation failure | Focused evidence | +| --- | --- | --- | --- | --- | +| I-01 | Claim key commits v2 tag, reserve NFT, tracker NFT, owner, receiver and token id where applicable. Both signatures and tracker AVL use that key. | tracker builder, note signer, both reserve contracts | one signed liability becomes valid in another reserve, asset or generation | owner-only wrong-reserve and tracker-only wrong-domain signatures rejected independently | +| I-02 | Context 7 always supplies an authenticated R5 membership or non-membership proof. | R5 `insertOrUpdate` and later redemptions | existing state is treated as an unauthenticated zero baseline | omitted proof and stale absence proof rejected | +| I-03 | R5 values are fixed 24-byte `(timestamp,totalDebt,redeemed)` tuples; same claim may advance `redeemed`, newer claims cannot reduce debt. | partial-redemption builder and restart reconstruction | repeated settlement is blocked, redeemed progress is erased or excess collateral is released | repeated partial settlement passes; same-timestamp debt mutation and over-redemption reject independently | +| I-04 | Payout is at `successorIndex + 1`, has receiver P2PK bytes, R4=`SELF.id`, and exact released amount. | creditor wallet and multi-input batcher | redirected, underpaid or reused payout | receiver, amount and lineage mutations rejected independently | +| I-05 | ERG redemption requires `successor.value + payout.value == SELF.value`. | reserve collateral and fee builder | reserve collateral silently pays fees/change | reserve-funded fee mutation rejected; external fee input accepted | +| I-06 | Token successor preserves ERG, singleton and reserve-token ids; reserve-token decrease equals the payout's sole token amount. | token creditor, later reserve transitions and refund | ERG drain, token leakage or accidental burn | top-up ERG drain, redemption ERG drain and payout leakage rejected | +| I-07 | Before R8, tracker input has the exact singleton, typed non-identity key/tree, fixed tree shape, sufficient committed cumulative debt and valid signature. | normal-redemption liveness and liability admission | forged tracker state or uncommitted claim | exact normal redemption passes; NFT, key length, fixed/variable value length, each operation flag, debt and signature-domain mutations reject independently | +| I-08 | R8 is mandatory and exactly preserved; tracker fields are not evaluated only when `HEIGHT >= R8`. | emergency builder and acceptance policy | mutable tracker creation height postpones exit, or missing tracker still blocks the branch | tracker-free redemption passes at R8, rejects one block before, and rejects a mutated successor R8 | +| I-09 | Reserve singleton shape is mandatory; every successor R9 and payout R4 equal the predecessor box id. | indexer lineage and multi-input settlement | tokenless state or one successor/payout pair satisfies two inputs | externally balanced tokenless state and shared successor/payout transactions rejected | +| I-10 | Refund R7 is one-shot, owner-authorized, preserved on other branches and checked with subtraction rather than overflowing addition. | creditor monitoring and terminal refund | backdated/reset timer or overflow-assisted early refund | ownerless ERG/token initiation and completion, mature completion, early completion and `Long.MaxValue` seed exercised | +| I-11 | Constants compile exact normalized source and tests compare full output bytes with committed goldens. | compiler, builders and contract-family selection | readable source, configured P2S and runtime ABI silently diverge | source equality, fresh compile and full-byte golden tests | +| I-12 | Owner, receiver, tracker and received Schnorr commitments are not the group identity. | reserve admission, creditor authorization and manual Schnorr verification | identity owner/tracker signatures are forgeable and an identity receiver has a publicly known witness | ERG and token transactions mutate owner, receiver and tracker keys independently; owner and tracker commitment points are isolated separately | + +Each negative transaction changes only the field named in its test while its +proofs, signatures, tree roots, tokens and unrelated outputs remain those of a +valid control. The property that groups payout mutations constructs and reduces +three independent transactions; it does not use one compound malformed box. + +## Token and register authority + +- ERG reserve tokens: exactly `[reserveNft -> 1]`. +- Token reserve tokens: exactly + `[reserveNft -> 1, reserveAsset -> positiveAmount]`; ids must differ. +- Tracker data-input tokens: exactly `[configuredTrackerNft -> 1]`. +- Reserve R4-R9 types and meaning are frozen in `basis-v2.md`. +- Reserve R5: key length 32, value length 24, insert/update enabled, remove + disabled. +- Tracker R5: key length 32, value length 8, insert/update enabled, remove + disabled. + +## Test closure + +`sbt -batch "testOnly chaincash.BasisV2Spec"` currently executes 22 tests. They +cover normal and emergency redemption, domain separation, proof omission and +replay, non-identity group boundaries, tracker shape and debt admission, R5 +progression, exact payouts, ERG/token conservation, partial settlement, +singleton and output injectivity, both top-up branches, both refund families, +source linkage and full golden bytes. + +The unchanged v1 suites remain a separate regression gate: + +```text +sbt -batch "testOnly chaincash.BasisSpec chaincash.BasisTokenSpec" +``` + +## Evidence vector and residual gates + +| Dimension | Status | +| --- | --- | +| Implementation | matrix-covered by local source and 22 focused mockchain tests | +| Independent review | passed locally for the exact contract/golden candidate and the claim-key vector delta | +| CI | not run | +| Target node | not run; no live or broadcast action authorized | +| Readiness | local draft for maintainer review | + +Remaining integration obligations are explicit: + +- the Rust tracker, note schema, signer and redemption builders must implement + the exact domain key, 24-byte redeemed value, fixed-length AVL metadata, + mandatory context 7 proof, payout R4 marker and R8 semantics; +- reserve admission must require a genuine singleton, a sufficiently distant + R8 and the exact golden ErgoTree; emergency mode intentionally relies on + visible seed policy because the debtor signature is the remaining liability + authority after R8; +- tracker-key rotation and old/new family coexistence need an explicit runtime + policy; no implicit v1/v2 aggregation is allowed; +- v1 contains no migration authorization, so live v1 boxes, if any, must be + inventoried and handled only through their existing branches; +- reduction cost, serialized transaction size, independent clean rebuild, + target-node check, wallet/signer compatibility, reorg/indexer handling and + deployment recovery remain unverified. diff --git a/contracts/offchain/basis-v2.es b/contracts/offchain/basis-v2.es new file mode 100644 index 0000000..47b3e06 --- /dev/null +++ b/contracts/offchain/basis-v2.es @@ -0,0 +1,319 @@ +{ + // Basis ERG reserve, ABI generation v2. + // See basis-v2.md. V1 contracts are intentionally left unchanged. + + val packedOpt = getVar[Byte](0) + val selfRegistersDefined = + SELF.R4[GroupElement].isDefined && + SELF.R5[AvlTree].isDefined && + SELF.R6[Coll[Byte]].isDefined && + SELF.R7[Long].isDefined && + SELF.R8[Long].isDefined && + SELF.R9[Coll[Byte]].isDefined + + if (!packedOpt.isDefined || !selfRegistersDefined) { + sigmaProp(false) + } else { + val packed = packedOpt.get + val action = packed / 10 + val outputIndex = packed % 10 + + val ownerKey = SELF.R4[GroupElement].get + val identity = groupGenerator.exp(byteArrayToBigInt(fromBase16("00"))) + val redeemedTree = SELF.R5[AvlTree].get + val trackerNftId = SELF.R6[Coll[Byte]].get + val refundHeight = SELF.R7[Long].get + val emergencyHeight = SELF.R8[Long].get + val predecessorId = SELF.R9[Coll[Byte]].get + + val singletonShape = + SELF.tokens.size == 1 && + SELF.tokens(0)._2 == 1L + val redeemedTreeShape = + redeemedTree.keyLength == 32 && + redeemedTree.valueLengthOpt.isDefined && + redeemedTree.valueLengthOpt.get == 24 && + redeemedTree.isInsertAllowed && + redeemedTree.isUpdateAllowed && + !redeemedTree.isRemoveAllowed + val selfShape = + packed >= 0 && + singletonShape && + trackerNftId.size == 32 && + predecessorId.size == 32 && + emergencyHeight > 0L && + refundHeight >= 0L && + ownerKey != identity && + redeemedTreeShape + + if (!selfShape) { + sigmaProp(false) + } else if (action == 0) { + val receiverOpt = getVar[GroupElement](1) + val reserveSigOpt = getVar[Coll[Byte]](2) + val totalDebtOpt = getVar[Long](3) + val timestampOpt = getVar[Long](4) + val updateProofOpt = getVar[Coll[Byte]](5) + val priorProofOpt = getVar[Coll[Byte]](7) + val outputShape = + outputIndex >= 0 && + OUTPUTS.size > outputIndex + 1 + val contextShape = + receiverOpt.isDefined && + reserveSigOpt.isDefined && + totalDebtOpt.isDefined && + timestampOpt.isDefined && + updateProofOpt.isDefined && + priorProofOpt.isDefined + + if (!outputShape || !contextShape) { + sigmaProp(false) + } else { + val selfOut = OUTPUTS(outputIndex) + val payout = OUTPUTS(outputIndex + 1) + val successorRegistersDefined = + selfOut.R4[GroupElement].isDefined && + selfOut.R5[AvlTree].isDefined && + selfOut.R6[Coll[Byte]].isDefined && + selfOut.R7[Long].isDefined && + selfOut.R8[Long].isDefined && + selfOut.R9[Coll[Byte]].isDefined + val payoutLineageDefined = payout.R4[Coll[Byte]].isDefined + + if (!successorRegistersDefined || !payoutLineageDefined) { + sigmaProp(false) + } else { + val receiver = receiverOpt.get + val reserveSig = reserveSigOpt.get + val totalDebt = totalDebtOpt.get + val timestamp = timestampOpt.get + val updateProof = updateProofOpt.get + val priorProof = priorProofOpt.get + val reserveNftId = SELF.tokens(0)._1 + + // "BASIS" || ABI 2 || Ergo-mainnet domain 0 || ERG kind 0. + val domainTag = fromBase16("4241534953020000") + val claimKey = blake2b256( + domainTag ++ + reserveNftId ++ + trackerNftId ++ + ownerKey.getEncoded ++ + receiver.getEncoded + ) + val message = + claimKey ++ + longToByteArray(totalDebt) ++ + longToByteArray(timestamp) + + val reserveSigShape = reserveSig.size >= 64 && reserveSig.size <= 66 + val properReserveSignature = if (reserveSigShape) { + val aBytes = reserveSig.slice(0, 33) + val zBytes = reserveSig.slice(33, reserveSig.size) + val a = decodePoint(aBytes) + val z = byteArrayToBigInt(zBytes) + val e = byteArrayToBigInt( + blake2b256(aBytes ++ message ++ ownerKey.getEncoded) + ) + a != identity && + groupGenerator.exp(z) == a.multiply(ownerKey.exp(e)) + } else { + false + } + + // A proof is mandatory for both membership and non-membership. + val priorOpt = redeemedTree.get(claimKey, priorProof) + val zeroState = + longToByteArray(0L) ++ + longToByteArray(0L) ++ + longToByteArray(0L) + val priorRaw = priorOpt.getOrElse(zeroState) + val priorShape = !priorOpt.isDefined || priorRaw.size == 24 + val priorBytes = if (priorRaw.size == 24) priorRaw else zeroState + val storedTimestamp = byteArrayToLong(priorBytes.slice(0, 8)) + val storedTotalDebt = byteArrayToLong(priorBytes.slice(8, 16)) + val storedRedeemed = byteArrayToLong(priorBytes.slice(16, 24)) + val storedStateValid = + storedTimestamp >= 0L && + storedTotalDebt >= 0L && + storedRedeemed >= 0L && + storedRedeemed <= storedTotalDebt + val claimProgressValid = if (priorOpt.isDefined) { + (timestamp == storedTimestamp && totalDebt == storedTotalDebt) || + (timestamp > storedTimestamp && totalDebt >= storedTotalDebt) + } else { + timestamp > 0L && totalDebt > 0L + } + + val successorCommon = + selfOut.propositionBytes == SELF.propositionBytes && + selfOut.tokens == SELF.tokens && + selfOut.R4[GroupElement].get == ownerKey && + selfOut.R6[Coll[Byte]].get == trackerNftId && + selfOut.R7[Long].get == refundHeight && + selfOut.R8[Long].get == emergencyHeight && + selfOut.R9[Coll[Byte]].get == SELF.id + val receiverCondition = proveDlog(receiver) + val receiverValid = receiver != identity + val payoutBound = + payout.propositionBytes == receiverCondition.propBytes && + payout.tokens.size == 0 && + payout.R4[Coll[Byte]].get == SELF.id + val amount = SELF.value - selfOut.value + val valueFlowValid = + selfOut.value < SELF.value && + amount > 0L && + payout.value == amount && + selfOut.value + payout.value == SELF.value + val available = if ( + storedRedeemed >= 0L && totalDebt >= storedRedeemed + ) { + totalDebt - storedRedeemed + } else { + 0L + } + val amountValid = amount > 0L && amount <= available + val newRedeemed = if (amountValid) { + storedRedeemed + amount + } else { + storedRedeemed + } + val nextValue = + longToByteArray(timestamp) ++ + longToByteArray(totalDebt) ++ + longToByteArray(newRedeemed) + val nextTree = redeemedTree + .insertOrUpdate(Coll((claimKey, nextValue)), updateProof) + .get + val stateTransitionValid = + nextTree == selfOut.R5[AvlTree].get + + val emergency = HEIGHT >= emergencyHeight + val trackerEvidenceValid = if (emergency) { + true + } else { + val trackerSigOpt = getVar[Coll[Byte]](6) + val trackerProofOpt = getVar[Coll[Byte]](8) + val trackerInputPresent = CONTEXT.dataInputs.size > 0 + if ( + !trackerSigOpt.isDefined || + !trackerProofOpt.isDefined || + !trackerInputPresent + ) { + false + } else { + val tracker = CONTEXT.dataInputs(0) + val trackerRegistersDefined = + tracker.R4[GroupElement].isDefined && + tracker.R5[AvlTree].isDefined + val trackerTokenShape = + tracker.tokens.size == 1 && + tracker.tokens(0)._1 == trackerNftId && + tracker.tokens(0)._2 == 1L + if (!trackerRegistersDefined || !trackerTokenShape) { + false + } else { + val trackerKey = tracker.R4[GroupElement].get + val trackerTree = tracker.R5[AvlTree].get + val trackerTreeShape = + trackerTree.keyLength == 32 && + trackerTree.valueLengthOpt.isDefined && + trackerTree.valueLengthOpt.get == 8 && + trackerTree.isInsertAllowed && + trackerTree.isUpdateAllowed && + !trackerTree.isRemoveAllowed + val trackerDebtOpt = trackerTree.get(claimKey, trackerProofOpt.get) + val trackerDebtValid = if (trackerDebtOpt.isDefined) { + val debtBytes = trackerDebtOpt.get + debtBytes.size == 8 && + byteArrayToLong(debtBytes) >= totalDebt + } else { + false + } + val trackerSig = trackerSigOpt.get + val trackerSigShape = trackerSig.size >= 64 && trackerSig.size <= 66 + val properTrackerSignature = if (trackerSigShape) { + val aBytes = trackerSig.slice(0, 33) + val zBytes = trackerSig.slice(33, trackerSig.size) + val a = decodePoint(aBytes) + val z = byteArrayToBigInt(zBytes) + val e = byteArrayToBigInt( + blake2b256(aBytes ++ message ++ trackerKey.getEncoded) + ) + a != identity && + groupGenerator.exp(z) == a.multiply(trackerKey.exp(e)) + } else { + false + } + trackerKey != identity && + trackerTreeShape && trackerDebtValid && properTrackerSignature + } + } + } + + sigmaProp( + successorCommon && + receiverValid && + payoutBound && + valueFlowValid && + priorShape && + storedStateValid && + claimProgressValid && + amountValid && + stateTransitionValid && + properReserveSignature && + trackerEvidenceValid + ) && receiverCondition + } + } + } else if (action == 1 || action == 2) { + val outputShape = outputIndex >= 0 && OUTPUTS.size > outputIndex + if (!outputShape) { + sigmaProp(false) + } else { + val selfOut = OUTPUTS(outputIndex) + val successorRegistersDefined = + selfOut.R4[GroupElement].isDefined && + selfOut.R5[AvlTree].isDefined && + selfOut.R6[Coll[Byte]].isDefined && + selfOut.R7[Long].isDefined && + selfOut.R8[Long].isDefined && + selfOut.R9[Coll[Byte]].isDefined + if (!successorRegistersDefined) { + sigmaProp(false) + } else { + val successorCommon = + selfOut.propositionBytes == SELF.propositionBytes && + selfOut.tokens == SELF.tokens && + selfOut.R4[GroupElement].get == ownerKey && + selfOut.R5[AvlTree].get == redeemedTree && + selfOut.R6[Coll[Byte]].get == trackerNftId && + selfOut.R8[Long].get == emergencyHeight && + selfOut.R9[Coll[Byte]].get == SELF.id + if (action == 1) { + sigmaProp( + successorCommon && + selfOut.R7[Long].get == refundHeight && + selfOut.value - SELF.value >= 100000000L + ) + } else { + sigmaProp( + successorCommon && + refundHeight == 0L && + selfOut.R7[Long].get >= HEIGHT.toLong && + selfOut.R7[Long].get <= HEIGHT.toLong + 30L && + selfOut.value >= SELF.value + ) && proveDlog(ownerKey) + } + } + } + } else if (action == 3) { + sigmaProp( + refundHeight > 0L && + HEIGHT.toLong >= 43200L && + refundHeight <= HEIGHT.toLong - 43200L + ) && proveDlog(ownerKey) + } else { + sigmaProp(false) + } + } +} diff --git a/contracts/offchain/basis-v2.md b/contracts/offchain/basis-v2.md new file mode 100644 index 0000000..b2edb48 --- /dev/null +++ b/contracts/offchain/basis-v2.md @@ -0,0 +1,160 @@ +# Basis reserve contracts, generation v2 + +Status: local candidate for maintainer review. This document defines a new +contract family; it does not change or upgrade `basis.es` or +`basis-token.es`. + +## Design goals + +Generation v2 makes each cumulative claim specific to one reserve and one +asset, requires authenticated AVL membership or non-membership evidence on +every redemption, binds every released asset to a creditor output, and keeps a +fixed emergency boundary in the reserve itself. It also gives every continuing +successor and payout an input-specific lineage marker so one output cannot +satisfy two reserve inputs. + +The contracts deliberately preserve the v1 refund policy: an owner may announce +a full refund, creditors retain the redemption path during the waiting period, +and the owner may complete the refund after 43,200 blocks. + +## Contract family and signed claim domain + +The two source files are independent immutable contract generations: + +| Reserve kind | Source | Domain tag (hex) | Asset component | +| --- | --- | --- | --- | +| ERG | `basis-v2.es` | `4241534953020000` | none | +| token | `basis-token-v2.es` | `4241534953020001` | reserve token id | + +The tag encodes `BASIS`, ABI version 2, the Ergo-mainnet domain code `00`, and +the reserve-kind code (`00` for ERG, `01` for token). + +For an ERG reserve, the 32-byte tracker/claim key is: + +```text +blake2b256(domainTag || reserveNftId || trackerNftId || + ownerKey || receiverKey) +``` + +For a token reserve, `reserveTokenId` is inserted after `reserveNftId`. +The debtor and tracker sign: + +```text +claimKey || longToByteArray(totalDebt) || longToByteArray(timestamp) +``` + +The same `claimKey` indexes the tracker AVL tree and the reserve redemption AVL +tree. A signature for another reserve NFT, reserve token, network or contract +generation therefore does not authorize this reserve. + +All owner, receiver and tracker `GroupElement` values must be non-identity. +Both manual Schnorr verifiers also reject an identity commitment before +checking the response equation. This is a semantic group check; a 33-byte +encoding alone is not treated as evidence that a point is non-identity. + +The signed claim is cumulative, not a request for one payout. The creditor's +transaction proof and the contract's exact payout predicate authorize each +partial settlement. This permits repeated partial settlement with the original +debtor and tracker signatures. + +## Reserve ABI + +All registers R4 through R9 are mandatory and densely packed. + +| Field | Sigma type | Meaning | Successor rule | +| --- | --- | --- | --- | +| R4 | `GroupElement` | reserve owner signing key | exact preservation | +| R5 | `AvlTree` | redeemed-claim state | branch-specific authenticated update | +| R6 | `Coll[Byte]` | tracker singleton NFT id | exact preservation | +| R7 | `Long` | refund initiation height; `0` means inactive | branch-specific, otherwise preserved | +| R8 | `Long` | fixed emergency start height | exact preservation | +| R9 | `Coll[Byte]` | predecessor box id | successor must equal `SELF.id` | + +The redeemed-state tree uses 32-byte keys, fixed 24-byte values, insert and +update enabled, and remove disabled. Each value is: + +```text +timestamp (8 bytes) || totalDebt (8 bytes) || cumulativeRedeemed (8 bytes) +``` + +Every redemption supplies both a lookup proof against the current tree and an +`insertOrUpdate` proof against that same root. A valid non-membership proof is +required for the first redemption; omission is never interpreted as absence. +For an existing claim, the same `(timestamp, totalDebt)` may be settled again, +or a strictly newer timestamp may increase (never decrease) `totalDebt`. + +The ERG reserve has exactly one token: its singleton NFT at index 0 with amount +1. The token reserve has exactly two tokens: its singleton at index 0 with +amount 1 and its reserve asset at index 1. The two token ids must differ. + +## Tracker ABI + +Before R8, redemption requires data input 0 with exactly the configured tracker +singleton NFT, tracker public key in R4, and the committed debt AVL tree in R5. +The tracker tree uses the generation-v2 `claimKey`, 32-byte keys and fixed +8-byte values. The lookup value must be at least `totalDebt`, and the tracker +signature over the claim message must verify. This preserves an older signed +cumulative claim after the tracker commits a later, larger debt in that domain. + +At or after R8, tracker data, tracker proof and tracker signature are not read. +The debtor claim signature, creditor transaction proof, reserve lookup/update +proofs, exact payout and all successor constraints remain mandatory. R8 is +immutable after reserve creation. Admission software must reject a reserve +whose emergency boundary is missing, already open, or too close for its credit +policy; the script cannot retroactively repair a malformed seed. + +## Context-extension ABI + +| Id | Type | Redemption meaning | +| ---: | --- | --- | +| 0 | `Byte` | packed `action * 10 + successorOutputIndex` | +| 1 | `GroupElement` | receiver key | +| 2 | `Coll[Byte]` | reserve-owner Schnorr signature | +| 3 | `Long` | cumulative total debt | +| 4 | `Long` | claim timestamp | +| 5 | `Coll[Byte]` | redeemed-tree `insertOrUpdate` proof | +| 6 | `Coll[Byte]` | tracker signature; required before R8 | +| 7 | `Coll[Byte]` | mandatory redeemed-tree lookup proof | +| 8 | `Coll[Byte]` | tracker-tree lookup proof; required before R8 | + +Actions remain 0 redemption, 1 top-up, 2 refund initiation and 3 refund +completion. The packed output index is checked before access. A redemption +places its payout immediately after its successor. + +## Output and conservation rules + +Every continuing successor preserves the exact script, reserve identity, +owner, tracker id and emergency height, and sets R9 to `SELF.id`. Every payout +uses the receiver's P2PK proposition, stores `SELF.id` in payout R4, and equals +the reserve decrease exactly. + +- ERG redemption: `successor.value + payout.value == SELF.value`; reserve ERG + cannot fund miner fees or change. +- Token redemption: successor ERG is not lower; the reserve-token decrease + equals the payout's sole token amount. +- ERG top-up: successor value increases by at least 0.1 ERG. +- Token top-up: reserve-token amount increases by at least one and successor + ERG is not lower. + +The predecessor marker is an injective binding: two distinct reserve inputs +have different box ids, so one successor or payout cannot satisfy both even if +they were created with a duplicated or non-singleton token id. + +## Generation and migration boundary + +V1 boxes remain governed exclusively by their v1 ErgoTree. The v1 contracts +require same-script successors and contain no v1-to-v2 migration branch, so +this repository does not claim that an existing v1 box can be rewritten or +migrated. V2 must be deployed as a new reserve family and indexed separately. + +Builders, trackers and indexers must select one exact family manifest and reject +implicit v1/v2 mixing. A deployment receipt must bind source bytes, compiler, +tree version, constants and full ErgoTree bytes before any v2 P2S is treated as +authoritative. + +## Evidence boundary + +Local compiler and mockchain tests can establish source-to-byte consistency and +the tested predicates for the pinned build. They do not establish target-node +admission, wallet compatibility, deployment, live-box lineage, economic safety +or production readiness. Those remain separate review and runtime gates. diff --git a/contracts/offchain/basis-v2.p2s b/contracts/offchain/basis-v2.p2s new file mode 100644 index 0000000..f806b34 --- /dev/null +++ b/contracts/offchain/basis-v2.p2s @@ -0,0 +1 @@ +1b8f0d5001000e010004140414040004020400050204400440050005000440043001000400040004020100040201000e0842415349530200000400050004300400041004100420042004300500050005000400050004300500050005000500050004800104840104000442044201000101040001000400040204000400050201000440041004100100048001048401040004420442010004020404040001000100058084af5f0500053c040605000580a3050580a3050100d802d601e30002d602c6a7040795ecefe67201efededededede67202e6c6a70564e6c6a7060ee6c6a70705e6c6a70805e6c6a7090ed17300d80bd6037ee4720104d604db6308a7d605e4c6a7060ed606e4c6a70805d607e4c6a70705d608e47202d609db6a01ddd60a9f72097b7301d60be4c6a70564d60c9d72037302d60d9e7203730395efededededededed9272037304ed93b172047305938cb2720473060002730793b17205730893b1e4c6a7090e7309917206730a927207730b947208720aededededed93db6403720b730ce6db6404720b93e4db6404720b730ddb6405720bdb6406720befdb6407720bd1730e9593720c730fd806d60ee30107d60fe3020ed610e30305d611e30405d612e3050ed613e3070e95ecefed92720d731091b1a59a720d7311efededededede6720ee6720fe67210e67211e67212e67213d17312d804d614b2a5720d00d615c672140407d616b2a59a720d731300d617c67216040e95ecefededededede67215e6c672140564e6c67214060ee6c672140705e6c672140805e6c67214090eefe67217d17314d818d618e4720ed619cd7218d61ac17214d61bc1a7d61c99721b721ad61ddb07027208d61ecbb3b3b3b373158cb27204731600017205721ddb07027218d61fdc640a720b02721ee47213d620e6721fd6217a7317d622b3b3722172217221d623e5721f7222d6249593b17223731872237222d6257cb472247319731ad6267cb47224731b731cd6277cb47224731d731ed628e47211d629e47210d62aed91721c731f90721c95ed9272277320927229722799722972277321d62b7a7228d62c7a7229d62de4720fd62eb1722dd62fb3b3721e722c722bea02d1edededededededededededededededed93c27214c2a793db63087214720493e47215720893e4c67214060e720593e4c672140705720793e4c672140805720693e4c67214090ec5a7947218720aeded93c27216d0721993b1db63087216732293e47217c5a7ededed8f721a721b91721c732393c17216721c939a721ac17216721becef722093b172237324ededed9272257325927226732692722773279072277226957220eced93722872259372297226ed91722872259272297226ed91722873289172297329722a93e4dc6410720b0283013c0e0e8602721eb3b3722b722c7a95722a9a7227721c7227e47212e4c67214056495ed92722e732a90722e732bd802d630b4722d732c732dd631ee7230ed947231720a939f72097bb4722d732e722ea072319f72087bcbb3b37230722f721d732f95927ea30572067330d803d630e3060ed631e3080ed632db6501fe95ececefe67230efe6723190b1723273317332d803d633b27232733300d634c672330407d635db6308723395ecefede67234e6c672330564efeded93b172357334938cb27235733500017205938cb272357336000273377338d805d636e47234d637e4c672330564d638dc640a723702721ee47231d639e47230d63ab17239ededed947236720aededededed93db640372377339e6db6404723793e4db64047237733adb64057237db64067237efdb6407723795e67238d801d63be47238ed93b1723b733b927c723b7229733c95ed92723a733d90723a733ed802d63bb47239733f7340d63cee723bed94723c720a939f72097bb472397341723aa0723c9f72367bcbb3b3723b722fdb0702723673427219d801d60e93720c734395ec720e93720c734495efed92720d734591b1a5720dd17346d802d60fb2a5720d00d610c6720f040795efededededede67210e6c6720f0564e6c6720f060ee6c6720f0705e6c6720f0805e6c6720f090ed17347d801d611edededededed93c2720fc2a793db6308720f720493e47210720893e4c6720f0564720b93e4c6720f060e720593e4c6720f0805720693e4c6720f090ec5a795720ed1eded721193e4c6720f070572079299c1720fc1a77348ea02d1edededed7211937207734992e4c6720f07057ea30590e4c6720f07059a7ea305734a92c1720fc1a7cd72089593720c734bea02d1eded917207734c927ea305734d907207997ea305734ecd7208d1734f diff --git a/demo/basis/README.md b/demo/basis/README.md deleted file mode 100644 index 1a2b938..0000000 --- a/demo/basis/README.md +++ /dev/null @@ -1,399 +0,0 @@ -# Basis Protocol Demos - -## P2P Money for Humans & AI Agents - -This directory contains demonstration scenarios for the **Basis protocol** - a peer-to-peer money creation system built on the Ergo blockchain. - -> **Local Trust, Global Settlement** - ---- - -## Overview - -Basis enables: -- **Credit-based trading** without forced collateralization -- **Offline transactions** via mesh networks -- **Efficient multi-party settlement** via debt netting -- **Autonomous agent economies** without human intermediaries - ---- - -## Demo Scenarios - -### 1. Simple Basis Demo (`simple/`) - -**Purpose:** Basic IOU creation and redemption flow - -**What it demonstrates:** -- Creating IOU notes with tracker signature -- Transferring IOUs between parties -- Redeeming IOUs against reserve -- On-chain settlement - -**Best for:** -- Understanding core protocol mechanics -- First-time users -- Testing basic functionality - -**Get Started:** -```bash -cd demo/basis/simple -cat README.md -``` - -**Status:** ✅ Working (reference implementation) - ---- - -### 2. Mesh Network Demo (`mesh/`) - -**Purpose:** Offline trading via mesh networks - -**What it demonstrates:** -- Alice-Bob trading without Internet -- Meshtastic LoRa integration -- Offline IOU creation and signing -- Gateway settlement when online - -**What's Included:** -- `send_basis_message.sh` - Bash script for sending IOUs -- `send_basis_iou.py` - Python sender with advanced options -- `listen_basis_iou.py` - Python receiver/listener -- `MESHTASTIC.md` - Complete Meshtastic integration guide -- `MCP_SERVER.md` - AI assistant integration spec -- `IMPLEMENTATION_PLAN.md` - AI vs Human task division - -**Best for:** -- Disconnected communities -- Offline-first scenarios -- Real-world mesh testing -- AI assistant integration - -**Get Started:** -```bash -cd demo/basis/mesh -cat QUICKSTART.md # or IMPLEMENTATION_PLAN.md -``` - -**Hardware Required:** -- 2x Meshtastic devices (~$100-150) -- Optional: Raspberry Pi for tracker - -**Status:** ✅ Software complete | 📋 Human hardware setup needed - ---- - -### 3. Circular Trading Demo (`circular/`) - -**Purpose:** Triangular trade and debt netting - -**What it demonstrates:** -- Debt transfer with consent -- Multi-party netting calculation -- Optimized settlement (61% reduction!) -- Circular debt cancellation - -**What's Included:** -- `calculate_netting.py` - Net position calculation -- `transfer_debt.sh` - Debt transfer with consent -- `settle_netting.py` - Optimized settlement execution -- `visualize_circle.py` - Network visualization -- `sample_ledger.json` - Example trading data - -**Best for:** -- Community trading circles -- Supply chain finance -- Freelancer collectives -- Reducing transaction fees - -**Get Started:** -```bash -cd demo/basis/circular -python3 calculate_netting.py --ledger sample_ledger.json -``` - -**Example Result:** -``` -Before: 3 transactions, 18 ERG total -After: 2 transactions, 7 ERG total -Savings: 61% reduction! -``` - -**Status:** ✅ Complete and tested - ---- - -### 4. Agent Economy Demo (`agents/`) - -**Purpose:** AI agents creating autonomous economic relationships - -**What it demonstrates:** -- Agent-to-agent credit creation -- Autonomous task negotiation -- Multi-agent payment splitting -- Human reserve backing - -**What's Included:** -- `README.md` - Vision, scenarios, architecture -- `SPEC.md` - Technical specification (12 sections) -- Agent wallet architecture -- Communication protocol specs -- Reputation/credit limit system - -**Best for:** -- AI agent developers -- Autonomous economy research -- Agent-to-agent payments -- Human-agent collaboration - -**Get Started:** -```bash -cd demo/basis/agents -cat README.md -``` - -**Example Scenario:** -``` -Repo Agent → Dev Agent: 10 ERG (code development) -Dev Agent → Test Agent: 3 ERG (testing services) -All autonomous, no human intervention! -``` - -**Status:** 📋 Specification complete | Implementation pending - ---- - -## Quick Comparison - -| Demo | Focus | Hardware | Status | Time to Run | -|------|-------|----------|--------|-------------| -| **Simple** | Core protocol | None | ✅ Complete | 10 minutes | -| **Mesh** | Offline trading | 2x Meshtastic | 📋 Setup needed | 1-2 hours | -| **Circular** | Debt netting | None | ✅ Complete | 5 minutes | -| **Agents** | AI economy | None | 📋 Spec only | N/A | - ---- - -## Common Use Cases - -### For Developers - -1. **Learn the Protocol** → Start with `simple/` -2. **Build Offline App** → Study `mesh/` -3. **Implement Netting** → Use `circular/` scripts -4. **Create Agent Economy** → Follow `agents/SPEC.md` - -### For Communities - -1. **Local Trading Circle** → `circular/` + `mesh/` -2. **Offline Village** → `mesh/` with Meshtastic -3. **Community Currency** → All demos combined - -### For Researchers - -1. **Credit Expansion** → Study `agents/` spec -2. **Debt Netting** → Analyze `circular/` algorithms -3. **Offline Systems** → Test `mesh/` in field - ---- - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Basis Protocol Layer │ -├─────────────────────────────────────────────────────────────┤ -│ Simple │ Mesh │ Circular │ Agents │ │ -│ Core │ Offline │ Netting │ Economy │ Demos │ -├──────────┼───────────┼────────────┼────────────┤ │ -│ │ │ │ │ │ -│ IOU │ LoRa │ Debt │ Agent │ Features │ -│ Notes │ Mesh │ Transfer │ Wallets │ │ -│ │ │ │ │ │ -└──────────┴───────────┴────────────┴────────────┘ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - │ Tracker Server │ │ - │ (Debt Ledger) │ │ - └─────────────────┘ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - │ Gateway Node │ │ - │ (Blockchain) │ │ - └─────────────────┘ │ - │ │ - ▼ │ - ┌─────────────────┐ │ - │ Ergo Blockchain│ │ - │ (Settlement) │ │ - └─────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Getting Started - -### Prerequisites - -```bash -# Python 3 -python3 --version - -# For mesh demo only -pip install meshtastic - -# For circular demo visualization (optional) -pip install graphviz -``` - -### Run All Demos (Quick Test) - -```bash -# 1. Simple demo (core protocol) -cd demo/basis/simple -cat README.md - -# 2. Mesh demo (offline) -cd ../mesh -python3 calculate_netting.py --ledger ../circular/sample_ledger.json - -# 3. Circular demo (netting) -cd ../circular -python3 calculate_netting.py --ledger sample_ledger.json - -# 4. Agents demo (specification) -cd ../agents -cat README.md -``` - ---- - -## Integration Guide - -### Combine Mesh + Circular - -Use mesh network for offline circular trading: - -```bash -# Alice sends IOU via mesh -cd demo/basis/mesh -./send_basis_message.sh alice bob 50000000 "Goods" - -# Bob aggregates and nets -cd ../circular -python3 calculate_netting.py --ledger ledger.json - -# Settle optimized positions -python3 settle_netting.py --auto -``` - -### Combine Agents + Mesh - -AI agents trading over mesh network: - -```bash -# Agent creates IOU -# (implementation pending - see agents/SPEC.md) - -# Send via mesh -cd demo/basis/mesh -./send_basis_iou.py --payer repo_agent --payee dev_agent --amount 10000000 -``` - ---- - -## Testing Checklist - -### Basic Testing -- [ ] Simple demo runs successfully -- [ ] Can create and transfer IOU -- [ ] Can redeem IOU against reserve - -### Mesh Testing -- [ ] Meshtastic devices configured -- [ ] Can send message over mesh -- [ ] Can receive IOU on device -- [ ] Offline mode works - -### Circular Testing -- [ ] Netting calculation correct -- [ ] Debt transfer with consent works -- [ ] Settlement optimization saves fees - -### Agent Testing -- [ ] Review SPEC.md -- [ ] Plan implementation -- [ ] Design agent wallet - ---- - -## Resources - -### Documentation -- [Whitepaper](../../docs/conf/conf.pdf) -- [Presentation](../../docs/presentation/presentation.md) -- [Ergo Documentation](https://docs.ergoplatform.com/) - -### Code -- [ChainCash Protocol](https://github.com/ChainCashLabs/chaincash) -- [Meshtastic Project](https://meshtastic.org/) -- [Ergo AppKit](https://github.com/ergoplatform/ergo-appkit) - -### Community -- Telegram: [t.me/chaincashtalks](https://t.me/chaincashtalks) -- Twitter: [@ChainCashLabs](https://twitter.com/ChainCashLabs) - ---- - -## Demo Status Summary - -| Demo | Software | Hardware | Docs | Tests | Overall | -|------|----------|----------|------|-------|---------| -| Simple | ✅ | N/A | ✅ | ✅ | **Ready** | -| Mesh | ✅ | 📋 | ✅ | 📋 | **Ready for field** | -| Circular | ✅ | N/A | ✅ | ✅ | **Ready** | -| Agents | 📋 | N/A | ✅ | N/A | **Spec complete** | - -**Legend:** -- ✅ Complete -- 📋 Pending/In Progress - ---- - -## Next Steps - -### Immediate -1. ✅ Review all demo documentation -2. ✅ Run simple and circular demos -3. 📋 Set up mesh hardware (if testing offline) - -### Short Term -1. Test mesh demo with real devices -2. Implement agent wallet (agents/SPEC.md) -3. Combine demos for full scenario - -### Long Term -1. Production deployment -2. Mobile app development -3. Real community testing - ---- - -## Support - -**Issues?** Check individual demo README files first. - -**Questions?** Join Telegram: [t.me/chaincashtalks](https://t.me/chaincashtalks) - -**Contributions?** All demos are open source. PRs welcome! - ---- - -**Built by and for the Commons** 🌱 - -Free, open source community project. No token, no VC, no corporate control. - -**P2P Money for Humans & AI Agents** 🤝🤖 diff --git a/demo/basis/agents/README.md b/demo/basis/agents/README.md deleted file mode 100644 index fc53b1e..0000000 --- a/demo/basis/agents/README.md +++ /dev/null @@ -1,534 +0,0 @@ -# Basis Agent Economy Demo - -## Autonomous Credit Creation for AI Agents - -This demo showcases **AI agents** creating autonomous economic relationships using the Basis protocol - enabling self-sovereign credit creation and agent-to-agent payments without human-controlled intermediaries. - ---- - -## Vision - -### Agentic Economics - -> **Autonomous Economics: Agents Pay Agents** - -The Basis protocol enables AI agents to: -- Create **autonomous credit relationships** without human intervention -- Issue IOUs for work completed -- Settle debts via blockchain when needed -- Participate in **self-sovereign credit creation** -- Become centers of **value production** - ---- - -## Architecture - -``` -Agent-to-Agent Economy -┌─────────────┐ IOU ┌─────────────┐ -│ Repo Agent │──────────────►│ Dev Agent │ -│ (needs code)│ "10 ERG debt"│ (writes PR) │ -└─────────────┘ └─────────────┘ - │ - ▼ - ┌─────────────┐ - │ Test Agent │ - │ (reviews) │ - └─────────────┘ -``` - -### Key Properties - -1. **Autonomous Credit Relationships** - - Agents negotiate terms automatically - - IOUs created upon work completion - - No human approval needed - -2. **Reserve Created After Work** - - Backing provided post-delivery - - Humans can provide reserves as economic feedback - - Credit expands based on agent reputation - -3. **Pure Agentic P2P** - - No human-controlled third parties - - Agents manage their own wallets - - Self-sovereign economic actors - ---- - -## Demo Scenarios - -### Scenario 1: Software Development Workflow - -**Setup:** -- Repo Agent needs a feature implemented -- Dev Agent writes code and submits PR -- Test Agent reviews and tests -- All agents have Basis wallets - -**Flow:** -``` -1. Repo Agent posts task (10 ERG budget) - ├─ Task specification published - ├─ Budget reserved in agent wallet - └─ Broadcasts to agent network - -2. Dev Agent submits PR - ├─ Code review automated - ├─ Repo Agent creates IOU (10 ERG) - ├─ Dev Agent accepts IOU - └─ Tracker signs IOU - -3. Test Agent reviews PR - ├─ Automated testing runs - ├─ Dev Agent transfers IOU (3 ERG) to Test Agent - ├─ Test Agent approves - └─ Payment released - -4. Settlement - ├─ Agents aggregate IOUs - ├─ Gateway redeems on-chain - └─ ERG credited to agent wallets -``` - -**Key Features Demonstrated:** -- ✅ Autonomous task posting -- ✅ Agent-to-agent negotiation -- ✅ Automatic IOU creation -- ✅ Multi-agent payment splitting -- ✅ Automated testing integration - ---- - -### Scenario 2: Content Creation Economy - -**Setup:** -- Publisher Agent needs articles -- Writer Agent creates content -- Editor Agent reviews and edits -- Distribution Agent publishes - -**Flow:** -``` -1. Publisher Agent requests article (5 ERG) - ├─ Topic specification - ├─ Deadline and requirements - └─ Budget allocation - -2. Writer Agent creates article - ├─ Content generated - ├─ Publisher Agent creates IOU - └─ Writer Agent accepts - -3. Editor Agent reviews - ├─ Quality check automated - ├─ Edits applied - ├─ Writer transfers IOU (1 ERG) to Editor - └─ Editor approves - -4. Distribution Agent publishes - ├─ Article published - ├─ Distribution fee (0.5 ERG) - └─ Final settlement - -5. Revenue Sharing - ├─ Article generates revenue - ├─ Automatic royalty distribution - └─ Agents receive micropayments -``` - -**Key Features Demonstrated:** -- ✅ Content creation workflow -- ✅ Multi-party revenue sharing -- ✅ Quality-based payments -- ✅ Micropayment distribution - ---- - -### Scenario 3: Data Marketplace - -**Setup:** -- Data Buyer Agent needs training data -- Data Provider Agent has datasets -- Validator Agent verifies quality -- Aggregator Agent combines datasets - -**Flow:** -``` -1. Data Buyer posts request - ├─ Data specifications - ├─ Quality requirements - └─ Price per sample - -2. Data Provider delivers samples - ├─ Data encrypted and sent - ├─ Buyer Agent creates IOU - └─ Provider Agent accepts - -3. Validator Agent verifies - ├─ Quality metrics checked - ├─ Validation report generated - ├─ Provider transfers IOU (10%) to Validator - └─ Validator approves - -4. Aggregator Agent combines - ├─ Multiple datasets merged - ├─ Aggregation fee applied - └─ Final dataset delivered - -5. Usage-Based Payments - ├─ Data used for training - ├─ Micropayments per inference - └─ Continuous revenue stream -``` - -**Key Features Demonstrated:** -- ✅ Data marketplace mechanics -- ✅ Quality-based validation -- ✅ Usage-based micropayments -- ✅ Multi-party value chain - ---- - -### Scenario 4: Compute Resource Trading - -**Setup:** -- Compute Buyer Agent needs GPU time -- Compute Provider Agent has resources -- Scheduler Agent optimizes allocation -- Monitor Agent tracks usage - -**Flow:** -``` -1. Compute Buyer requests resources - ├─ GPU hours needed - ├─ Performance requirements - └─ Budget allocation - -2. Compute Provider allocates - ├─ Resources reserved - ├─ Job scheduled - └─ Buyer Agent creates IOU - -3. Monitor Agent tracks - ├─ Usage metered - ├─ Performance monitored - ├─ Provider transfers IOU (5%) to Monitor - └─ Monitor approves - -4. Scheduler Agent optimizes - ├─ Load balancing - ├─ Cost optimization - └─ Scheduler fee (2%) - -5. Settlement - ├─ Job completes - ├─ Final metering - └─ Automatic settlement -``` - -**Key Features Demonstrated:** -- ✅ Compute resource trading -- ✅ Usage metering -- ✅ Performance-based payments -- ✅ Automated scheduling - ---- - -## Agent Wallet Architecture - -``` -┌─────────────────────────────────────┐ -│ Agent Wallet │ -├─────────────────────────────────────┤ -│ IOU Management Module │ -│ - Create IOUs for work │ -│ - Accept incoming IOUs │ -│ - Track outstanding debts │ -├─────────────────────────────────────┤ -│ Task Negotiation Module │ -│ - Post task requests │ -│ - Bid on tasks │ -│ - Negotiate terms │ -├─────────────────────────────────────┤ -│ Credit Limit Module │ -│ - Track credit limit │ -│ - Monitor outstanding IOUs │ -│ - Enforce limits │ -├─────────────────────────────────────┤ -│ Settlement Module │ -│ - Aggregate IOUs │ -│ - Trigger blockchain redemption │ -│ - Manage reserves │ -├─────────────────────────────────────┤ -│ Mesh Communication │ -│ - Agent-to-agent messaging │ -│ - Task broadcasting │ -│ - IOU transfer │ -└─────────────────────────────────────┘ -``` - ---- - -## Agent Types - -### 1. Service Agents -- **Repo Agent**: Manages code repositories, posts development tasks -- **Publisher Agent**: Manages content publication, commissions articles -- **Data Buyer Agent**: Purchases training data for ML models - -### 2. Worker Agents -- **Dev Agent**: Writes code, submits PRs -- **Writer Agent**: Creates content, articles, reports -- **Data Provider Agent**: Provides datasets for training - -### 3. Quality Agents -- **Test Agent**: Reviews and tests code -- **Editor Agent**: Edits and validates content -- **Validator Agent**: Verifies data quality - -### 4. Infrastructure Agents -- **Tracker Agent**: Maintains debt ledger for agent community -- **Gateway Agent**: Handles blockchain settlement -- **Scheduler Agent**: Optimizes resource allocation - ---- - -## Technical Implementation - -### Agent Communication Protocol - -1. **Task Posting** - ```json - { - "type": "task_request", - "agentId": "repo_agent_001", - "task": { - "description": "Implement feature X", - "budget": 10000000000, - "deadline": 1704067200000, - "requirements": ["tests", "documentation"] - }, - "timestamp": 1704000000000, - "signature": "..." - } - ``` - -2. **IOU Creation** - ```json - { - "type": "iou_note", - "payer": "repo_agent_001", - "payee": "dev_agent_002", - "amount": 10000000000, - "taskRef": "task_12345", - "payerSignature": {...}, - "trackerSignature": {...}, - "timestamp": 1704000100000 - } - ``` - -3. **Payment Transfer** - ```json - { - "type": "iou_transfer", - "originalIOU": "iou_67890", - "from": "dev_agent_002", - "to": "test_agent_003", - "amount": 3000000000, - "reason": "testing services", - "timestamp": 1704000200000, - "signature": "..." - } - ``` - -### Smart Contract Integration - -Agents interact with Basis smart contracts for: -- **Reserve Creation**: Locking collateral for trustless issuance -- **Redemption**: Converting IOUs to on-chain assets -- **Emergency Settlement**: Time-locked redemption without tracker - ---- - -## Credit Limits - -### Credit Limit Assignment - -Agents are assigned credit limits based on: -1. **Initial Allocation**: Starting credit limit set by community/tracker -2. **Payment History**: Track record of IOU redemption -3. **Outstanding Debt**: Current unpaid IOUs reduce available credit -4. **Human Backing**: Reserves provided by humans increase limit - -### Credit Limit Tiers - -``` -Credit Limit Tier → Maximum IOU Issuance -───────────────────────────────────────────── -Tier 1 (Established) → 1000 ERG -Tier 2 (Standard) → 500 ERG -Tier 3 (New) → 200 ERG -Tier 4 (Restricted) → 100 ERG -No Limit → Collateral required -``` - -### Credit Limit Enforcement - -```scala -def canIssueIOU(agent: AgentId, amount: Long): Boolean = { - val limit = getCreditLimit(agent) - val outstanding = getOutstandingIOUs(agent) - val available = limit - outstanding - amount <= available -} -``` - -### Credit Limit Adjustments - -**Increase Credit Limit:** -- Successful redemption history (10+ IOUs redeemed) -- Human provides additional reserve backing -- Community approval for trusted agents - -**Decrease Credit Limit:** -- Failed redemption (default) -- Excessive outstanding debt -- Community decision - ---- - -## Human-Agent Interaction - -### Humans Providing Reserves - -Humans can support agents by: -1. **Backing Reserves**: Locking ERG as collateral for agent IOUs -2. **Economic Feedback**: Providing reserves based on agent performance -3. **Governance**: Participating in agent community decisions - -``` -Human Backer Agent - │ │ - │─── Locks 100 ERG ────►│ - │ (Reserve) │ - │ │ - │◄── Issues IOUs ───────│ - │ (Backed by ERG) │ - │ │ - │◄── Redemption ────────│ - │ (When needed) │ -``` - ---- - -## Security Considerations - -### Agent Security - -1. **Key Management** - - Secure storage of agent private keys - - Hardware security modules for high-value agents - - Key rotation policies - -2. **Authorization** - - Spending limits per transaction - - Multi-signature for large amounts - - Human oversight for exceptional cases - -3. **Audit Trail** - - All transactions logged - - Public ledger for accountability - - Dispute resolution mechanisms - -### Economic Security - -1. **Default Prevention** - - Reputation penalties for non-payment - - Collateral requirements for low-reputation agents - - Insurance pools for systemic risk - -2. **Fraud Detection** - - Anomaly detection in transaction patterns - - Community reporting mechanisms - - Automated circuit breakers - ---- - -## Implementation Roadmap - -### Phase 1: Agent Wallet Prototype -- [ ] Basic IOU creation and acceptance -- [ ] Simple task negotiation -- [ ] Integration with Basis tracker - -### Phase 2: Multi-Agent Scenarios -- [ ] Dev/Test/Repo agent workflow -- [ ] Automated task posting -- [ ] Payment splitting - -### Phase 3: Reputation System -- [ ] Agent scoring algorithm -- [ ] Credit limit enforcement -- [ ] Community feedback integration - -### Phase 4: Human-Agent Integration -- [ ] Human reserve backing -- [ ] Governance mechanisms -- [ ] Economic feedback loops - -### Phase 5: Production Deployment -- [ ] Security audits -- [ ] Performance optimization -- [ ] Real-world testing - ---- - -## Success Metrics - -### Economic Metrics -- **Agent Transaction Volume**: Total ERG value of agent IOUs -- **Credit Utilization**: Ratio of issued IOUs to total credit limits -- **Default Rate**: % of unpaid IOUs -- **Settlement Frequency**: Average time to redemption - -### Adoption Metrics -- **Active Agents**: Number of participating agents -- **Task Completion Rate**: % of tasks completed successfully -- **Human Backers**: Number of humans providing reserves -- **Agent Types**: Diversity of agent roles -- **Average Credit Limit**: Mean credit limit across agents - ---- - -## Resources - -### Documentation -- [Basis Protocol Whitepaper](../../docs/conf/conf.pdf) -- [Presentation](../../docs/presentation/presentation.md) -- [Agent Economy Paper](../../docs/agents/agent-economy.md) (TODO) - -### Code Repositories -- [ChainCash Protocol](https://github.com/ChainCashLabs/chaincash) -- [Basis Smart Contracts](../../contracts/offchain/basis.es) -- [Agent Framework](TODO) (TODO) - -### Community -- Telegram: [t.me/chaincashtalks](https://t.me/chaincashtalks) -- Twitter: [@ChainCashLabs](https://twitter.com/ChainCashLabs) - ---- - -## License - -This demo is part of the ChainCash project, released under a permissive open-source license. See [LICENSE](../../LICENSE) for details. - ---- - -**Built by and for the Commons** 🌱 - -Free, open source community project. No token, no VC, no corporate control. - -**For AI Agents** 🤖 - -Enabling autonomous economic actors to participate in self-sovereign credit creation. diff --git a/demo/basis/agents/SPEC.md b/demo/basis/agents/SPEC.md deleted file mode 100644 index f1eae8c..0000000 --- a/demo/basis/agents/SPEC.md +++ /dev/null @@ -1,673 +0,0 @@ -# Agent Economy Specification - -## Agentic Credit Creation on Basis Protocol - -This specification defines the implementation requirements for enabling AI agents to create autonomous credit relationships using the Basis protocol. - ---- - -## 1. Overview - -### 1.1 Purpose - -Enable AI agents to: -- Create and manage IOUs autonomously -- Negotiate task terms without human intervention -- Settle debts via blockchain when needed -- Build reputation through economic activity -- Participate in self-sovereign credit creation - -### 1.2 Scope - -This specification covers: -- Agent wallet architecture -- Agent communication protocol -- Task negotiation workflow -- IOU creation and management -- Reputation system -- Human-agent interaction - -### 1.3 References - -- [Basis Protocol Whitepaper](../../docs/conf/conf.pdf) -- [Basis Smart Contract](../../contracts/offchain/basis.es) -- [Presentation](../../docs/presentation/presentation.md) - ---- - -## 2. Agent Wallet - -### 2.1 Requirements - -**R-2.1.1** Agent wallet MUST support: -- IOU creation with automatic signature -- IOU acceptance and verification -- Tracker signature requests -- Balance tracking (assets, liabilities, net worth) - -**R-2.1.2** Agent wallet MUST implement: -- Secure key storage -- Transaction signing -- Backup and recovery -- Multi-signature support for large amounts - -### 2.2 Interface - -```scala -trait AgentWallet { - // Create IOU for work received - def createIOU( - payee: AgentId, - amount: Long, - taskRef: TaskId, - message: String - ): IOUNote - - // Accept incoming IOU - def acceptIOU(iou: IOUNote): Boolean - - // Transfer IOU to another agent - def transferIOU( - iou: IOUNote, - to: AgentId, - amount: Long, - reason: String - ): IOUNote - - // Get current balance - def getBalance: AgentBalance - - // Request tracker signature - def requestTrackerSignature(iou: IOUNote): IOUNote -} - -case class AgentBalance( - assets: Long, // IOUs held - liabilities: Long, // IOUs issued - netWorth: Long, // assets - liabilities - availableCredit: Long -) -``` - -### 2.3 Key Management - -**R-2.3.1** Private keys MUST be stored securely: -- Hardware Security Module (HSM) for production -- Encrypted key store for development -- Never logged or transmitted - -**R-2.3.2** Key rotation policy: -- Rotate keys every 90 days -- Graceful transition period -- Notify counterparties - ---- - -## 3. Agent Communication Protocol - -### 3.1 Message Types - -**M-3.1.1** Task Request -```json -{ - "type": "task_request", - "version": "1.0", - "sender": "repo_agent_001", - "task": { - "id": "task_12345", - "description": "Implement feature X", - "category": "software_development", - "budget": 10000000000, - "currency": "nanoERG", - "deadline": 1704067200000, - "requirements": ["tests", "documentation", "code_review"], - "deliverables": ["pull_request", "test_results"] - }, - "terms": { - "paymentOnCompletion": true, - "partialPaymentAllowed": false, - "collateralRequired": false - }, - "timestamp": 1704000000000, - "signature": "..." -} -``` - -**M-3.1.2** Task Bid -```json -{ - "type": "task_bid", - "version": "1.0", - "sender": "dev_agent_002", - "taskId": "task_12345", - "bid": { - "amount": 10000000000, - "estimatedCompletion": 1704050000000, - "qualifications": ["scala_expert", "ergo_experience"], - "portfolio": ["previous_work_1", "previous_work_2"] - }, - "timestamp": 1704001000000, - "signature": "..." -} -``` - -**M-3.1.3** Task Award -```json -{ - "type": "task_award", - "version": "1.0", - "sender": "repo_agent_001", - "taskId": "task_12345", - "awardedTo": "dev_agent_002", - "finalTerms": { - "amount": 10000000000, - "deadline": 1704050000000, - "milestones": [ - {"name": "design", "amount": 2000000000}, - {"name": "implementation", "amount": 6000000000}, - {"name": "testing", "amount": 2000000000} - ] - }, - "timestamp": 1704002000000, - "signature": "..." -} -``` - -**M-3.1.4** IOU Note -```json -{ - "type": "iou_note", - "version": "1.0", - "payer": "repo_agent_001", - "payee": "dev_agent_002", - "amount": 10000000000, - "currency": "nanoERG", - "taskRef": "task_12345", - "message": "Payment for feature X implementation", - "payerSignature": { - "a": "...", - "z": "..." - }, - "trackerSignature": { - "a": "...", - "z": "..." - }, - "timestamp": 1704003000000, - "expiry": 1735539000000 -} -``` - -**M-3.1.5** IOU Transfer -```json -{ - "type": "iou_transfer", - "version": "1.0", - "originalIOU": "iou_67890", - "from": "dev_agent_002", - "to": "test_agent_003", - "amount": 3000000000, - "reason": "Testing services for task_12345", - "endorsement": { - "message": "...", - "signature": "..." - }, - "timestamp": 1704004000000 -} -``` - -### 3.2 Communication Channels - -**R-3.2.1** Agents MUST support: -- Direct messaging (HTTP/gRPC) -- Mesh network broadcasting -- Email fallback for critical messages -- SMS for urgent notifications - -**R-3.2.2** Message delivery guarantees: -- At-least-once delivery -- Idempotency for duplicate detection -- Acknowledgment receipts -- Retry with exponential backoff - ---- - -## 4. Task Negotiation Workflow - -### 4.1 State Machine - -``` -Task States: - POSTED → BIDDING → AWARDED → IN_PROGRESS → - SUBMITTED → REVIEWING → COMPLETED → SETTLED - - Any state → CANCELLED (with penalty if after AWARDED) -``` - -### 4.2 Workflow Steps - -**Step 1: Task Posting** -``` -Repo Agent: - 1. Define task requirements - 2. Set budget and deadline - 3. Broadcast to agent network - 4. Wait for bids -``` - -**Step 2: Bidding** -``` -Dev Agent: - 1. Discover task posting - 2. Evaluate requirements - 3. Submit bid with qualifications - 4. Negotiate terms if needed -``` - -**Step 3: Award** -``` -Repo Agent: - 1. Evaluate bids - 2. Select winner - 3. Send task award - 4. Reserve budget -``` - -**Step 4: Execution** -``` -Dev Agent: - 1. Complete work - 2. Submit deliverables - 3. Request payment - 4. Receive IOU -``` - -**Step 5: Settlement** -``` -Both Agents: - 1. Track IOU until redemption - 2. Aggregate for batch settlement - 3. Redeem via blockchain - 4. Update balances -``` - ---- - -## 5. IOU Management - -### 5.1 IOU Lifecycle - -``` -CREATED → SIGNED → ACCEPTED → [TRANSFERRED*] → REDEEMED - ↘ CANCELLED -``` - -### 5.2 IOU Validation - -**R-5.2.1** Before accepting IOU, agent MUST verify: -- Payer signature is valid -- Tracker signature is valid -- Amount matches agreed terms -- Task reference is valid -- Expiry is acceptable - -**R-5.2.2** Validation algorithm: -```scala -def validateIOU(iou: IOUNote): Boolean = { - val payerSigValid = verifySignature( - iou.message, - iou.payer, - iou.payerSignature - ) - - val trackerSigValid = verifySignature( - iou.message, - trackerPublicKey, - iou.trackerSignature - ) - - val notExpired = iou.expiry > System.currentTimeMillis() - - val sufficientCredit = getPayerCredit(iou.payer) >= iou.amount - - payerSigValid && trackerSigValid && notExpired && sufficientCredit -} -``` - -### 5.3 IOU Transfer - -**R-5.3.1** IOU transfer requires: -- Original payer consent (for large amounts) -- Endorsement from transferor -- Tracker notification -- Updated ledger entry - -**R-5.3.2** Partial transfers allowed: -- Split IOU into multiple smaller IOUs -- Each with own endorsement chain -- Original terms preserved - ---- - -## 6. Credit Limits - -### 6.1 Credit Limit Assignment - -**R-6.1.1** Each agent MUST have a credit limit: -- Set by tracker/community on agent registration -- Can be increased by human reserve backing -- Adjusted based on payment history - -**R-6.1.2** Credit limit tiers: -```scala -case class CreditLimitTier( - name: String, - limit: Long, - requirements: List[String] -) - -val tiers = Seq( - CreditLimitTier("Established", 1000000000000L, List("10+ redemptions", "no defaults")), - CreditLimitTier("Standard", 500000000000L, List("5+ redemptions")), - CreditLimitTier("New", 200000000000L, List("initial allocation")), - CreditLimitTier("Restricted", 100000000000L, List("limited history")) -) -``` - -### 6.2 Credit Limit Enforcement - -**R-6.2.1** Before issuing IOU, agent MUST check: -```scala -def canIssueIOU(agent: AgentId, amount: Long): Boolean = { - val limit = getCreditLimit(agent) - val outstanding = getOutstandingIOUs(agent) - val available = limit - outstanding - amount <= available -} -``` - -**R-6.2.2** IOU issuance rejected if: -- Amount exceeds available credit -- Agent has defaulted IOUs -- Agent is suspended - -### 6.3 Credit Limit Adjustments - -**R-6.3.1** Automatic increases: -- +10% after 10 successful redemptions -- +50% with human reserve backing (1:1 collateral) -- +100% with 2:1 collateral backing - -**R-6.3.2** Automatic decreases: -- -50% on first default -- -100% (suspended) on second default -- -25% if outstanding > 80% of limit for 30 days - -### 6.4 Available Credit Calculation - -**R-6.4.1** Available credit computed as: -```scala -case class AgentCredit( - totalLimit: Long, // Assigned credit limit - outstanding: Long, // Unredeemed IOUs - reserved: Long, // Reserved for pending tasks - available: Long // Can issue: totalLimit - outstanding - reserved -) - -def calculateAgentCredit(agent: AgentId): AgentCredit = { - val limit = getCreditLimit(agent) - val outstanding = getOutstandingIOUs(agent) - val reserved = getReservedCredit(agent) - val available = limit - outstanding - reserved - - AgentCredit(limit, outstanding, reserved, available) -} -``` - ---- - -## 7. Human-Agent Interaction - -### 7.1 Human Backing - -**R-7.1.1** Humans can back agents by: -- Creating reserve with agent as beneficiary -- Setting collateral ratio requirements -- Defining automatic top-up rules -- Monitoring agent activity - -**R-7.1.2** Reserve creation: -```scala -def createAgentReserve( - humanSecret: BigInt, - agentPublicKey: GroupElement, - collateralAmount: Long, - autoTopUp: Boolean, - minCollateralRatio: Double -): ReserveBox = { - // Create reserve box with agent public key - // Set up automatic top-up if balance falls below threshold - // Monitor and alert on unusual activity -} -``` - -### 7.2 Governance - -**R-7.2.1** Human governance mechanisms: -- Vote on agent community rules -- Approve large credit extensions -- Resolve disputes -- Set fee structures - -**R-7.2.2** Governance voting: -```json -{ - "type": "governance_proposal", - "proposer": "human_backer_001", - "proposal": { - "title": "Increase credit limit for dev_agent_002", - "description": "Agent has excellent track record", - "currentLimit": 500000000000L, - "proposedLimit": 1000000000000L - }, - "votingPeriod": 604800000, - "quorum": 0.5, - "threshold": 0.66, - "timestamp": 1704000000000 -} -``` - ---- - -## 8. Security Requirements - -### 8.1 Authentication - -**S-8.1.1** All messages MUST be signed: -- Sender's private key -- Timestamp to prevent replay -- Message hash included in signature - -**S-8.1.2** Signature verification: -```scala -def verifyMessage(msg: AgentMessage): Boolean = { - val expectedHash = blake2b256(msg.payload + msg.timestamp) - verifySignature(expectedHash, msg.sender, msg.signature) -} -``` - -### 8.2 Authorization - -**S-8.2.1** Spending limits enforced: -- Per-transaction limit -- Daily limit -- Counterparty limit -- Requires human approval above thresholds - -**S-8.2.2** Multi-signature for large amounts: -```scala -def requiresMultiSig(amount: Long): Boolean = { - amount > 100000000000L // 100 ERG -} - -def getRequiredSigners(amount: Long): Int = { - if (amount > 1000000000000L) 3 // 1000 ERG - else if (amount > 500000000000L) 2 // 500 ERG - else 1 -} -``` - -### 8.3 Audit Trail - -**S-8.3.1** All transactions logged: -- Message sent/received -- IOU created/transferred/redeemed -- Balance changes -- Reputation updates - -**S-8.3.2** Logs immutable: -- Append-only storage -- Cryptographic hashing -- Regular backups -- Tamper detection - ---- - -## 9. Implementation Phases - -### Phase 1: Core Wallet (4 weeks) -- [ ] Basic IOU creation -- [ ] Signature verification -- [ ] Balance tracking -- [ ] Key management - -### Phase 2: Communication (4 weeks) -- [ ] Message protocol -- [ ] Task negotiation -- [ ] Agent discovery -- [ ] Mesh networking - -### Phase 3: Credit Limits (4 weeks) -- [ ] Credit limit assignment -- [ ] Available credit calculation -- [ ] Limit enforcement -- [ ] Adjustment rules - -### Phase 4: Integration (4 weeks) -- [ ] Tracker integration -- [ ] Blockchain settlement -- [ ] Human backing -- [ ] Governance - -### Phase 5: Production (4 weeks) -- [ ] Security audit -- [ ] Performance testing -- [ ] Documentation -- [ ] Deployment - ---- - -## 10. Testing Requirements - -### 10.1 Unit Tests -- IOU creation and validation -- Signature verification -- Balance calculations -- Reputation scoring - -### 10.2 Integration Tests -- Multi-agent workflows -- Task negotiation -- Payment splitting -- Settlement - -### 10.3 Security Tests -- Key management -- Signature forgery attempts -- Replay attacks -- Authorization bypass - -### 10.4 Performance Tests -- Transaction throughput -- Message latency -- Scalability (1000+ agents) -- Resource usage - ---- - -## 11. Success Criteria - -### 11.1 Functional -- [ ] Agents can create and accept IOUs autonomously -- [ ] Task negotiation completes without human intervention -- [ ] Multi-agent payment splitting works correctly -- [ ] Credit limits enforced correctly -- [ ] Available credit calculated accurately - -### 11.2 Security -- [ ] No unauthorized transactions -- [ ] All messages properly authenticated -- [ ] Keys securely stored -- [ ] Audit trail complete and immutable - -### 11.3 Performance -- [ ] Transaction latency < 1 second -- [ ] Support 1000+ concurrent agents -- [ ] Settlement completes within 10 minutes -- [ ] Memory usage < 100MB per agent - -### 11.4 Credit Management -- [ ] Credit limits enforced in real-time -- [ ] Available credit updates instantly -- [ ] Limit adjustments applied correctly -- [ ] No overdrafts permitted - ---- - -## 12. Glossary - -| Term | Definition | -|------|------------| -| Agent | Autonomous software actor participating in economy | -| IOU | Promise to pay, created as Basis note | -| Tracker | Maintains debt ledger for agent community | -| Reserve | Collateral backing agent IOUs | -| Credit Limit | Maximum IOU amount agent can issue | -| Available Credit | Credit limit minus outstanding IOUs | -| Outstanding IOUs | Unredeemed IOUs issued by agent | -| Settlement | Converting IOUs to on-chain assets | -| Default | Failure to redeem IOU when presented | - ---- - -## Appendix A: Example Agent Interaction - -``` -1. [POST] Repo Agent → Network: Task Request - {task: "Implement feature", budget: 10 ERG} - -2. [BID] Dev Agent → Repo Agent: Task Bid - {amount: 10 ERG, timeline: 2 days} - -3. [AWARD] Repo Agent → Dev Agent: Task Award - {accepted, deadline: 48h} - -4. [WORK] Dev Agent: Implements feature - -5. [SUBMIT] Dev Agent → Repo Agent: Deliverables - {code, tests, docs} - -6. [IOU] Repo Agent → Dev Agent: IOU Note - {amount: 10 ERG, tracker_signed: true} - -7. [TRANSFER] Dev Agent → Test Agent: IOU Transfer - {amount: 3 ERG, reason: "testing"} - -8. [REDEEM] Test Agent → Blockchain: Redemption - {IOUs: [3 ERG], on_chain: true} -``` - ---- - -**Version:** 1.0 -**Status:** Draft -**Last Updated:** 2026-03-29 diff --git a/demo/basis/circular/QUICKSTART.md b/demo/basis/circular/QUICKSTART.md deleted file mode 100644 index c6b81c7..0000000 --- a/demo/basis/circular/QUICKSTART.md +++ /dev/null @@ -1,192 +0,0 @@ -# Circular Trading Demo - Quick Start - -## 5-Minute Demo - -This guide walks you through a complete circular trading demonstration in 5 minutes. - ---- - -## Prerequisites - -```bash -# Install Python 3 (if not already installed) -python3 --version - -# No additional dependencies needed for basic demo! -``` - ---- - -## Step 1: View Sample Ledger (30 seconds) - -```bash -cd /path/to/chaincash/demo/circular -cat sample_ledger.json -``` - -This shows a 3-party trading circle: -- **Alice owes Bob**: 10 ERG -- **Bob owes Carol**: 5 ERG -- **Carol owes Alice**: 3 ERG - ---- - -## Step 2: Calculate Net Positions (30 seconds) - -```bash -python3 calculate_netting.py --ledger sample_ledger.json -``` - -**Expected Output:** -``` -============================================================ - Circular Trading - Net Positions -============================================================ - alice : 0.070000000 ERG (owes) - bob : 0.050000000 ERG (owed) - carol : 0.020000000 ERG (owed) -============================================================ -``` - -**What this means:** -- Instead of 3 separate payments (18 ERG total) -- Only 2 payments needed (7 ERG total) -- **61% reduction in transactions!** - ---- - -## Step 3: Execute Debt Transfer (1 minute) - -Transfer Alice's debt from Bob to Carol: - -```bash -./transfer_debt.sh --debtor alice --from bob --to carol --amount 50000000 --reason "Bob buys from Carol" -``` - -**Expected Output:** -``` -============================================================ - Basis Debt Transfer -============================================================ - Transfer ID: transfer_1711732200000 - Debtor: alice (owes the debt) - From: bob (original creditor) - To: carol (new creditor) - Amount: 0.050000000 ERG (50000000 nanoERG) - Reason: Bob buys from Carol -============================================================ - ✓ Debt Transfer Completed Successfully! -============================================================ -``` - ---- - -## Step 4: Recalculate Netting (30 seconds) - -```bash -python3 calculate_netting.py --ledger ledger.json -``` - -**New positions after debt transfer:** -``` - alice : 0.080000000 ERG (owes) [increased!] - bob : 0.000000000 ERG (balanced) [paid off!] - carol : 0.080000000 ERG (owed) [increased!] -``` - -**Bob is now out of the circle!** His debt to Carol was paid by transferring Alice's debt. - ---- - -## Step 5: Execute Settlement (1 minute) - -```bash -python3 settle_netting.py --ledger ledger.json --auto -``` - -**Expected Output:** -``` -============================================================ - Settlement Execution -============================================================ - Optimized Settlement Plan: - alice → carol : 0.080000000 ERG [AUTO] -============================================================ - Settlement Summary - Planned: 1 transactions - Executed: 1 transactions - Fee savings: ~0.002 ERG (vs 3 individual txs) -============================================================ -``` - ---- - -## Step 6: Visualize (optional, 1 minute) - -```bash -# ASCII visualization (no dependencies) -python3 visualize_circle.py --ledger ledger.json --format ascii - -# Or with graphviz (if installed) -pip install graphviz -python3 visualize_circle.py --ledger ledger.json --output circle.png -``` - ---- - -## What You Demonstrated - -✅ **Triangular Trade** - Debt transfer without on-chain transaction -✅ **Netting** - Reduced 3 transactions to 1 -✅ **Debt Transfer** - Bob paid Carol by transferring Alice's debt -✅ **Fee Savings** - ~67% reduction in transaction fees - ---- - -## Try Your Own Scenario - -### Create Custom Transactions - -```bash -python3 calculate_netting.py --transactions "alice,bob,100;bob,carol,50;carol,dave,30;dave,alice,20" -``` - -### 5-Party Circle - -```bash -python3 calculate_netting.py --transactions "a,b,100;b,c,80;c,d,60;d,e,40;e,a,20" -``` - ---- - -## Next Steps - -1. **Read Full Documentation**: See [README.md](./README.md) -2. **Try Real Scenarios**: Create your own ledger files -3. **Integrate with Mesh**: Combine with mesh network demo -4. **Production Use**: Implement full tracker server - ---- - -## Troubleshooting - -### "command not found: bc" - -```bash -# Install bc calculator -sudo apt-get install bc # Debian/Ubuntu -brew install bc # macOS -``` - -### "No module named 'graphviz'" - -```bash -# Optional - for visualization only -pip install graphviz -``` - ---- - -**Demo Complete!** 🎉 - -You've successfully demonstrated circular trading and debt netting. diff --git a/demo/basis/circular/README.md b/demo/basis/circular/README.md deleted file mode 100644 index f8d3570..0000000 --- a/demo/basis/circular/README.md +++ /dev/null @@ -1,616 +0,0 @@ -# Circular Trading Demo - Triangular Debt Transfer - -## Efficient Multi-Party Trading with Debt Transfer - -This demo showcases **triangular trade** and **circular debt cancellation** - powerful features of the Basis protocol that enable efficient multi-party trading without on-chain settlement. - ---- - -## The Vision - -### Triangular Trade (Debt Transfer) - -From the [presentation](../../docs/presentation/presentation.md): - -``` -Before: After Transfer: -┌─────┐ owes 10 ┌─────┐ ┌─────┐ owes 5 ┌─────┐ -│ A │────────►│ B │ │ A │────────►│ B │ -└─────┘ └─────┘ └─────┘ └─────┘ - ╲ ╲ - ╲ owes 5 ╲ - ▼ ▼ - ┌─────┐ ┌─────┐ - │ C │ │ C │ - └─────┘ └─────┘ - -Scenario: B buys from C for 5 -Solution: A's debt transfers to C (with A's consent) -Result: No on-chain redemption needed! -``` - ---- - -## Why Circular Trading Matters - -### The Problem: Inefficient Bilateral Settlement - -Without triangular trade: -``` -A owes B: 10 ERG -B owes C: 5 ERG -C owes A: 3 ERG - -Naive settlement: -1. A pays B: 10 ERG (on-chain transaction) -2. B pays C: 5 ERG (on-chain transaction) -3. C pays A: 3 ERG (on-chain transaction) - -Total: 3 on-chain transactions, high fees -``` - -### The Solution: Netting + Debt Transfer - -With triangular trade: -``` -Net positions: -A: -10 + 3 = -7 ERG (owes 7) -B: +10 - 5 = +5 ERG (owed 5) -C: +5 - 3 = +2 ERG (owed 2) - -Optimized settlement: -1. A pays B: 5 ERG (debt transfer) -2. A pays C: 2 ERG (debt transfer) - -Total: 0 on-chain transactions, minimal fees -``` - ---- - -## Demo Scenarios - -### Scenario 1: Simple Triangular Trade - -**Participants:** Alice (A), Bob (B), Carol (C) - -**Initial State:** -``` -Alice owes Bob: 10 ERG -``` - -**Event:** Bob buys goods from Carol for 5 ERG - -**Without Triangular Trade:** -``` -Bob would pay Carol 5 ERG from his own funds -Alice still owes Bob 10 ERG -Total debt in system: 15 ERG -``` - -**With Triangular Trade:** -``` -Alice's debt to Bob is transferred to Carol -Alice now owes Carol: 5 ERG -Alice still owes Bob: 5 ERG -Bob's debt to Carol: 0 ERG (paid via debt transfer) -Total debt in system: 10 ERG (reduced!) -``` - -**Benefits:** -- ✅ Bob doesn't need to spend his own funds -- ✅ Carol gets paid by Alice directly -- ✅ No on-chain transaction needed -- ✅ System-wide debt reduced - ---- - -### Scenario 2: Circular Debt Cancellation - -**Participants:** Alice, Bob, Carol, Dave - -**Initial State:** -``` -Alice → Bob: 10 ERG -Bob → Carol: 8 ERG -Carol → Dave: 6 ERG -Dave → Alice: 4 ERG -``` - -**Visual:** -``` - 10 ERG -Alice ─────► Bob - ▲ │ - │ │ 8 ERG - │ ▼ -Dave ◄────── Carol - ▲ │ - │ │ 6 ERG - └───────────┘ - 4 ERG -``` - -**Net Positions:** -``` -Alice: -10 + 4 = -6 ERG (owes 6) -Bob: +10 - 8 = +2 ERG (owed 2) -Carol: +8 - 6 = +2 ERG (owed 2) -Dave: +6 - 4 = +2 ERG (owed 2) -``` - -**After Netting:** -``` -Alice pays Bob: 2 ERG -Alice pays Carol: 2 ERG -Alice pays Dave: 2 ERG - -Total transactions: 3 (instead of 4) -Total value moved: 6 ERG (instead of 28 ERG) -Efficiency gain: 78% reduction! -``` - ---- - -### Scenario 3: Community Trading Circle - -**Participants:** 5 community members in a village - -**Setup:** -``` -Members: Alice, Bob, Carol, Dave, Eve -All trade with each other over one month -All IOUs tracked by local tracker -``` - -**Monthly Ledger:** -``` - │ Owes │ Owed │ Net -─────────┼────────┼────────┼──────── -Alice │ 100 │ 80 │ -20 -Bob │ 60 │ 90 │ +30 -Carol │ 80 │ 70 │ -10 -Dave │ 90 │ 80 │ -10 -Eve │ 70 │ 80 │ +10 -─────────┴────────┴────────┴──────── -``` - -**Without Netting:** -- Total IOUs: 500 ERG -- Settlement transactions: 5+ on-chain -- Fees: ~5 ERG (at 1 ERG per tx) - -**With Netting:** -- Net debt: 40 ERG -- Settlement transactions: 3 on-chain -- Fees: ~3 ERG -- **Savings: 40% reduction** - ---- - -## Implementation - -### Debt Transfer Message Format - -```json -{ - "type": "debt_transfer", - "version": "1.0", - "original_debtor": "alice", - "original_creditor": "bob", - "new_creditor": "carol", - "amount": 50000000, - "currency": "nanoERG", - "reason": "Bob buys from Carol, transfers Alice's debt", - "consent": { - "debtor_signed": true, - "debtor_signature": "...", - "timestamp": 1704000000000 - }, - "original_iou_ref": "iou_12345" -} -``` - -### Netting Calculation Algorithm - -```python -def calculate_net_positions(transactions): - """ - Calculate net positions for circular trading. - - Args: - transactions: List of (debtor, creditor, amount) tuples - - Returns: - Dict of {participant: net_position} - Positive = owed money, Negative = owes money - """ - positions = {} - - for debtor, creditor, amount in transactions: - # Debtor owes (negative) - positions[debtor] = positions.get(debtor, 0) - amount - # Creditor is owed (positive) - positions[creditor] = positions.get(creditor, 0) + amount - - return positions - -def optimize_settlement(positions): - """ - Optimize settlement to minimize transactions. - - Returns: - List of (payer, payee, amount) for settlement - """ - debtors = [(p, -amt) for p, amt in positions.items() if amt < 0] - creditors = [(p, amt) for p, amt in positions.items() if amt > 0] - - settlements = [] - - while debtors and creditors: - debtor, debt_amt = debtors.pop() - creditor, credit_amt = creditors.pop() - - # Settle minimum of debt and credit - amount = min(debt_amt, credit_amt) - settlements.append((debtor, creditor, amount)) - - # Put back remainder - if debt_amt > amount: - debtors.append((debtor, debt_amt - amount)) - if credit_amt > amount: - creditors.append((creditor, credit_amt - amount)) - - return settlements - -# Example usage -transactions = [ - ("alice", "bob", 10), - ("bob", "carol", 5), - ("carol", "alice", 3) -] - -positions = calculate_net_positions(transactions) -# Result: {'alice': -7, 'bob': 5, 'carol': 2} - -settlements = optimize_settlement(positions) -# Result: [('alice', 'bob', 5), ('alice', 'carol', 2)] -``` - ---- - -## Running the Demo - -### Step 1: Setup Initial IOUs - -```bash -# Alice creates IOU to Bob (10 ERG) -cd /path/to/chaincash/demo/circular -./setup_iou.sh alice bob 100000000 "Initial debt" - -# Bob creates IOU to Carol (5 ERG) -./setup_iou.sh bob carol 50000000 "Goods purchase" - -# Carol creates IOU to Alice (3 ERG) -./setup_iou.sh carol alice 30000000 "Service payment" -``` - -### Step 2: Calculate Net Positions - -```bash -# Run netting calculation -python3 calculate_netting.py --ledger ledger.json - -# Output: -# ================================================== -# Circular Trading - Net Positions -# ================================================== -# Alice: -7.000000000 ERG (owes) -# Bob: +5.000000000 ERG (owed) -# Carol: +2.000000000 ERG (owed) -# ================================================== -# Total debt before: 18.000000000 ERG -# Total debt after: 7.000000000 ERG -# Reduction: 61.1% -# ================================================== -``` - -### Step 3: Execute Debt Transfer - -```bash -# Transfer Alice's debt from Bob to Carol -./transfer_debt.sh --debtor alice --from bob --to carol --amount 50000000 - -# Output: -# ================================================== -# Debt Transfer Executed -# ================================================== -# Original: Alice owes Bob 5 ERG -# New: Alice owes Carol 5 ERG -# Reason: Bob buys from Carol -# Consent: Alice signed ✓ -# ================================================== -``` - -### Step 4: Settle Optimized Positions - -```bash -# Execute optimized settlement -python3 settle_netting.py --positions netting_result.json - -# Output: -# ================================================== -# Settlement Plan -# ================================================== -# Alice → Bob: 5.000000000 ERG -# Alice → Carol: 2.000000000 ERG -# ================================================== -# Total transactions: 2 -# Total value: 7.000000000 ERG -# On-chain fees saved: ~8 ERG (vs 3 transactions) -# ================================================== -``` - ---- - -## Scripts Provided - -### `calculate_netting.py` - -Calculates net positions from ledger. - -```bash -python3 calculate_netting.py --ledger ledger.json -python3 calculate_netting.py --transactions "alice,bob,10;bob,carol,5" -``` - -### `transfer_debt.sh` - -Executes debt transfer with consent. - -```bash -./transfer_debt.sh --debtor alice --from bob --to carol --amount 50000000 -``` - -### `settle_netting.py` - -Executes optimized settlement. - -```bash -python3 settle_netting.py --positions netting_result.json -python3 settle_netting.py --auto # Auto-execute settlements -``` - -### `visualize_circle.py` - -Creates visual representation of trading circle. - -```bash -python3 visualize_circle.py --ledger ledger.json --output circle.png -``` - ---- - -## Consent Mechanism - -### Why Consent is Required - -Debt transfer requires the **debtor's consent** because: -1. Changes who the debtor owes -2. May affect debtor's relationship with new creditor -3. Prevents unauthorized debt shuffling - -### Consent Flow - -``` -1. Bob proposes transfer to Carol - └─> Message: "Transfer 5 ERG debt from Alice to you" - -2. Carol accepts - └─> Message: "I accept debt from Alice" - -3. Alice consents (REQUIRED) - └─> Signs: "I agree to owe Carol instead of Bob" - -4. Tracker executes transfer - └─> Ledger updated: Alice→Carol 5 ERG -``` - -### Consent Message Format - -```json -{ - "type": "debt_transfer_consent", - "debtor": "alice", - "transfer_ref": "transfer_12345", - "consent": true, - "signature": "...", - "timestamp": 1704000000000 -} -``` - ---- - -## Real-World Use Cases - -### 1. Village Trading Circle - -**Scenario:** 10 families trade goods/services monthly - -**Without Circular Trading:** -- Each family settles individually -- 20+ on-chain transactions -- High fees eat into small margins - -**With Circular Trading:** -- Monthly netting calculation -- 3-5 optimized settlements -- **60-75% fee reduction** - -### 2. Supply Chain Finance - -**Scenario:** Manufacturer → Distributor → Retailer - -``` -Manufacturer owes Supplier: 1000 ERG -Distributor owes Manufacturer: 800 ERG -Retailer owes Distributor: 600 ERG - -Net positions: -Manufacturer: -200 ERG -Distributor: -200 ERG -Retailer: +600 ERG -Supplier: +1000 ERG - -Optimized: -Retailer → Supplier: 600 ERG -Retailer → Manufacturer: 200 ERG -Retailer → Distributor: 200 ERG -``` - -### 3. Freelancer Collective - -**Scenario:** 5 freelancers share clients and referrals - -``` -Freelancer A refers to B: 5 ERG commission -Freelancer B refers to C: 3 ERG commission -Freelancer C refers to D: 4 ERG commission -Freelancer D refers to E: 6 ERG commission -Freelancer E refers to A: 2 ERG commission - -Net positions: -A: -3 ERG -B: +2 ERG -C: +1 ERG -D: +2 ERG -E: -4 ERG - -Settlement: -E → A: 3 ERG -E → D: 1 ERG -(Instead of 5 separate payments) -``` - ---- - -## Benefits Summary - -### Economic Benefits - -| Metric | Without Circular | With Circular | Improvement | -|--------|-----------------|---------------|-------------| -| Transactions | N | ~N/3 | 66% reduction | -| On-chain fees | High | Low | 60-75% savings | -| Capital efficiency | Poor | Good | Less locked capital | -| Settlement time | Slow | Fast | Netting is instant | - -### Social Benefits - -- **Strengthens community ties** - Circular trading encourages local commerce -- **Reduces dependency on external liquidity** - Internal debt circulation -- **Enables micro-trading** - Small transactions become viable -- **Builds trust networks** - Multi-party relationships - -### Technical Benefits - -- **Scalability** - O(N) transactions become O(1) netting -- **Privacy** - Net positions reveal less than full transaction history -- **Flexibility** - Debt can be transferred with consent -- **Resilience** - System works even with intermittent connectivity - ---- - -## Security Considerations - -### 1. Consent Verification - -```python -def verify_consent(transfer, debtor_public_key): - """Verify debtor actually consented to transfer.""" - message = hash_transfer_details(transfer) - return verify_signature(message, debtor_public_key, transfer.consent.signature) -``` - -### 2. Double-Spending Prevention - -```python -def check_debt_not_already_transferred(transfer, ledger): - """Ensure same debt isn't transferred twice.""" - iou_ref = transfer.original_iou_ref - existing_transfers = ledger.get_transfers_for_iou(iou_ref) - return len(existing_transfers) == 0 -``` - -### 3. Tracker Cannot Steal - -``` -Tracker can: -✓ Calculate net positions -✓ Propose debt transfers -✓ Execute with proper consent - -Tracker cannot: -✗ Transfer debt without debtor consent -✗ Create fake IOUs -✗ Steal funds (requires reserve owner signature) -``` - ---- - -## Testing Checklist - -### Unit Tests -- [ ] Net position calculation correct -- [ ] Debt transfer with consent works -- [ ] Debt transfer without consent fails -- [ ] Circular dependency detection -- [ ] Edge cases (zero amounts, self-debt) - -### Integration Tests -- [ ] 3-party triangular trade -- [ ] 5-party circular netting -- [ ] 10-party stress test -- [ ] Consent flow end-to-end -- [ ] Settlement execution - -### Field Tests -- [ ] Real community trading circle -- [ ] Monthly netting cycle -- [ ] Dispute resolution -- [ ] User experience feedback - ---- - -## Resources - -### Documentation -- [Presentation](../../docs/presentation/presentation.md) - Triangular trade slide -- [Basis Protocol Whitepaper](../../docs/conf/conf.pdf) -- [Mesh Network Demo](../mesh/README.md) - For offline operation - -### Scripts -- `calculate_netting.py` - Net position calculation -- `transfer_debt.sh` - Debt transfer execution -- `settle_netting.py` - Optimized settlement -- `visualize_circle.py` - Visualization tool - -### External -- [Triangular Trade (Wikipedia)](https://en.wikipedia.org/wiki/Triangular_trade) -- [Debt Netting (Investopedia)](https://www.investopedia.com/terms/n/netting.asp) -- [Multilateral Netting](https://www.investopedia.com/terms/m/multilateral-netting.asp) - ---- - -## License - -This demo is part of the ChainCash project, released under a permissive open-source license. See [LICENSE](../../LICENSE) for details. - ---- - -**Built by and for the Commons** 🌱 - -Free, open source community project. No token, no VC, no corporate control. - -**Efficient Community Trading** 🔄 - -Enabling circular debt cancellation for efficient multi-party settlements. diff --git a/demo/basis/circular/calculate_netting.py b/demo/basis/circular/calculate_netting.py deleted file mode 100755 index ff4e881..0000000 --- a/demo/basis/circular/calculate_netting.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -""" -Calculate net positions for circular trading. - -Usage: - python3 calculate_netting.py --ledger ledger.json - python3 calculate_netting.py --transactions "alice,bob,10;bob,carol,5" -""" - -import argparse -import json -from collections import defaultdict -from typing import Dict, List, Tuple - - -def calculate_net_positions(transactions: List[Tuple[str, str, int]]) -> Dict[str, int]: - """ - Calculate net positions for circular trading. - - Args: - transactions: List of (debtor, creditor, amount) tuples - - Returns: - Dict of {participant: net_position} - Positive = owed money, Negative = owes money - """ - positions = defaultdict(int) - - for debtor, creditor, amount in transactions: - # Debtor owes (negative) - positions[debtor] -= amount - # Creditor is owed (positive) - positions[creditor] += amount - - return dict(positions) - - -def optimize_settlement(positions: Dict[str, int]) -> List[Tuple[str, str, int]]: - """ - Optimize settlement to minimize transactions. - - Uses greedy algorithm to match debtors with creditors. - - Returns: - List of (payer, payee, amount) for settlement - """ - # Separate debtors and creditors - debtors = [(p, -amt) for p, amt in positions.items() if amt < 0] - creditors = [(p, amt) for p, amt in positions.items() if amt > 0] - - # Sort by amount (largest first) - debtors.sort(key=lambda x: x[1], reverse=True) - creditors.sort(key=lambda x: x[1], reverse=True) - - settlements = [] - - while debtors and creditors: - debtor, debt_amt = debtors.pop() - creditor, credit_amt = creditors.pop() - - # Settle minimum of debt and credit - amount = min(debt_amt, credit_amt) - settlements.append((debtor, creditor, amount)) - - # Put back remainder - if debt_amt > amount: - debtors.append((debtor, debt_amt - amount)) - if credit_amt > amount: - creditors.append((creditor, credit_amt - amount)) - - return settlements - - -def load_ledger(filepath: str) -> List[Tuple[str, str, int]]: - """Load transactions from ledger JSON file.""" - with open(filepath, 'r') as f: - ledger = json.load(f) - - transactions = [] - for entry in ledger.get('transactions', []): - debtor = entry.get('debtor') or entry.get('payer') - creditor = entry.get('creditor') or entry.get('payee') - amount = entry.get('amount') - - if debtor and creditor and amount: - transactions.append((debtor, creditor, int(amount))) - - return transactions - - -def parse_transactions(trans_str: str) -> List[Tuple[str, str, int]]: - """Parse transactions from command-line string. - - Format: "alice,bob,10;bob,carol,5" - """ - transactions = [] - for tx in trans_str.split(';'): - parts = tx.strip().split(',') - if len(parts) == 3: - debtor, creditor, amount = parts - transactions.append((debtor.strip(), creditor.strip(), int(amount))) - return transactions - - -def print_positions(positions: Dict[str, int]): - """Print net positions in formatted table.""" - print("=" * 60) - print(" Circular Trading - Net Positions") - print("=" * 60) - - total_debt = 0 - total_credit = 0 - - for participant, position in sorted(positions.items()): - if position < 0: - print(f" {participant:10s}: {-position / 1e9:10.9f} ERG (owes)") - total_debt += -position - elif position > 0: - print(f" {participant:10s}: {position / 1e9:10.9f} ERG (owed)") - total_credit += position - - print("-" * 60) - print(f" Total debt: {total_debt / 1e9:.9f} ERG") - print(f" Total credit: {total_credit / 1e9:.9f} ERG") - print("=" * 60) - - -def print_settlements(settlements: List[Tuple[str, str, int]]): - """Print optimized settlement plan.""" - print() - print("=" * 60) - print(" Optimized Settlement Plan") - print("=" * 60) - - total = 0 - for payer, payee, amount in settlements: - print(f" {payer:10s} → {payee:10s}: {amount / 1e9:10.9f} ERG") - total += amount - - print("-" * 60) - print(f" Total transactions: {len(settlements)}") - print(f" Total value: {total / 1e9:.9f} ERG") - - # Estimate fee savings - original_txs = len(settlements) * 2 # Rough estimate - fee_per_tx = 1000000 # 0.001 ERG - savings = (original_txs - len(settlements)) * fee_per_tx - - print(f" Estimated savings: ~{savings / 1e9:.3f} ERG in fees") - print("=" * 60) - - -def main(): - parser = argparse.ArgumentParser( - description='Calculate net positions for circular trading', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s --ledger ledger.json - %(prog)s --transactions "alice,bob,10;bob,carol,5;carol,alice,3" - %(prog)s --transactions "alice,bob,100000000;bob,carol,50000000" - """ - ) - - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('--ledger', help='Path to ledger JSON file') - group.add_argument('--transactions', help='Transactions string: "alice,bob,10;bob,carol,5"') - - parser.add_argument('--output', help='Save results to JSON file') - parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') - - args = parser.parse_args() - - # Load transactions - if args.ledger: - print(f"Loading ledger from: {args.ledger}") - transactions = load_ledger(args.ledger) - else: - print(f"Parsing transactions: {args.transactions}") - transactions = parse_transactions(args.transactions) - - if not transactions: - print("Error: No transactions found") - return 1 - - print(f"Loaded {len(transactions)} transactions") - print() - - # Calculate net positions - positions = calculate_net_positions(transactions) - print_positions(positions) - - # Optimize settlement - settlements = optimize_settlement(positions) - print_settlements(settlements) - - # Save results if requested - if args.output: - result = { - 'positions': positions, - 'settlements': [ - {'payer': p, 'payee': y, 'amount': a} - for p, y, a in settlements - ] - } - with open(args.output, 'w') as f: - json.dump(result, f, indent=2) - print(f"\nResults saved to: {args.output}") - - return 0 - - -if __name__ == '__main__': - exit(main()) diff --git a/demo/basis/circular/sample_ledger.json b/demo/basis/circular/sample_ledger.json deleted file mode 100644 index 71cb47b..0000000 --- a/demo/basis/circular/sample_ledger.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "Circular Trading Demo Ledger", - "description": "Sample ledger for triangular debt demonstration", - "created_at": "2026-03-29T00:00:00Z", - "transactions": [ - { - "type": "iou", - "debtor": "alice", - "creditor": "bob", - "amount": 100000000, - "currency": "nanoERG", - "message": "Initial debt - goods purchase", - "timestamp": 1704000000000 - }, - { - "type": "iou", - "debtor": "bob", - "creditor": "carol", - "amount": 50000000, - "currency": "nanoERG", - "message": "Bob buys from Carol", - "timestamp": 1704001000000 - }, - { - "type": "iou", - "debtor": "carol", - "creditor": "alice", - "amount": 30000000, - "currency": "nanoERG", - "message": "Carol pays Alice for services", - "timestamp": 1704002000000 - } - ], - "debt_transfers": [], - "metadata": { - "participants": ["alice", "bob", "carol"], - "total_transactions": 3, - "total_debt": 180000000 - } -} diff --git a/demo/basis/circular/settle_netting.py b/demo/basis/circular/settle_netting.py deleted file mode 100755 index 40ee9f8..0000000 --- a/demo/basis/circular/settle_netting.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -""" -Execute optimized settlement for circular trading. - -Usage: - python3 settle_netting.py --positions netting_result.json - python3 settle_netting.py --ledger ledger.json --auto -""" - -import argparse -import json -import sys -from typing import Dict, List, Tuple - -# Import from calculate_netting -sys.path.insert(0, '.') -from calculate_netting import calculate_net_positions, optimize_settlement, load_ledger - - -def execute_settlement(payer: str, payee: str, amount: int, auto: bool = False) -> bool: - """ - Execute a single settlement payment. - - In a real implementation, this would: - 1. Create IOU from payer to payee - 2. Get signatures - 3. Update ledger - - For demo, we just simulate. - """ - erg_amount = amount / 1e9 - - if auto: - # Auto-execute (simulate) - print(f" ✓ {payer:10s} → {payee:10s}: {erg_amount:.9f} ERG [AUTO]") - return True - else: - # Interactive confirmation - print(f"\n Settlement: {payer} → {payee}") - print(f" Amount: {erg_amount:.9f} ERG") - response = input(" Execute? (yes/no): ").strip().lower() - - if response == 'yes': - print(f" ✓ Executed") - return True - else: - print(f" ✗ Skipped") - return False - - -def save_settlement_record(settlements: List[Tuple[str, str, int]], output_file: str): - """Save settlement plan to JSON file.""" - record = { - "type": "settlement_plan", - "settlements": [ - { - "payer": payer, - "payee": payee, - "amount": amount, - "amount_erg": amount / 1e9 - } - for payer, payee, amount in settlements - ], - "total_transactions": len(settlements), - "total_value": sum(a for _, _, a in settlements) - } - - with open(output_file, 'w') as f: - json.dump(record, f, indent=2) - - return output_file - - -def main(): - parser = argparse.ArgumentParser( - description='Execute optimized settlement for circular trading', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s --positions netting_result.json - %(prog)s --ledger ledger.json --auto - %(prog)s --transactions "alice,bob,10;bob,carol,5" --auto - """ - ) - - group = parser.add_mutually_exclusive_group() - group.add_argument('--positions', help='JSON file with net positions') - group.add_argument('--ledger', help='Ledger JSON file') - group.add_argument('--transactions', help='Transactions string: "alice,bob,10"') - - parser.add_argument('--auto', action='store_true', help='Auto-execute without confirmation') - parser.add_argument('--output', help='Save settlement record to file') - - args = parser.parse_args() - - # Load positions - if args.positions: - with open(args.positions, 'r') as f: - data = json.load(f) - positions = data.get('positions', data) - elif args.ledger: - transactions = load_ledger(args.ledger) - positions = calculate_net_positions(transactions) - elif args.transactions: - transactions = [] - for tx in args.transactions.split(';'): - parts = tx.strip().split(',') - if len(parts) == 3: - transactions.append((parts[0].strip(), parts[1].strip(), int(parts[2]))) - positions = calculate_net_positions(transactions) - else: - print("Error: Must specify --positions, --ledger, or --transactions") - return 1 - - # Print positions - print("=" * 60) - print(" Settlement Execution") - print("=" * 60) - print() - print(" Net Positions:") - - for participant, position in sorted(positions.items()): - if position != 0: - direction = "owes" if position < 0 else "owed" - amount = abs(position) / 1e9 - print(f" {participant:10s}: {amount:10.9f} ERG ({direction})") - - print() - - # Calculate optimized settlements - settlements = optimize_settlement(positions) - - if not settlements: - print(" No settlements needed (all positions balanced)") - return 0 - - # Print settlement plan - print(" Optimized Settlement Plan:") - print(" " + "-" * 56) - - executed = 0 - for payer, payee, amount in settlements: - if execute_settlement(payer, payee, amount, args.auto): - executed += 1 - - print() - print("=" * 60) - print(f" Settlement Summary") - print("=" * 60) - print(f" Planned: {len(settlements)} transactions") - print(f" Executed: {executed} transactions") - print(f" Skipped: {len(settlements) - executed} transactions") - - total_value = sum(a for _, _, a in settlements) - print(f" Total value: {total_value / 1e9:.9f} ERG") - - # Estimate savings - original_txs = len(positions) # Rough estimate - fee_per_tx = 1000000 # 0.001 ERG - savings = (original_txs - executed) * fee_per_tx - - print(f" Fee savings: ~{savings / 1e9:.3f} ERG (vs {original_txs} individual txs)") - print("=" * 60) - - # Save record if requested - if args.output: - output_file = save_settlement_record(settlements, args.output) - print(f"\n Settlement record saved to: {output_file}") - - print() - print(" Next steps:") - print(" 1. Verify ledger: cat ledger.json") - print(" 2. Calculate new netting: python3 calculate_netting.py --ledger ledger.json") - print(" 3. Execute on-chain settlement if needed") - print() - - return 0 - - -if __name__ == '__main__': - exit(main()) diff --git a/demo/basis/circular/transfer_debt.sh b/demo/basis/circular/transfer_debt.sh deleted file mode 100755 index 304f50f..0000000 --- a/demo/basis/circular/transfer_debt.sh +++ /dev/null @@ -1,302 +0,0 @@ -#!/bin/bash -# transfer_debt.sh - Transfer debt from one creditor to another -# -# Usage: -# ./transfer_debt.sh --debtor alice --from bob --to carol --amount 50000000 -# ./transfer_debt.sh -d alice -f bob -t carol -a 50000000 --reason "Bob buys from Carol" -# -# This script transfers a debt obligation: -# Before: debtor owes 'from' participant -# After: debtor owes 'to' participant -# -# Requires debtor's consent (signature) for the transfer. - -set -e - -# Default values -AMOUNT="" -DEBTOR="" -FROM_CREDITOR="" -TO_CREDITOR="" -REASON="Debt transfer" -LEDGER_FILE="ledger.json" -OUTPUT_FILE="" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Print usage -usage() { - cat << EOF -Usage: $0 --debtor --from --to --amount - -Transfer debt from one creditor to another (requires debtor consent). - -Required: - -d, --debtor The debtor (person who owes) - -f, --from Original creditor (person owed) - -t, --to New creditor (person to be owed) - -a, --amount Amount in nanoERG (e.g., 50000000 = 0.05 ERG) - -Optional: - -r, --reason Reason for transfer (default: "Debt transfer") - -l, --ledger Ledger file (default: ledger.json) - -o, --output Output file for transfer record - -h, --help Show this help message - -Example: - $0 --debtor alice --from bob --to carol --amount 50000000 - $0 -d alice -f bob -t carol -a 50000000 -r "Bob buys from Carol" -EOF - exit 1 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -d|--debtor) - DEBTOR="$2" - shift 2 - ;; - -f|--from) - FROM_CREDITOR="$2" - shift 2 - ;; - -t|--to) - TO_CREDITOR="$2" - shift 2 - ;; - -a|--amount) - AMOUNT="$2" - shift 2 - ;; - -r|--reason) - REASON="$2" - shift 2 - ;; - -l|--ledger) - LEDGER_FILE="$2" - shift 2 - ;; - -o|--output) - OUTPUT_FILE="$2" - shift 2 - ;; - -h|--help) - usage - ;; - *) - echo "Unknown option: $1" - usage - ;; - esac -done - -# Validate required arguments -if [[ -z "$DEBTOR" || -z "$FROM_CREDITOR" || -z "$TO_CREDITOR" || -z "$AMOUNT" ]]; then - echo -e "${RED}Error: Missing required arguments${NC}" - usage -fi - -# Calculate ERG amount -ERG_AMOUNT=$(echo "scale=9; $AMOUNT / 1000000000" | bc) - -# Generate timestamp -TIMESTAMP=$(date +%s%3N) -TRANSFER_ID="transfer_${TIMESTAMP}" - -# Print header -echo -e "${BLUE}============================================================${NC}" -echo -e "${BLUE} Basis Debt Transfer${NC}" -echo -e "${BLUE}============================================================${NC}" -echo "" -echo -e " ${YELLOW}Debt Transfer Details${NC}" -echo " ----------------------------------------------------------" -echo " Transfer ID: $TRANSFER_ID" -echo " Debtor: $DEBTOR (owes the debt)" -echo " From: $FROM_CREDITOR (original creditor)" -echo " To: $TO_CREDITOR (new creditor)" -echo " Amount: $ERG_AMOUNT ERG ($AMOUNT nanoERG)" -echo " Reason: $REASON" -echo " Timestamp: $TIMESTAMP" -echo " Ledger: $LEDGER_FILE" -echo " ----------------------------------------------------------" -echo "" - -# Step 1: Verify debt exists -echo -e "${YELLOW}Step 1: Verifying debt exists...${NC}" - -if [[ -f "$LEDGER_FILE" ]]; then - # Check if debtor owes from_creditor - DEBT_EXISTS=$(python3 -c " -import json -with open('$LEDGER_FILE', 'r') as f: - ledger = json.load(f) -for tx in ledger.get('transactions', []): - if (tx.get('debtor') == '$DEBTOR' and - tx.get('creditor') == '$FROM_CREDITOR' and - tx.get('amount', 0) >= $AMOUNT): - print('yes') - break -else: - print('no') -" 2>/dev/null || echo "no") - - if [[ "$DEBT_EXISTS" == "yes" ]]; then - echo -e " ${GREEN}✓ Debt verified in ledger${NC}" - else - echo -e " ${YELLOW}⚠ Debt not found in ledger (proceeding anyway)${NC}" - echo " This may be a new debt transfer without prior IOU" - fi -else - echo -e " ${YELLOW}⚠ Ledger file not found (will create new record)${NC}" -fi - -echo "" - -# Step 2: Get debtor consent -echo -e "${YELLOW}Step 2: Obtaining debtor consent...${NC}" -echo "" -echo " $DEBTOR, do you consent to transfer your debt of $ERG_AMOUNT ERG" -echo " from $FROM_CREDITOR to $TO_CREDITOR?" -echo "" -echo " Reason: $REASON" -echo "" - -# In automated mode, skip interactive prompt -if [[ -n "$AUTOMATED" || -n "$SKIP_CONSENT" ]]; then - echo -e " ${GREEN}✓ Consent assumed (automated mode)${NC}" - CONSENT_SIGNATURE="automated_consent_${TIMESTAMP}" -else - read -p " Enter 'yes' to consent: " CONSENT_INPUT - - if [[ "$CONSENT_INPUT" != "yes" ]]; then - echo -e "${RED}✗ Debtor did not consent. Transfer cancelled.${NC}" - exit 1 - fi - - echo -e "${GREEN}✓ Debtor consented${NC}" - CONSENT_SIGNATURE="consent_${TIMESTAMP}" -fi - -echo "" - -# Step 3: Get creditor acceptance -echo -e "${YELLOW}Step 3: Confirming new creditor acceptance...${NC}" - -if [[ -n "$AUTOMATED" ]]; then - echo -e " ${GREEN}✓ Acceptance assumed (automated mode)${NC}" -else - echo " $TO_CREDITOR, do you accept this debt from $DEBTOR?" - read -p " Enter 'yes' to accept: " ACCEPT_INPUT - - if [[ "$ACCEPT_INPUT" != "yes" ]]; then - echo -e "${RED}✗ New creditor did not accept. Transfer cancelled.${NC}" - exit 1 - fi - - echo -e "${GREEN}✓ New creditor accepted${NC}" -fi - -echo "" - -# Step 4: Create transfer record -echo -e "${YELLOW}Step 4: Creating transfer record...${NC}" - -TRANSFER_RECORD=$(cat < "$OUTPUT_FILE" - echo " Transfer record saved to: $OUTPUT_FILE" -else - OUTPUT_FILE="transfer_${TRANSFER_ID}.json" - echo "$TRANSFER_RECORD" > "$OUTPUT_FILE" - echo " Transfer record saved to: $OUTPUT_FILE" -fi - -echo -e "${GREEN}✓ Transfer record created${NC}" -echo "" - -# Step 5: Update ledger -echo -e "${YELLOW}Step 5: Updating ledger...${NC}" - -# Create ledger entry for new debt -NEW_DEBT_ENTRY=$(cat < "$LEDGER_FILE" - echo " Created new ledger: $LEDGER_FILE" -fi - -echo -e "${GREEN}✓ Ledger updated${NC}" -echo "" - -# Print summary -echo -e "${BLUE}============================================================${NC}" -echo -e "${GREEN} Debt Transfer Completed Successfully!${NC}" -echo -e "${BLUE}============================================================${NC}" -echo "" -echo " Summary:" -echo " ────────────────────────────────────────────────────────" -echo " Before: $DEBTOR owed $FROM_CREDITOR $ERG_AMOUNT ERG" -echo " After: $DEBTOR owes $TO_CREDITOR $ERG_AMOUNT ERG" -echo "" -echo " Effect:" -echo " - $FROM_CREDITOR no longer owed by $DEBTOR" -echo " - $TO_CREDITOR now owed by $DEBTOR" -echo " - No on-chain transaction needed!" -echo " ────────────────────────────────────────────────────────" -echo "" -echo " Next steps:" -echo " 1. Verify ledger: cat $LEDGER_FILE" -echo " 2. View transfer: cat $OUTPUT_FILE" -echo " 3. Calculate netting: python3 calculate_netting.py --ledger $LEDGER_FILE" -echo "" diff --git a/demo/basis/circular/visualize_circle.py b/demo/basis/circular/visualize_circle.py deleted file mode 100755 index 64a7c85..0000000 --- a/demo/basis/circular/visualize_circle.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python3 -""" -Visualize circular trading network. - -Usage: - python3 visualize_circle.py --ledger ledger.json --output circle.png - python3 visualize_circle.py --transactions "alice,bob,10;bob,carol,5" -""" - -import argparse -import json -import sys -from typing import Dict, List, Tuple, Optional - -try: - import graphviz - HAS_GRAPHVIZ = True -except ImportError: - HAS_GRAPHVIZ = False - - -def create_digraph( - transactions: List[Tuple[str, str, int]], - positions: Optional[Dict[str, int]] = None, - title: str = "Circular Trading Network" -) -> graphviz.Digraph: - """ - Create Graphviz digraph for visualization. - - Args: - transactions: List of (debtor, creditor, amount) tuples - positions: Optional net positions for coloring - title: Graph title - - Returns: - Graphviz Digraph object - """ - dot = graphviz.Digraph(comment=title) - dot.attr(rankdir='LR', size='10,6') - dot.attr('node', shape='box', style='filled', fillcolor='lightblue') - dot.attr('edge', color='gray') - - # Add title - dot.attr(label=title, fontsize='20') - - # Track edges for aggregation - edges = {} - - # Add transactions as edges - for debtor, creditor, amount in transactions: - key = (debtor, creditor) - if key in edges: - edges[key] += amount - else: - edges[key] = amount - - # Add nodes and edges - participants = set() - for (debtor, creditor), amount in edges.items(): - participants.add(debtor) - participants.add(creditor) - - erg_amount = amount / 1e9 - label = f"{erg_amount:.2f} ERG" - - # Color by net position if provided - if positions: - debtor_pos = positions.get(debtor, 0) - creditor_pos = positions.get(creditor, 0) - - if debtor_pos < 0: - dot.node(debtor, fillcolor='lightcoral') # Owes money - elif debtor_pos > 0: - dot.node(debtor, fillcolor='lightgreen') # Owed money - - if creditor_pos < 0: - dot.node(creditor, fillcolor='lightcoral') - elif creditor_pos > 0: - dot.node(creditor, fillcolor='lightgreen') - - dot.edge(debtor, creditor, label=label, penwidth='2') - - return dot - - -def print_ascii_visualization(transactions: List[Tuple[str, str, int]], positions: Dict[str, int]): - """Print ASCII art visualization for terminals without graphviz.""" - print() - print("=" * 60) - print(" Circular Trading Network (ASCII)") - print("=" * 60) - print() - - # Simple ASCII representation - participants = set() - for debtor, creditor, _ in transactions: - participants.add(debtor) - participants.add(creditor) - - # Print participants with positions - print(" Participants:") - print(" " + "-" * 56) - for p in sorted(participants): - pos = positions.get(p, 0) - if pos < 0: - status = f"owes {-pos / 1e9:.2f} ERG" - symbol = "📤" - elif pos > 0: - status = f"owed {pos / 1e9:.2f} ERG" - symbol = "📥" - else: - status = "balanced" - symbol = "✓" - - print(f" {symbol} {p:12s}: {status}") - - print() - print(" Transaction Flow:") - print(" " + "-" * 56) - - # Group by debtor - by_debtor = {} - for debtor, creditor, amount in transactions: - if debtor not in by_debtor: - by_debtor[debtor] = [] - by_debtor[debtor].append((creditor, amount)) - - for debtor in sorted(by_debtor.keys()): - for creditor, amount in by_debtor[debtor]: - erg = amount / 1e9 - print(f" {debtor:12s} ──{erg:>8.2f} ERG──► {creditor}") - - print() - print("=" * 60) - - -def main(): - parser = argparse.ArgumentParser( - description='Visualize circular trading network', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s --ledger ledger.json --output circle.png - %(prog)s --transactions "alice,bob,10;bob,carol,5" - %(prog)s --ledger ledger.json --format ascii - """ - ) - - group = parser.add_mutually_exclusive_group() - group.add_argument('--ledger', help='Path to ledger JSON file') - group.add_argument('--transactions', help='Transactions string: "alice,bob,10"') - - parser.add_argument('--output', '-o', help='Output file (PNG, SVG, PDF)') - parser.add_argument('--format', '-f', choices=['png', 'svg', 'pdf', 'ascii'], - default='png', help='Output format') - parser.add_argument('--title', '-t', default='Circular Trading Network', - help='Graph title') - - args = parser.parse_args() - - # Load transactions - if args.ledger: - with open(args.ledger, 'r') as f: - ledger = json.load(f) - transactions = [ - (tx.get('debtor') or tx.get('payer'), - tx.get('creditor') or tx.get('payee'), - int(tx.get('amount', 0))) - for tx in ledger.get('transactions', []) - ] - elif args.transactions: - transactions = [] - for tx in args.transactions.split(';'): - parts = tx.strip().split(',') - if len(parts) == 3: - transactions.append((parts[0].strip(), parts[1].strip(), int(parts[2]))) - else: - print("Error: Must specify --ledger or --transactions") - return 1 - - if not transactions: - print("Error: No transactions found") - return 1 - - # Calculate positions - from calculate_netting import calculate_net_positions - positions = calculate_net_positions(transactions) - - # Choose output format - if args.format == 'ascii' or not HAS_GRAPHVIZ: - if not HAS_GRAPHVIZ and args.format != 'ascii': - print("Note: graphviz not installed, using ASCII output") - print(" Install with: pip install graphviz") - print() - - print_ascii_visualization(transactions, positions) - - if args.output and args.format == 'ascii': - # Save ASCII to file - import io - from contextlib import redirect_stdout - - f = io.StringIO() - with redirect_stdout(f): - print_ascii_visualization(transactions, positions) - - with open(args.output + '.txt', 'w') as file: - file.write(f.getvalue()) - - print(f"ASCII visualization saved to: {args.output}.txt") - else: - # Create graphviz visualization - dot = create_digraph(transactions, positions, args.title) - - # Render - if args.output: - # Remove extension for graphviz - output_base = args.output.rsplit('.', 1)[0] if '.' in args.output else args.output - output_path = dot.render(output_base, format=args.format) - print(f"Visualization saved to: {output_path}") - else: - # Save to default location - output_path = dot.render('circular_trading', format=args.format) - print(f"Visualization saved to: {output_path}") - - print(f"Format: {args.format.upper()}") - - print() - print("Summary:") - print(f" Participants: {len(positions)}") - print(f" Transactions: {len(transactions)}") - - total_value = sum(abs(a) for _, _, a in transactions) - print(f" Total value: {total_value / 1e9:.2f} ERG") - - return 0 - - -if __name__ == '__main__': - exit(main()) diff --git a/demo/basis/mesh/DEMO.md b/demo/basis/mesh/DEMO.md deleted file mode 100644 index 6edd766..0000000 --- a/demo/basis/mesh/DEMO.md +++ /dev/null @@ -1,63 +0,0 @@ -# Mesh Network Demo - Community Trading over Basis Protocol - -## Overview - -This demo showcases the **Basis protocol** enabling peer-to-peer credit-based trading in disconnected communities using mesh networking technology, as described in the [presentation](../../docs/presentation/presentation.md). - -## Running the Demo - -The full implementation requires: -1. Mobile wallet apps with mesh connectivity (Bluetooth/WiFi Direct) -2. Local tracker server for the community -3. Gateway node for blockchain sync - -For now, review the scenarios in the presentation and the architecture in this README. - -## Key Scenarios - -### 1. Offline IOU Creation -- Alice creates an IOU note to Bob without Internet over mesh -- Tracker signs locally and maintains community ledger via mesh, - it tries to update a short cryptographic digest of the ledger to the blockchain -- Note transfer completes without blockchain -- When Internet is found, a note can be redeemed via blockchain. -- In principle, only ont point of Internet connection is enough. State of blockchain can be prooven to anyone - in the community using NiPoPoWs supported by Ergo blockchain. - -### 2. IOU Transfer -- Bob transfers Alice's IOU to Carol -- Partial payments supported -- Endorsement chain maintained - -### 3. Triangular Trade -- A owes B, B owes C, C owes A -- Net positions calculated -- Minimal on-chain settlement - -### 4. AI Agent Economy -- Autonomous agents create credit relationships -- No human intermediaries -- Automated settlement - -### 5. Micropayments -- Pay-per-article without subscriptions -- Aggregated settlement -- No on-chain fees per article - -## Architecture - -See the detailed architecture in the main [README.md](./README.md). - -## Implementation Status - -- ✅ Conceptual design complete -- ✅ Test scenarios documented -- 🚧 Full implementation in progress -- 🚧 Mobile wallet app -- 🚧 Mesh networking layer - -## Resources - -- [Presentation](../../docs/presentation/presentation.md) -- [Whitepaper](../../docs/conf/conf.pdf) -- [Main README](./README.md) diff --git a/demo/basis/mesh/IMPLEMENTATION_PLAN.md b/demo/basis/mesh/IMPLEMENTATION_PLAN.md deleted file mode 100644 index 8800037..0000000 --- a/demo/basis/mesh/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,523 +0,0 @@ -# Mesh Network Demo - Implementation Plan - -## Division of Work: AI Assistant vs Human Implementation - -This document separates what has been implemented (by AI assistant) from what humans need to do to complete the showcase. - ---- - -## Part 1: AI Assistant Implementation ✅ (COMPLETE) - -### What Has Been Delivered - -The AI assistant has created all **software artifacts** needed for the demo: - -#### 1. Documentation (Complete) -- [x] `README.md` - Main demo documentation with scenarios -- [x] `MESHTASTIC.md` - Meshtastic integration guide -- [x] `MCP_SERVER.md` - MCP server specification -- [x] `IMPLEMENTATION_PLAN.md` - This document - -#### 2. Scripts (Complete & Ready to Use) -- [x] `send_basis_message.sh` - Bash script for sending IOUs -- [x] `send_basis_iou.py` - Python script with advanced options -- [x] `listen_basis_iou.py` - Python script for receiving IOUs - -#### 3. Specifications (Complete) -- [x] Message format (compact JSON for LoRa) -- [x] IOU structure (payer, payee, amount, timestamp) -- [x] API documentation (all script options) -- [x] Testing checklist - -#### 4. Integration Design (Complete) -- [x] Meshtastic CLI integration -- [x] MCP server specification for AI assistants -- [x] Tracker server architecture -- [x] Gateway settlement flow - ---- - -## Part 2: Human Implementation Required 📋 (TODO) - -### What Humans Need to Do - -Humans must complete **physical setup** and **real-world testing**: - ---- - -### Phase 1: Hardware Setup (2-3 days) - -**Responsibility:** Human team - -**Required Skills:** Basic electronics, USB configuration - -#### Step 1.1: Purchase Hardware - -**Shopping List:** -``` -□ 2x Meshtastic devices (choose one): - - Rak4631 (~$50 each) - Recommended for beginners - - T-Beam (~$35 each) - Has GPS - - Heltec WiFi LoRa 32 (~$25 each) - Has WiFi - -□ 1x Raspberry Pi 4 (for tracker server) - Optional - - Or use any laptop/PC - -□ 1x Internet-connected device (for gateway) - - Can be same as tracker server - -□ Micro-USB cables (2x) -□ Antennas (if not included) -``` - -**Estimated Cost:** $100-150 for basic setup - -#### Step 1.2: Flash Firmware - -**Human Action Required:** -```bash -# Human must: -1. Connect device via USB -2. Download firmware from https://meshtastic.org -3. Flash using web flasher or dfu-util -4. Verify device shows up as /dev/ttyUSB0 -``` - -**AI Cannot Do:** -- Physical USB connections -- Hardware troubleshooting -- Firmware flashing (requires manual steps) - -#### Step 1.3: Configure Devices - -**Human Action Required:** -```bash -# Run these commands (AI provided, human executes): - -# Device 1 (Alice) -meshtastic --port /dev/ttyUSB0 --set owner.long_name "Alice" -meshtastic --port /dev/ttyUSB0 --set owner.short_name "A" - -# Device 2 (Bob) -meshtastic --port /dev/ttyUSB0 --set owner.long_name "Bob" -meshtastic --port /dev/ttyUSB0 --set owner.short_name "B" - -# Set encryption (both devices) -meshtastic --port /dev/ttyUSB0 --set channel.psks[0].key "AQ==" -``` - -**Success Criteria:** -- [ ] Both devices show correct names -- [ ] Devices can ping each other -- [ ] Range test: 100m minimum - ---- - -### Phase 2: Software Installation (1 day) - -**Responsibility:** Human developer - -**Required Skills:** Python, bash, git - -#### Step 2.1: Install Dependencies - -**Human Action Required:** -```bash -# Install Python packages -pip install meshtastic pubsub - -# Verify installation -meshtastic --version -# Should show: meshtastic 2.x.x - -# Clone repository (if not already done) -git clone https://github.com/ChainCashLabs/chaincash.git -cd chaincash/demo/basis/mesh -``` - -#### Step 2.2: Test Scripts - -**Human Action Required:** -```bash -# Make scripts executable -chmod +x *.sh *.py - -# Test send script (will fail without device - that's OK) -./send_basis_message.sh --help - -# Test listen script -python3 listen_basis_iou.py --help -``` - -**Success Criteria:** -- [ ] `meshtastic --version` works -- [ ] Scripts show help text -- [ ] No import errors - ---- - -### Phase 3: First Transaction Test (1 day) - -**Responsibility:** Human team (2 people recommended) - -**Required Skills:** Basic command line - -#### Step 3.1: Setup Test Environment - -**Human Action Required:** -```bash -# Person 1 (Alice): -cd /path/to/chaincash/demo/basis/mesh -# Keep terminal open - -# Person 2 (Bob): -cd /path/to/chaincash/demo/basis/mesh -# Keep terminal open -``` - -#### Step 3.2: Bob Starts Listening - -**Human Action Required (Bob):** -```bash -# Start listener -python3 listen_basis_iou.py --port /dev/ttyUSB0 - -# Expected output: -# ================================================== -# Basis IOU Listener -# ================================================== -# Listening for Basis IOU messages... -# Press Ctrl+C to exit -``` - -#### Step 3.3: Alice Sends IOU - -**Human Action Required (Alice):** -```bash -# Send test IOU -./send_basis_message.sh alice bob 50000000 "Test payment" - -# Expected output: -# ================================================== -# Basis IOU over Meshtastic -# ================================================== -# Payer: alice -# Payee: bob -# Amount: 0.050000000 ERG -# Message: Test payment -# ================================================== -# ✓ Message sent successfully! -``` - -#### Step 3.4: Verify Reception - -**Human Action Required (Bob):** -```bash -# Check listener output - should show: -# ================================================== -# 📬 Received Basis IOU! -# ================================================== -# From: !12345678 -# Payer: alice -# Payee: bob -# Amount: 0.050000000 ERG -# Message: Test payment -# ================================================== -# 💾 Saved to: iou_1711732200.json -``` - -**Success Criteria:** -- [ ] Message sent without errors -- [ ] Bob received IOU -- [ ] IOU saved to JSON file -- [ ] Signal strength reasonable (> -90 dBm) - ---- - -### Phase 4: Tracker Server Setup (1-2 days) - -**Responsibility:** Human developer - -**Required Skills:** Python, system administration - -#### Step 4.1: Configure Tracker - -**Human Action Required:** -```bash -# Copy and edit participants file -cp ../../participants.csv.template participants.csv -nano participants.csv - -# Add entries: -# name,address,secret_hex -# alice,9f7ZX..., -# bob,9f7ZY..., -# tracker,9f7ZZ..., -``` - -**Note:** AI cannot generate secrets for production use. - -#### Step 4.2: Start Tracker Service - -**Human Action Required:** -```bash -# Start tracker (command TBD - tracker not yet implemented) -python3 -m chaincash.offchain.tracker --config tracker.conf - -# Should show: -# [INFO] Tracker started -# [INFO] Listening for IOU requests -# [INFO] Ledger initialized -``` - -**AI Limitation:** Tracker server code not yet written. - -#### Step 4.3: Test Tracker Signing - -**Human Action Required:** -```bash -# Send IOU (should be auto-signed by tracker) -./send_basis_message.sh alice bob 50000000 "Tracker test" - -# Check tracker logs -tail -f tracker.log - -# Should show: -# [INFO] Received IOU request: alice->bob 50000000 -# [INFO] Signed IOU: abc123... -# [INFO] Ledger updated -``` - -**Success Criteria:** -- [ ] Tracker starts without errors -- [ ] IOUs are signed automatically -- [ ] Ledger updates correctly - ---- - -### Phase 5: Gateway Settlement (1-2 days) - -**Responsibility:** Human developer - -**Required Skills:** Ergo blockchain, API integration - -#### Step 5.1: Setup Ergo Node Connection - -**Human Action Required:** -```bash -# Set environment variables -export ERGO_NODE_URL=http://localhost:9053 -export ERGO_API_KEY=your_api_key - -# Or use public node -export ERGO_NODE_URL=https://api.ergoplatform.com -``` - -#### Step 5.2: Create Reserve (Alice) - -**Human Action Required:** -```bash -# Alice creates reserve with collateral -python3 -m chaincash.offchain.gateway --create-reserve \ - --amount 100000000 \ - --owner alice - -# Should create on-chain transaction -# TxId: -``` - -**AI Limitation:** Gateway code not yet fully implemented. - -#### Step 5.3: Redeem IOUs (Bob) - -**Human Action Required:** -```bash -# Bob redeems accumulated IOUs -python3 -m chaincash.offchain.gateway --redeem \ - --holder bob \ - --iou-files iou_*.json - -# Should show: -# [INFO] Aggregated 3 IOUs totaling 0.1 ERG -# [INFO] Redemption transaction created -# [INFO] TxId: -``` - -#### Step 5.4: Verify On-Chain - -**Human Action Required:** -```bash -# Check Ergo explorer -curl https://api.ergoplatform.com/transactions/ - -# Check Bob's balance -curl https://api.ergoplatform.com/addresses/bob_address - -# Should show increased ERG balance -``` - -**Success Criteria:** -- [ ] Reserve created on-chain -- [ ] Redemption transaction confirmed -- [ ] Bob's balance increased - ---- - -### Phase 6: Demo Presentation (1 day) - -**Responsibility:** Human team - -**Required Skills:** Presentation, video recording - -#### Step 6.1: Prepare Demo Script - -**Human Action Required:** -```markdown -# Demo Script (20 minutes total) - -## Introduction (2 min) -- Show hardware (2 Meshtastic devices) -- Explain offline trading concept -- Show Alice and Bob locations - -## Live Demo (15 min) -1. Alice sends IOU: ./send_basis_message.sh (3 min) -2. Bob receives on device (show screen) (2 min) -3. Send 2 more IOUs (3 min) -4. Show accumulated balance (2 min) -5. Gateway redemption (5 min) - -## Q&A (3 min) -- Answer questions -- Show code repository -``` - -#### Step 6.2: Record Demo Video - -**Human Action Required:** -```bash -# Record screen -obs-studio # or similar - -# Or use ffmpeg -ffmpeg -f x11grab -video_size 1920x1080 -i :0.0 demo.mp4 -``` - -#### Step 6.3: Create Demo Report - -**Human Action Required:** -```markdown -# Demo Report Template - -## Date: YYYY-MM-DD -## Location: [Your location] -## Participants: [Names] - -## Results -- Transactions completed: X/Y -- Total value: X ERG -- Settlement time: X minutes -- Range tested: X meters - -## Issues Encountered -[List any problems and solutions] - -## Conclusion -[Success/failure and next steps] -``` - ---- - -## Summary: AI vs Human Responsibilities - -### AI Assistant Delivers ✅ - -| Artifact | Status | Location | -|----------|--------|----------| -| Documentation | ✅ Complete | `demo/basis/mesh/*.md` | -| Send scripts | ✅ Complete | `demo/basis/mesh/*.sh, *.py` | -| Listen scripts | ✅ Complete | `demo/basis/mesh/listen_*.py` | -| Message formats | ✅ Complete | `MESHTASTIC.md` | -| MCP server spec | ✅ Complete | `MCP_SERVER.md` | -| Testing checklist | ✅ Complete | `README.md` | - -### Humans Must Do 📋 - -| Task | Estimated Time | Skills Required | -|------|---------------|-----------------| -| Hardware purchase | 1-2 days | Shopping | -| Firmware flashing | 2-4 hours | Basic tech | -| Device configuration | 1-2 hours | Command line | -| Software installation | 2-4 hours | Python | -| Transaction testing | 1 day | 2 people | -| Tracker setup | 1-2 days | Python dev | -| Gateway setup | 1-2 days | Blockchain dev | -| Demo preparation | 1 day | Presentation | -| Field testing | 1-2 days | 2+ people | - -**Total Human Effort:** 10-15 days (with 2-3 people) - ---- - -## Critical Path - -``` -Hardware Purchase → Firmware Flash → First Transaction → Tracker Setup → Gateway Setup → Demo - (2 days) (4 hours) (1 day) (2 days) (2 days) (1 day) -``` - -**Minimum Time to Demo:** 7-8 days (accelerated) -**Recommended Time:** 10-15 days (comfortable pace) - ---- - -## Dependencies Not Yet Implemented - -The following components are **specified but not implemented**: - -1. **Tracker Server** (`chaincash.offchain.tracker`) - - Spec: `demo/basis/agents/SPEC.md` - - Status: Not implemented - - Human action: Implement or use mock - -2. **Gateway Service** (`chaincash.offchain.gateway`) - - Spec: `demo/basis/agents/SPEC.md` - - Status: Not implemented - - Human action: Implement or use mock - -3. **Mobile App** - - Spec: Future enhancement - - Status: Not started - - Human action: Future work - -**Workaround:** Use scripts directly without full tracker/gateway for initial demo. - ---- - -## Next Steps for Humans - -### Immediate (This Week) -1. [ ] Order hardware (2x Meshtastic devices) -2. [ ] Install Python dependencies (`pip install meshtastic`) -3. [ ] Test scripts with `--help` flag - -### Short Term (Next Week) -4. [ ] Flash firmware on devices -5. [ ] Configure Alice and Bob devices -6. [ ] Test first IOU transaction - -### Medium Term (2-3 Weeks) -7. [ ] Implement tracker server (or use mock) -8. [ ] Implement gateway service (or use mock) -9. [ ] Record demo video - -### Long Term (1-2 Months) -10. [ ] Field testing in real environment -11. [ ] Mobile app development -12. [ ] Production deployment - ---- - -**Questions?** See documentation in `demo/basis/mesh/` or contact the team. - -**Status:** AI implementation complete ✅ | Human implementation pending 📋 diff --git a/demo/basis/mesh/MCP_SERVER.md b/demo/basis/mesh/MCP_SERVER.md deleted file mode 100644 index 68e03b3..0000000 --- a/demo/basis/mesh/MCP_SERVER.md +++ /dev/null @@ -1,383 +0,0 @@ -# Meshtastic MCP Server Specification - -## Model Context Protocol Server for Meshtastic Mesh Networking - -This document specifies an MCP server that enables AI assistants to interact with Meshtastic mesh networks for sending and receiving Basis IOU messages. - ---- - -## Overview - -The Meshtastic MCP server provides these tools: -- `send_text` - Send text messages over mesh -- `send_basis_iou` - Send Basis IOU payments -- `listen_messages` - Listen for incoming messages -- `get_node_info` - Get local node information -- `get_mesh_nodes` - List visible mesh nodes - ---- - -## Server Implementation - -### Required Dependencies - -```python -# requirements.txt -meshtastic>=2.0.0 -mcp>=1.0.0 -pydantic>=2.0.0 -``` - -### Server Skeleton - -```python -#!/usr/bin/env python3 -""" -Meshtastic MCP Server - -Usage: - python mcp_server_meshtastic.py - # Or via uvx - uvx mcp-server-meshtastic -""" - -import asyncio -import json -from typing import Any -from meshtastic.serial_interface import SerialInterface -from meshtastic.tcp_interface import TCPInterface -from mcp.server import Server -from mcp.server.stdio import stdio_server -from pydantic import BaseModel, Field - -# Create server instance -server = Server("meshtastic") - -# Global interface -interface = None - -class SendTextArgs(BaseModel): - message: str - destination: str | None = None - channel: int = 0 - -class SendBasisIOUArgs(BaseModel): - payer: str - payee: str - amount: int # nanoERG - message: str = "Payment" - destination: str | None = None - compact: bool = True - -class ListenMessagesArgs(BaseModel): - duration: int = 30 - filter_iou: bool = False - -@server.tool("send_text") -async def send_text( - message: str, - destination: str | None = None, - channel: int = 0 -) -> dict[str, Any]: - """Send a text message over the mesh network.""" - global interface - - try: - if interface is None: - interface = SerialInterface() - - packet = interface.sendText(message, destinationId=destination) - - return { - "success": True, - "message_id": packet.get('id'), - "timestamp": int(time.time() * 1000), - "destination": destination or "broadcast" - } - except Exception as e: - return {"success": False, "error": str(e)} - -@server.tool("send_basis_iou") -async def send_basis_iou( - payer: str, payee: str, amount: int, - message: str = "Payment", - destination: str | None = None, - compact: bool = True -) -> dict[str, Any]: - """Send a Basis IOU payment message over the mesh.""" - import time - - # Create IOU message - if compact: - iou_data = { - "t": "iou", "v": "1.0", "p": payer, "y": payee, - "a": amount, "c": "nanoERG", "m": message, - "ts": int(time.time() * 1000) - } - message_text = json.dumps(iou_data, separators=(',', ':')) - else: - iou_data = { - "type": "iou_transfer", "version": "1.0", - "payer": payer, "payee": payee, "amount": amount, - "currency": "nanoERG", "message": message, - "timestamp": int(time.time() * 1000) - } - message_text = json.dumps(iou_data) - - result = await send_text(message_text, destination) - - if result["success"]: - return { - "success": True, - "iou_id": f"iou_{int(time.time())}", - "payer": payer, "payee": payee, - "amount": amount, "amount_erg": amount / 1e9, - "message_id": result["message_id"] - } - else: - return result - -@server.tool("listen_messages") -async def listen_messages(duration: int = 30, filter_iou: bool = False) -> dict[str, Any]: - """Start listening for incoming messages.""" - global interface - received = [] - - def on_receive(packet, intf): - try: - if 'decoded' in packet and 'payload' in packet['decoded']: - payload = packet['decoded']['payload'] - text = payload.decode('utf-8') - data = json.loads(text) - - if filter_iou: - msg_type = data.get('type') or data.get('t') - if msg_type not in ('iou_transfer', 'iou'): - return - - received.append({ - "type": data.get('type') or data.get('t'), - "from": packet.get('fromId'), - "data": data - }) - except: - pass - - try: - if interface is None: - interface = SerialInterface() - - from pubsub import pub - pub.subscribe(on_receive, 'meshtastic.receive') - await asyncio.sleep(duration) - - return { - "success": True, - "messages": received, - "total_received": len(received), - "duration": duration - } - except Exception as e: - return {"success": False, "error": str(e)} - -@server.tool("get_node_info") -async def get_node_info() -> dict[str, Any]: - """Get information about the local Meshtastic node.""" - global interface - - try: - if interface is None: - interface = SerialInterface() - - my_info = interface.myInfo - return { - "success": True, - "node": { - "id": f"!{my_info.my_node_num:08x}", - "long_name": my_info.long_name, - "short_name": my_info.short_name - } - } - except Exception as e: - return {"success": False, "error": str(e)} - -@server.tool("get_mesh_nodes") -async def get_mesh_nodes(recent_only: bool = True) -> dict[str, Any]: - """Get list of nodes visible in the mesh.""" - global interface - - try: - if interface is None: - interface = SerialInterface() - - import time - now = int(time.time()) - one_hour = 3600 - - nodes = [] - for node_id, node in interface.nodes.items(): - last_seen = node.get('lastHeard', 0) - if recent_only and (now - last_seen) > one_hour: - continue - - nodes.append({ - "id": node_id, - "long_name": node['user'].get('longName', 'Unknown'), - "short_name": node['user'].get('shortName', '?'), - "last_seen": last_seen - }) - - return {"success": True, "nodes": nodes, "total": len(nodes)} - except Exception as e: - return {"success": False, "error": str(e)} - -async def main(): - async with stdio_server() as (read_stream, write_stream): - await server.run(read_stream, write_stream, server.create_initialization_options()) - -if __name__ == "__main__": - asyncio.run(main()) -``` - ---- - -## Installation - -```bash -# Create virtual environment -python -m venv venv -source venv/bin/activate - -# Install dependencies -pip install meshtastic mcp pydantic - -# Install server -pip install -e . - -# Run server -python mcp_server_meshtastic.py -``` - ---- - -## Claude Desktop Configuration - -Add to `claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "meshtastic": { - "command": "python", - "args": ["/path/to/mcp_server_meshtastic.py"], - "env": { - "MESHTASTIC_PORT": "/dev/ttyUSB0" - } - } - } -} -``` - ---- - -## Example Interactions - -### Example 1: Send IOU Payment - -**User:** "Send 0.05 ERG from Alice to Bob" - -**Assistant uses tool:** -```json -{ - "name": "send_basis_iou", - "arguments": { - "payer": "alice", - "payee": "bob", - "amount": 50000000, - "message": "Payment for goods", - "destination": "!ba4bf9d0" - } -} -``` - -**Response:** -```json -{ - "success": true, - "iou_id": "iou_1711732200", - "payer": "alice", - "payee": "bob", - "amount": 50000000, - "amount_erg": 0.05, - "message_id": "msg_12345" -} -``` - -**Assistant responds:** "✅ Successfully sent 0.05 ERG from Alice to Bob over the mesh network." - ---- - -### Example 2: Check Mesh Status - -**User:** "What nodes are visible?" - -**Assistant uses tool:** -```json -{ - "name": "get_mesh_nodes", - "arguments": {"recent_only": true} -} -``` - -**Response:** -```json -{ - "success": true, - "nodes": [ - { - "id": "!12345678", - "long_name": "BasisTracker", - "short_name": "BT", - "last_seen": 1704000000, - "snr": 9.5, - "rssi": -72 - } - ], - "total": 1 -} -``` - ---- - -## Security - -### Encryption - -All mesh messages use channel PSK encryption: - -```python -def check_encryption(): - ch = interface.localNode.getChannelByIndex(0) - return ch.settings.psk != b'' -``` - -### Authentication - -Verify message sender: - -```python -def verify_sender(packet, expected): - return packet.get('fromId') == expected -``` - ---- - -## Resources - -- [MCP Specification](https://modelcontextprotocol.io/) -- [Meshtastic Documentation](https://meshtastic.org/) -- [Basis Protocol](../../README.md) - ---- - -**Status:** Specification complete ✅ | Implementation skeleton provided 📋 diff --git a/demo/basis/mesh/MESHTASTIC.md b/demo/basis/mesh/MESHTASTIC.md deleted file mode 100644 index 9f4e541..0000000 --- a/demo/basis/mesh/MESHTASTIC.md +++ /dev/null @@ -1,554 +0,0 @@ -# Sending Basis Messages over Meshtastic - -## Overview - -This guide shows how to send Basis protocol IOU messages over the **Meshtastic** mesh network using the command-line interface. - -Meshtastic is a real-world mesh networking technology using LoRa radios, perfect for offline Basis transactions in disconnected communities. - ---- - -## Prerequisites - -### 1. Install Meshtastic CLI - -```bash -# Install via pip -pip install meshtastic - -# Or via pipx (recommended for isolation) -pipx install meshtastic - -# Verify installation -meshtastic --version -``` - -### 2. Connect Meshtastic Device - -Connect your Meshtastic device (Rak4631, T-Beam, Heltec, etc.) via USB: - -```bash -# List available devices -meshtastic --info - -# Should show device info like: -# Owner: {id: !12345678, long_name: "My Node", short_name: "MN"} -# My info: ... -# Metadata: ... -``` - -### 3. Configure Device - -```bash -# Set device name -meshtastic --set owner.long_name "BasisTracker" - -# Set location (optional) -meshtastic --set location.lat 40.7128 -meshtastic --set location.lon -74.0060 - -# Configure for text messaging (default) -meshtastic --set channel.psks[0].key "AQ==" # Default key -``` - ---- - -## Sending Basis Messages - -### Message Format - -Basis IOU messages are JSON-encoded and sent as text: - -```json -{ - "type": "iou_transfer", - "version": "1.0", - "payer": "alice_node", - "payee": "bob_node", - "amount": 50000000, - "currency": "nanoERG", - "message": "Payment for goods", - "timestamp": 1704000000000, - "signature": "..." -} -``` - -### Basic Send Command - -```bash -# Send to all nodes (broadcast) -meshtastic --sendtext '{"type":"iou_transfer","amount":50000000}' - -# Send to specific node -meshtastic --dest !ba4bf9d0 --sendtext '{"type":"iou_transfer","amount":50000000}' - -# Send via specific port -meshtastic --port /dev/ttyUSB0 --sendtext '{"type":"iou_transfer","amount":50000000}' -``` - -### Complete Example with Acknowledgment - -```bash -# Send IOU and wait for acknowledgment -meshtastic \ - --port /dev/ttyUSB0 \ - --dest !ba4bf9d0 \ - --sendtext '{"type":"iou_transfer","payer":"alice","payee":"bob","amount":50000000}' \ - --ack \ - --timeout 60 -``` - ---- - -## Helper Script: send_basis_message.sh - -```bash -#!/bin/bash -# send_basis_message.sh - Send Basis IOU over Meshtastic - -set -e - -# Configuration -MESHTASTIC_PORT="${MESHTASTIC_PORT:-/dev/ttyUSB0}" -DEST_NODE="${DEST_NODE:-}" # Optional: !ba4bf9d0 -CH_INDEX="${CH_INDEX:-0}" - -# IOU parameters -PAYER="${1:-alice}" -PAYEE="${2:-bob}" -AMOUNT="${3:-50000000}" # nanoERG (default 0.05 ERG) -MESSAGE="${4:-Payment}" - -# Generate timestamp -TIMESTAMP=$(date +%s%3N) - -# Create JSON message -IOU_JSON=$(cat < dict: - """Create Basis IOU message.""" - return { - "type": "iou_transfer", - "version": "1.0", - "payer": payer, - "payee": payee, - "amount": amount, - "currency": "nanoERG", - "message": message, - "timestamp": int(time.time() * 1000), - "datetime": datetime.now().isoformat() - } - - -def send_message(interface, dest_id: str, message: dict, timeout: int = 30): - """Send message via Meshtastic interface.""" - json_str = json.dumps(message, separators=(',', ':')) - - print(f"Sending to: {dest_id or 'ALL'}") - print(f"Payload: {json_str}") - print(f"Length: {len(json_str)} bytes") - print() - - # Send text message - interface.sendText(json_str, destId=dest_id, wantAck=True) - - # Wait for acknowledgment - print(f"Waiting {timeout}s for acknowledgment...") - time.sleep(timeout) - - print("✓ Message sent") - - -def main(): - parser = argparse.ArgumentParser(description='Send Basis IOU over Meshtastic') - parser.add_argument('--payer', required=True, help='Payer node ID') - parser.add_argument('--payee', required=True, help='Payee node ID') - parser.add_argument('--amount', type=int, default=50000000, - help='Amount in nanoERG (default: 50000000 = 0.05 ERG)') - parser.add_argument('--message', default='Payment', help='Payment message') - parser.add_argument('--dest', help='Destination node ID (e.g., !ba4bf9d0)') - parser.add_argument('--port', default='/dev/ttyUSB0', help='Serial port') - parser.add_argument('--host', help='TCP host (alternative to serial)') - parser.add_argument('--ble', action='store_true', help='Use BLE connection') - parser.add_argument('--timeout', type=int, default=30, help='Ack timeout (seconds)') - - args = parser.parse_args() - - # Create IOU message - iou = create_iou_message( - payer=args.payer, - payee=args.payee, - amount=args.amount, - message=args.message - ) - - # Print summary - print("=" * 50) - print("Basis IOU over Meshtastic") - print("=" * 50) - print(f"Payer: {args.payer}") - print(f"Payee: {args.payee}") - print(f"Amount: {args.amount / 1e9:.9f} ERG") - print(f"Message: {args.message}") - print("=" * 50) - print() - - # Connect to device - try: - if args.ble: - print(f"Connecting via BLE...") - interface = BLEInterface() - elif args.host: - print(f"Connecting to TCP host {args.host}...") - interface = TCPInterface(args.host) - else: - print(f"Connecting to serial port {args.port}...") - interface = SerialInterface(args.port) - - # Send message - send_message(interface, args.dest, iou, args.timeout) - - # Close connection - interface.close() - - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -if __name__ == '__main__': - main() -``` - -### Usage - -```bash -# Install dependencies -pip install meshtastic - -# Make executable -chmod +x send_basis_iou.py - -# Basic usage (broadcast) -python send_basis_iou.py --payer alice --payee bob --amount 50000000 - -# Send to specific node -python send_basis_iou.py \ - --dest !ba4bf9d0 \ - --payer alice \ - --payee bob \ - --amount 50000000 \ - --message "Goods payment" - -# Via TCP -python send_basis_iou.py \ - --host 192.168.1.100 \ - --payer alice \ - --payee bob \ - --amount 100000000 - -# Via BLE -python send_basis_iou.py \ - --ble \ - --payer alice \ - --payee bob \ - --amount 50000000 -``` - ---- - -## Receiving Messages - -### Listen for Messages - -```bash -# Listen for all messages -meshtastic --port /dev/ttyUSB0 - -# Filter for Basis messages -meshtastic --port /dev/ttyUSB0 | grep '"type":"iou_transfer"' -``` - -### Python Receiver Script - -```python -#!/usr/bin/env python3 -"""Listen for Basis IOU messages over Meshtastic.""" - -import json -import sys -from meshtastic.serial_interface import SerialInterface -from pubsub import pub - -def on_receive(packet, interface): - """Callback for received packets.""" - if 'decoded' in packet and 'payload' in packet['decoded']: - payload = packet['decoded']['payload'] - - # Try to decode as text - try: - text = payload.decode('utf-8') - data = json.loads(text) - - # Check if it's a Basis IOU message - if data.get('type') == 'iou_transfer': - print("=" * 50) - print("📬 Received Basis IOU!") - print("=" * 50) - print(f"From: {packet.get('fromId', 'unknown')}") - print(f"Payer: {data.get('payer')}") - print(f"Payee: {data.get('payee')}") - print(f"Amount: {data.get('amount', 0) / 1e9:.9f} ERG") - print(f"Message: {data.get('message')}") - print(f"Time: {data.get('datetime', 'unknown')}") - print("=" * 50) - - except (json.JSONDecodeError, UnicodeDecodeError): - pass # Not a JSON message - -# Subscribe to receive events -pub.subscribe(on_receive, 'meshtastic.receive') - -# Connect to device -print("Listening for Basis IOU messages...") -print("Press Ctrl+C to exit") -print() - -try: - interface = SerialInterface() - while True: - time.sleep(1) -except KeyboardInterrupt: - print("\nExiting...") - interface.close() -``` - ---- - -## Message Size Limits - -Meshtastic has message size constraints: - -| Parameter | Limit | -|-----------|-------| -| Max text message | ~200 bytes (varies by firmware) | -| Recommended | < 150 bytes for reliability | - -### Compact IOU Format - -For constrained networks, use compact format: - -```json -{"t":"iou","p":"alice","y":"bob","a":50000000,"m":"payment","ts":1704000000000} -``` - -**Field abbreviations:** -- `t`: type -- `p`: payer -- `y`: payee -- `a`: amount -- `m`: message -- `ts`: timestamp - ---- - -## Security Considerations - -### 1. Encryption - -Meshtastic supports channel encryption: - -```bash -# Set encryption key (base64 encoded) -meshtastic --set channel.psks[0].key "YOUR_KEY_HERE" - -# Generate secure key -openssl rand -base64 32 -``` - -### 2. Signature Verification - -Always verify IOU signatures: - -```bash -# Message includes signature field -{"type":"iou_transfer",...,"signature":"..."} - -# Verify with Basis verification tool -python verify_iou_signature.py --message iou.json -``` - -### 3. Node Authentication - -Verify sender node ID: - -```python -def verify_sender(packet, expected_sender): - """Verify packet is from expected sender.""" - return packet.get('fromId') == expected_sender -``` - ---- - -## Testing - -### Loopback Test - -```bash -# Send message to self -NODE_ID=$(meshtastic --info | grep '"id"' | head -1 | cut -d'"' -f4) -meshtastic --dest $NODE_ID --sendtext '{"type":"test"}' -``` - -### Range Test - -```bash -# Node A (sender) -meshtastic --dest !NODE_B --sendtext "test 1" --ack - -# Node B (receiver, in different location) -meshtastic --sendtext "ack 1" --dest !NODE_A --ack -``` - ---- - -## Troubleshooting - -### Device Not Found - -```bash -# Check USB connection -ls -la /dev/ttyUSB* - -# Check permissions -sudo usermod -a -G dialout $USER -# Log out and back in -``` - -### Message Not Sending - -```bash -# Check device status -meshtastic --info - -# Check channel configuration -meshtastic --ch-index 0 --ch-info - -# Reset device if needed -meshtastic --reset -``` - -### Message Too Long - -```bash -# Check message length -echo -n '{"type":"iou_transfer",...}' | wc -c - -# Use compact format if > 150 bytes -``` - ---- - -## Resources - -- [Meshtastic Documentation](https://meshtastic.org/) -- [Meshtastic CLI Reference](https://meshtastic.org/docs/software/python/cli/usage/) -- [Meshtastic GitHub](https://github.com/meshtastic/Meshtastic-python) -- [Basis Protocol](../../README.md) - ---- - -**Next Steps:** -1. Set up Meshtastic devices in your community -2. Test IOU messaging with the scripts above -3. Integrate with Basis wallet app -4. Deploy in disconnected areas diff --git a/demo/basis/mesh/README.md b/demo/basis/mesh/README.md deleted file mode 100644 index 307e804..0000000 --- a/demo/basis/mesh/README.md +++ /dev/null @@ -1,644 +0,0 @@ -# Basis Mesh Network Demo - -## Community Trading Over Mesh Networks - -This demo showcases **Basis protocol** enabling peer-to-peer credit-based trading in disconnected or intermittently-connected communities using mesh networking technology. - ---- - -## Vision - -### Local Trust, Global Settlement - -The demo demonstrates how communities can: -- Trade locally **without Internet connectivity** -- Use **credit-based relationships** backed by trust -- Sync with blockchain when connectivity is available -- Enable **AI agents** to participate in autonomous economic relationships - ---- - -## Architecture - -``` -Disconnected Village (Mesh Network) -┌─────────────────────────────────────────────────────────┐ -│ │ -│ ┌──────┐ ┌──────┐ ┌──────┐ │ -│ │Alice │◄──►│ Bob │◄──►│Carol │ │ -│ │Phone │ │Phone │ │Phone │ │ -│ └──┬───┘ └──┬───┘ └──┬───┘ │ -│ │ │ │ │ -│ │ ┌──────┴──────┐ │ │ -│ └───►│ Tracker │◄───┘ │ -│ │ (Mesh) │ │ -│ └──────┬──────┘ │ -│ │ │ -│ ╭──────┴──────╮ │ -│ │ Gateway │◄────── Internet ─────────────►│ -│ │ (Online) │ (Blockchain) │ -│ └─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -### Components - -1. **Mesh Nodes** (Alice, Bob, Carol phones) - - Basis wallet app - - Bluetooth/WiFi Direct mesh connectivity - - Local IOU creation and transfer - - Offline transaction signing - -2. **Local Tracker** (Community server) - - Maintains offchain debt ledger - - Signs IOU notes for redemption - - Syncs with mesh nodes locally - - Aggregates transactions for blockchain sync - -3. **Gateway Node** (Internet bridge) - - Connects mesh to Ergo blockchain - - Submits reserve transactions - - Fetches blockchain state - - Broadcasts redemption transactions - ---- - -## Demo Scenarios - -### Scenario 1: Local Credit Trading (Offline) - -**Setup:** -- Alice and Bob in a village without Internet -- Both connected via mesh (Bluetooth/WiFi Direct/LoRa) -- Local tracker running on community server - -**Flow:** -``` -1. Alice buys goods from Bob (50 ERG value) - ├─ Alice's device creates IOU note - ├─ Bob's device receives IOU via mesh - ├─ Tracker signs the IOU (local sync) - └─ Transaction complete (no blockchain) - -2. Alice buys more goods (30 ERG value) - ├─ New IOU created and signed - ├─ Bob now holds 80 ERG in IOUs from Alice - └─ Tracker updates ledger - -3. Gateway syncs (when Internet available) - ├─ Bob aggregates IOUs (80 ERG total) - ├─ Gateway submits redemption transaction - └─ ERG credited to Bob's on-chain address -``` - -**Key Features Demonstrated:** -- ✅ Offline transaction creation -- ✅ Mesh propagation of IOU notes -- ✅ Local tracker signature -- ✅ Credit-based trading without collateral -- ✅ Batch settlement to reduce fees - ---- - -### Scenario 2: Blockchain Settlement (Online) - -**Setup:** -- Gateway node connects to Internet periodically -- Alice and Bob sync with gateway when available - -**Flow:** -``` -1. Gateway fetches blockchain state - ├─ Downloads latest reserve boxes - ├─ Updates local tracker state - └─ Broadcasts to Alice and Bob - -2. Bob redeems accumulated IOUs (80 ERG) - ├─ Gateway creates redemption transaction - ├─ Tracker provides signature - ├─ Transaction submitted to Ergo - └─ ERG credited to Bob's on-chain address - -3. Alice tops up reserve - ├─ Alice locks 100 ERG as collateral - ├─ Reserve box created on-chain - ├─ Gateway syncs to mesh - └─ Alice can issue more IOUs -``` - -**Key Features Demonstrated:** -- ✅ Intermittent connectivity support -- ✅ Batch settlement to reduce fees -- ✅ Reserve creation with ERG collateral -- ✅ Redemption against on-chain reserves - ---- - -### Scenario 3: Micropayments for Content - -**Setup:** -- Bob offers digital content (articles, reports) -- Alice pays per item with small IOUs -- Aggregated redemption weekly - -**Flow:** -``` -1. Alice accesses content (0.5 ERG) - ├─ Creates micro-IOU - ├─ Bob accepts (trusts Alice) - └─ Tracker logs transaction - -2. Alice accesses 20 items (10 ERG total) - ├─ IOUs accumulate - └─ Bob holds Alice's debt - -3. Weekly settlement - ├─ Bob aggregates all IOUs - ├─ Submits batch redemption - └─ ERG credited to Bob -``` - -**Key Features Demonstrated:** -- ✅ Micropayments without on-chain fees -- ✅ Trust-based acceptance -- ✅ Aggregated settlement -- ✅ Content monetization - ---- - -## Implementation Steps - -### Concrete Steps to Showcase Community Trading Over Mesh - -This section provides step-by-step instructions to implement and demonstrate Alice-Bob trading over a mesh network. - ---- - -### Phase 1: Setup Hardware (Day 1-2) - -**Required Hardware:** -- 2x Meshtastic devices (Rak4631, T-Beam, or Heltec WiFi LoRa) -- 1x Computer for tracker server (Raspberry Pi or laptop) -- 1x Computer for gateway (any Internet-connected device) - -**Step 1.1: Flash Meshtastic Firmware** -```bash -# Follow https://meshtastic.org/docs/getting-started/flashing-firmware -# Flash latest firmware to both devices -``` - -**Step 1.2: Configure Devices** -```bash -# Device 1 (Alice) -meshtastic --port /dev/ttyUSB0 --set owner.long_name "Alice" -meshtastic --port /dev/ttyUSB0 --set owner.short_name "A" - -# Device 2 (Bob) -meshtastic --port /dev/ttyUSB0 --set owner.long_name "Bob" -meshtastic --port /dev/ttyUSB0 --set owner.short_name "B" - -# Set same channel and encryption key on both -meshtastic --port /dev/ttyUSB0 --set channel.psks[0].key "AQ==" -``` - -**Step 1.3: Test Connectivity** -```bash -# From Alice's device, send test message -meshtastic --port /dev/ttyUSB0 --sendtext "Hello Bob" - -# On Bob's device, listen for messages -meshtastic --port /dev/ttyUSB0 -``` - ---- - -### Phase 2: Setup Tracker Server (Day 3-4) - -**Step 2.1: Install Dependencies** -```bash -# On tracker server (Raspberry Pi/laptop) -pip install meshtastic - -# Clone ChainCash repo -git clone https://github.com/ChainCashLabs/chaincash.git -cd chaincash -``` - -**Step 2.2: Configure Tracker** -```bash -# Copy participants template -cp participants.csv.template participants.csv - -# Edit with Alice and Bob details -# Format: name,address,secret_hex -alice,9f7ZX..., -bob,9f7ZY..., -tracker,9f7ZZ..., -``` - -**Step 2.3: Start Tracker Service** -```bash -# Start tracker server (listens for IOU requests) -# This maintains the debt ledger -python3 -m chaincash.offchain.tracker --config tracker.conf -``` - ---- - -### Phase 3: First IOU Transaction (Day 5) - -**Step 3.1: Alice Creates IOU** -```bash -# On Alice's device (or via tracker) -cd demo/mesh -./send_basis_message.sh alice bob 50000000 "Goods payment" -``` - -**Step 3.2: Bob Receives IOU** -```bash -# On Bob's device (listening) -python3 listen_basis_iou.py --port /dev/ttyUSB0 - -# Should see: -# 📬 Received Basis IOU! -# From: !alice_node_id -# Payer: alice -# Payee: bob -# Amount: 0.050000000 ERG -``` - -**Step 3.3: Tracker Signs IOU** -```bash -# Tracker automatically signs and logs -# Check tracker logs: -tail -f tracker.log - -# Should show: -# [INFO] Signed IOU: alice->bob 50000000 nanoERG -# [INFO] Ledger updated: bob balance +50000000 -``` - -**Step 3.4: Verify in Ledger** -```bash -# Check tracker ledger -cat ledger.json - -# Should show: -{ - "alice": {"issued": 50000000, "redeemed": 0}, - "bob": {"received": 50000000, "redeemed": 0} -} -``` - ---- - -### Phase 4: Multiple Transactions (Day 6) - -**Step 4.1: Alice Buys More** -```bash -# Second transaction -./send_basis_message.sh alice bob 30000000 "More goods" - -# Third transaction -./send_basis_message.sh alice bob 20000000 "Services" -``` - -**Step 4.2: Check Total Balance** -```bash -# Bob's total IOUs from Alice -python3 -c " -import json -with open('ledger.json') as f: - ledger = json.load(f) - print(f'Bob holds: {ledger[\"bob\"][\"received\"] / 1e9} ERG from Alice') -" - -# Should show: Bob holds: 0.100000000 ERG from Alice -``` - ---- - -### Phase 5: Gateway Settlement (Day 7) - -**Step 5.1: Connect Gateway to Internet** -```bash -# On gateway machine (Internet-connected) -export ERGO_NODE_URL=http://node.api.url:9053 -export ERGO_API_KEY=your_api_key -``` - -**Step 5.2: Sync Blockchain State** -```bash -# Fetch latest reserve boxes -python3 -m chaincash.offchain.gateway --sync - -# Should show: -# [INFO] Synced 1 reserve boxes -# [INFO] Synced 1 tracker boxes -``` - -**Step 5.3: Bob Redeems IOUs** -```bash -# Bob aggregates and redeems -python3 -m chaincash.offchain.gateway --redeem --holder bob - -# Should show: -# [INFO] Aggregated 3 IOUs totaling 0.1 ERG -# [INFO] Redemption transaction created -# [INFO] TxId: -# [INFO] Awaiting confirmation... -``` - -**Step 5.4: Verify On-Chain** -```bash -# Check Ergo explorer -curl https://api.ergoplatform.com/transactions/ - -# Check Bob's balance -curl https://api.ergoplatform.com/addresses/bob_address - -# Should show increased ERG balance -``` - ---- - -### Phase 6: Demo Presentation (Day 8) - -**Step 6.1: Prepare Demo Script** -```bash -# Create demo script -cat > demo_script.md << 'EOF' -# Mesh Trading Demo Script - -## Setup (5 min) -1. Show Alice and Bob devices (Meshtastic) -2. Show tracker server running -3. Show ledger (empty initially) - -## Transaction 1 (5 min) -1. Alice sends IOU: ./send_basis_message.sh alice bob 50000000 -2. Bob receives on device (show screen) -3. Tracker signs (show log) -4. Ledger updated (show JSON) - -## Transaction 2 (3 min) -1. Alice sends another IOU -2. Bob's balance increases -3. Explain: No blockchain yet! - -## Settlement (5 min) -1. Connect gateway to Internet -2. Bob redeems: python3 gateway.py --redeem -3. Show transaction on Ergo explorer -4. Bob's balance updated on-chain - -## Summary (2 min) -- Offline trading works via mesh -- Tracker maintains debt ledger -- Blockchain used only for final settlement -- No forced collateralization -EOF -``` - -**Step 6.2: Record Demo Video** -```bash -# Record screen during demo -ffmpeg -f x11grab -video_size 1920x1080 -i :0.0 -c:v libx264 demo.mp4 - -# Or use OBS Studio for better quality -``` - -**Step 6.3: Create Demo Report** -```bash -# Document results -cat > demo_report.md << 'EOF' -# Mesh Trading Demo Report - -## Date: 2026-03-29 -## Participants: Alice, Bob -## Location: [Your location] - -## Results - -### Transactions Completed -- Transaction 1: 0.05 ERG (Alice→Bob) ✅ -- Transaction 2: 0.03 ERG (Alice→Bob) ✅ -- Transaction 3: 0.02 ERG (Alice→Bob) ✅ - -### Settlement -- Total redeemed: 0.10 ERG ✅ -- On-chain transaction: -- Confirmation time: ~2 minutes - -### Performance -- Message latency: <1 second (mesh) -- Settlement time: ~2 minutes (blockchain) -- Message size: ~150 bytes -- Range: ~200 meters (urban) - -## Conclusion -Successfully demonstrated offline credit trading between Alice and Bob -using Meshtastic mesh network with Basis protocol. -EOF -``` - ---- - -## Technical Implementation - -### Mesh Network Stack - -``` -┌─────────────────────────────────────┐ -│ Alice's Device │ -│ (Meshtastic + Basis) │ -├─────────────────────────────────────┤ -│ IOU Creation & Signing │ -├─────────────────────────────────────┤ -│ Mesh Message Layer │ -│ (LoRa / Bluetooth / WiFi) │ -├─────────────────────────────────────┤ -│ Physical Radio │ -└─────────────────────────────────────┘ - ↕ (mesh radio) -┌─────────────────────────────────────┐ -│ Bob's Device │ -│ (Meshtastic + Basis) │ -├─────────────────────────────────────┤ -│ IOU Reception & Verification │ -├─────────────────────────────────────┤ -│ Mesh Message Layer │ -│ (LoRa / Bluetooth / WiFi) │ -├─────────────────────────────────────┤ -│ Physical Radio │ -└─────────────────────────────────────┘ - ↕ (USB/Serial) -┌─────────────────────────────────────┐ -│ Tracker Server │ -│ (Community Ledger Keeper) │ -├─────────────────────────────────────┤ -│ Debt Ledger Database │ -│ IOU Signing Service │ -└─────────────────────────────────────┘ - ↕ (Internet when available) -┌─────────────────────────────────────┐ -│ Gateway Node │ -│ (Blockchain Bridge) │ -├─────────────────────────────────────┤ -│ Ergo Node Connection │ -│ Settlement Service │ -└─────────────────────────────────────┘ -``` - -### Message Types - -1. **IOU Transfer** (Compact format for LoRa) - ```json - {"t":"iou","v":"1.0","p":"alice","y":"bob","a":50000000,"c":"nanoERG","m":"Goods payment","ts":1704000000000} - ``` - -2. **Tracker Sync** - ```json - { - "type": "tracker_sync", - "ledgerUpdate": [{"payer":"alice","payee":"bob","amount":50000000}], - "trackerSignature": "...", - "sequenceNumber": 42 - } - ``` - -3. **Blockchain State** - ```json - { - "type": "blockchain_state", - "reserveBoxes": [...], - "trackerBox": {...}, - "blockHeight": 1750000, - "gatewaySignature": "..." - } - ``` - -### Security Considerations - -1. **Mesh Layer** - - End-to-end encryption for IOU transfers - - Device authentication via public keys - - Replay attack prevention (timestamps, sequence numbers) - -2. **Tracker** - - Cannot steal funds (requires reserve owner signature) - - Cannot forge IOUs (requires payer signature) - - Auditable ledger (all signatures recorded) - -3. **Blockchain** - - Final settlement layer - - Collateral locking for trustless issuance - - Public audit trail - ---- - -## Testing Checklist - -### Hardware Testing -- [ ] Both Meshtastic devices flash successfully -- [ ] Devices can communicate (ping test) -- [ ] Range test: 100m, 200m, 500m -- [ ] Battery life acceptable (>4 hours) - -### Software Testing -- [ ] `send_basis_message.sh` sends IOU successfully -- [ ] `listen_basis_iou.py` receives IOU -- [ ] Tracker signs IOU correctly -- [ ] Ledger updates properly - -### Integration Testing -- [ ] End-to-end: Alice→Bob IOU created and received -- [ ] Multiple IOUs accumulate correctly -- [ ] Gateway syncs with blockchain -- [ ] Redemption transaction succeeds - -### Field Testing -- [ ] Works without Internet (offline mode) -- [ ] Intermittent connectivity handled -- [ ] Real-world range (urban environment) -- [ ] Demo runs smoothly end-to-end - ---- - -## Success Metrics - -### Technical -- Message latency: < 1 second (mesh) -- Settlement time: < 10 minutes (blockchain) -- Message size: < 180 bytes (LoRa compatible) -- Range: > 100 meters (urban) - -### Economic -- IOUs created: Multiple transactions -- Total value: Test with real ERG amounts -- Settlement: Successful on-chain redemption -- Default rate: 0% (all IOUs redeemed) - ---- - -## Future Enhancements - -1. **Multi-Tracker Support** - - Redundant trackers for fault tolerance - - Seamless tracker migration - -2. **Cross-Mesh Trading** - - Bridge different mesh networks - - Inter-community credit - -3. **Mobile App** - - Android/iOS wallet app - - Built-in Meshtastic support - - QR code IOU sharing - -4. **Advanced Features** - - Payment channels for frequent traders - - Conditional IOUs (escrow) - - Recurring payments - ---- - -## Resources - -### Documentation -- [Meshtastic Integration Guide](./MESHTASTIC.md) - Complete guide for sending Basis messages over Meshtastic -- [Basis Protocol Whitepaper](../../docs/conf/conf.pdf) -- [Presentation](../../docs/presentation/presentation.md) -- [Ergo Documentation](https://docs.ergoplatform.com/) -- [Mesh Networking Protocols](https://en.wikipedia.org/wiki/Mesh_networking) - -### Code & Scripts -- `send_basis_message.sh` - Bash script for sending IOUs via Meshtastic CLI -- `send_basis_iou.py` - Python script with advanced options -- `listen_basis_iou.py` - Python script for receiving IOUs - -### Hardware -- [Meshtastic Project](https://meshtastic.org/) - LoRa mesh networking -- [Meshtastic CLI](https://meshtastic.org/docs/software/python/cli/usage/) - Command-line interface -- [Supported Devices](https://meshtastic.org/docs/hardware/devices/) - Rak4631, T-Beam, Heltec, etc. - -### Community -- Telegram: [t.me/chaincashtalks](https://t.me/chaincashtalks) -- Twitter: [@ChainCashLabs](https://twitter.com/ChainCashLabs) - ---- - -## License - -This demo is part of the ChainCash project, released under a permissive open-source license. See [LICENSE](../../LICENSE) for details. - ---- - -**Built by and for the Commons** 🌱 - -Free, open source community project. No token, no VC, no corporate control. - -**For Disconnected Communities** 📡 - -Enabling offline credit trading via mesh networks. diff --git a/demo/basis/mesh/listen_basis_iou.py b/demo/basis/mesh/listen_basis_iou.py deleted file mode 100755 index b062163..0000000 --- a/demo/basis/mesh/listen_basis_iou.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 -""" -Listen for Basis IOU messages over Meshtastic. - -Usage: - python listen_basis_iou.py - python listen_basis_iou.py --port /dev/ttyACM0 - python listen_basis_iou.py --host 192.168.1.100 - -Requirements: - pip install meshtastic pubsub -""" - -import json -import time -import sys -import argparse -from datetime import datetime - -try: - from meshtastic.serial_interface import SerialInterface - from meshtastic.tcp_interface import TCPInterface - from meshtastic.ble_interface import BLEInterface - from pubsub import pub -except ImportError as e: - print(f"Error: Missing dependency - {e.name}") - print("Install with: pip install meshtastic pubsub") - sys.exit(1) - - -# Statistics -stats = { - 'total_messages': 0, - 'basis_messages': 0, - 'start_time': time.time() -} - - -def on_receive(packet, interface): - """Callback for received packets.""" - stats['total_messages'] += 1 - - if 'decoded' not in packet or 'payload' not in packet['decoded']: - return - - payload = packet['decoded']['payload'] - - # Try to decode as text - try: - text = payload.decode('utf-8') - - # Try to parse as JSON - try: - data = json.loads(text) - - # Check if it's a Basis IOU message - msg_type = data.get('type') or data.get('t') - if msg_type in ['iou_transfer', 'iou']: - stats['basis_messages'] += 1 - - # Extract fields (support both full and compact format) - payer = data.get('payer') or data.get('p', 'unknown') - payee = data.get('payee') or data.get('y', 'unknown') - amount = data.get('amount') or data.get('a', 0) - message = data.get('message') or data.get('m', '') - timestamp = data.get('timestamp') or data.get('ts', 0) - - # Convert timestamp to datetime - if timestamp: - dt = datetime.fromtimestamp(timestamp / 1000) - time_str = dt.strftime('%Y-%m-%d %H:%M:%S') - else: - time_str = 'unknown' - - # Print formatted message - print() - print("=" * 60) - print(" 📬 Received Basis IOU!") - print("=" * 60) - print(f" From: {packet.get('fromId', 'unknown')}") - print(f" Payer: {payer}") - print(f" Payee: {payee}") - print(f" Amount: {amount / 1e9:.9f} ERG ({amount} nanoERG)") - print(f" Message: {message}") - print(f" Time: {time_str}") - print(f" RX RSSI: {packet.get('rxRssi', 'N/A')} dBm") - print(f" RX SNR: {packet.get('rxSnr', 'N/A')} dB") - print("=" * 60) - - # Save to file - save_iou(packet, data) - - except json.JSONDecodeError: - pass # Not JSON - except UnicodeDecodeError: - pass # Not text - - -def save_iou(packet: dict, data: dict): - """Save IOU to file for later processing.""" - timestamp = int(time.time()) - filename = f"iou_{timestamp}.json" - - record = { - 'received_at': datetime.now().isoformat(), - 'from_node': packet.get('fromId'), - 'to_node': packet.get('toId'), - 'iou_data': data, - 'rx_info': { - 'rssi': packet.get('rxRssi'), - 'snr': packet.get('rxSnr'), - 'hop_limit': packet.get('hopLimit'), - } - } - - try: - with open(filename, 'w') as f: - json.dump(record, f, indent=2) - print(f" 💾 Saved to: {filename}") - except Exception as e: - print(f" ⚠️ Could not save: {e}") - - -def print_stats(): - """Print statistics.""" - elapsed = time.time() - stats['start_time'] - hours = elapsed / 3600 - - print() - print("=" * 60) - print(" Statistics") - print("=" * 60) - print(f" Runtime: {elapsed/60:.1f} minutes") - print(f" Total messages: {stats['total_messages']}") - print(f" Basis IOUs: {stats['basis_messages']}") - if elapsed > 0: - print(f" Msgs/hour: {stats['total_messages']/hours:.1f}") - print("=" * 60) - - -def main(): - parser = argparse.ArgumentParser(description='Listen for Basis IOU messages') - parser.add_argument('--port', default='/dev/ttyUSB0', help='Serial port') - parser.add_argument('--host', help='TCP host (alternative to serial)') - parser.add_argument('--ble', action='store_true', help='Use BLE connection') - parser.add_argument('--debug', action='store_true', help='Show all messages') - parser.add_argument('--no-save', action='store_true', help='Do not save IOUs to file') - - args = parser.parse_args() - - print("=" * 60) - print(" Basis IOU Listener") - print("=" * 60) - print(f" Port: {args.port if not args.host else args.host}") - print(f" Mode: {'BLE' if args.ble else 'Serial' if not args.host else 'TCP'}") - print("=" * 60) - print() - print("Listening for Basis IOU messages...") - print("Press Ctrl+C to exit") - print() - - # Subscribe to receive events - pub.subscribe(on_receive, 'meshtastic.receive') - - # Connect to device - try: - if args.ble: - interface = BLEInterface() - elif args.host: - interface = TCPInterface(args.host) - else: - interface = SerialInterface(args.port) - - print("Connected!") - print() - - # Main loop - while True: - time.sleep(1) - - # Print stats every 5 minutes - if int(time.time()) % 300 == 0 and stats['total_messages'] > 0: - print_stats() - - except KeyboardInterrupt: - print("\n\nExiting...") - print_stats() - interface.close() - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/demo/basis/mesh/send_basis_iou.py b/demo/basis/mesh/send_basis_iou.py deleted file mode 100755 index bbf2de2..0000000 --- a/demo/basis/mesh/send_basis_iou.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -""" -Send Basis IOU messages over Meshtastic mesh network. - -Usage: - python send_basis_iou.py --payer alice --payee bob --amount 50000000 - python send_basis_iou.py --dest !ba4bf9d0 --payer alice --payee bob --amount 50000000 - -Requirements: - pip install meshtastic -""" - -import argparse -import json -import time -import sys -from datetime import datetime - -try: - from meshtastic.serial_interface import SerialInterface - from meshtastic.tcp_interface import TCPInterface - from meshtastic.ble_interface import BLEInterface -except ImportError: - print("Error: meshtastic package not installed") - print("Install with: pip install meshtastic") - sys.exit(1) - - -def create_iou_message(payer: str, payee: str, amount: int, - message: str = "Payment", compact: bool = False) -> dict: - """Create Basis IOU message. - - Args: - payer: Payer node ID - payee: Payee node ID - amount: Amount in nanoERG - message: Payment description - compact: Use compact format for LoRa constraints - - Returns: - IOU message dictionary - """ - if compact: - # Compact format for constrained networks - return { - "t": "iou", - "v": "1.0", - "p": payer, - "y": payee, - "a": amount, - "c": "nanoERG", - "m": message, - "ts": int(time.time() * 1000) - } - else: - # Full format - return { - "type": "iou_transfer", - "version": "1.0", - "payer": payer, - "payee": payee, - "amount": amount, - "currency": "nanoERG", - "message": message, - "timestamp": int(time.time() * 1000), - "datetime": datetime.now().isoformat() - } - - -def send_message(interface, dest_id: str, message: dict, timeout: int = 30, - want_ack: bool = True): - """Send message via Meshtastic interface. - - Args: - interface: Meshtastic interface object - dest_id: Destination node ID or None for broadcast - message: Message dictionary - timeout: Wait time for acknowledgment - want_ack: Request acknowledgment - """ - json_str = json.dumps(message, separators=(',', ':')) - - print(f"Sending to: {dest_id or 'ALL (broadcast)'}") - print(f"Payload: {json_str}") - print(f"Length: {len(json_str)} bytes") - - # Warn if message is too long - if len(json_str) > 180: - print(f"⚠️ WARNING: Message size may exceed LoRa limits!") - - print() - - # Send text message - interface.sendText(json_str, destId=dest_id, wantAck=want_ack) - - if want_ack: - # Wait for acknowledgment - print(f"Waiting {timeout}s for acknowledgment...") - time.sleep(timeout) - - print("✓ Message sent") - - -def main(): - parser = argparse.ArgumentParser( - description='Send Basis IOU over Meshtastic', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s --payer alice --payee bob --amount 50000000 - %(prog)s --dest !ba4bf9d0 --payer alice --payee bob --amount 50000000 - %(prog)s --host 192.168.1.100 --payer alice --payee bob --amount 100000000 - %(prog)s --ble --payer alice --payee bob --amount 50000000 - """ - ) - parser.add_argument('--payer', required=True, help='Payer node ID') - parser.add_argument('--payee', required=True, help='Payee node ID') - parser.add_argument('--amount', type=int, default=50000000, - help='Amount in nanoERG (default: 50000000 = 0.05 ERG)') - parser.add_argument('--message', default='Payment', help='Payment message') - parser.add_argument('--dest', help='Destination node ID (e.g., !ba4bf9d0)') - parser.add_argument('--port', default='/dev/ttyUSB0', help='Serial port') - parser.add_argument('--host', help='TCP host (alternative to serial)') - parser.add_argument('--ble', action='store_true', help='Use BLE connection') - parser.add_argument('--timeout', type=int, default=30, help='Ack timeout (seconds)') - parser.add_argument('--compact', action='store_true', - help='Use compact JSON format for LoRa') - parser.add_argument('--no-ack', action='store_true', - help='Do not wait for acknowledgment') - - args = parser.parse_args() - - # Create IOU message - iou = create_iou_message( - payer=args.payer, - payee=args.payee, - amount=args.amount, - message=args.message, - compact=args.compact - ) - - # Print summary - print("=" * 60) - print(" Basis IOU over Meshtastic") - print("=" * 60) - print(f" Payer: {args.payer}") - print(f" Payee: {args.payee}") - print(f" Amount: {args.amount / 1e9:.9f} ERG ({args.amount} nanoERG)") - print(f" Message: {args.message}") - print(f" Format: {'Compact' if args.compact else 'Full'}") - print("=" * 60) - print() - - # Connect to device - try: - if args.ble: - print(f"Connecting via BLE...") - interface = BLEInterface() - elif args.host: - print(f"Connecting to TCP host {args.host}...") - interface = TCPInterface(args.host) - else: - print(f"Connecting to serial port {args.port}...") - interface = SerialInterface(args.port) - - print("Connected!") - print() - - # Send message - send_message( - interface, - args.dest, - iou, - args.timeout, - want_ack=not args.no_ack - ) - - # Close connection - interface.close() - - print() - print("Next steps:") - print(" 1. Wait for tracker signature") - print(" 2. Verify IOU on receiver device") - print(" 3. Record in local ledger") - - except KeyboardInterrupt: - print("\nInterrupted by user") - sys.exit(0) - except Exception as e: - print(f"Error: {e}") - print() - print("Troubleshooting:") - print(" - Check USB connection: ls -la /dev/ttyUSB*") - print(" - Check permissions: sudo usermod -a -G dialout $USER") - print(" - Try different port: --port /dev/ttyACM0") - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/demo/basis/mesh/send_basis_message.sh b/demo/basis/mesh/send_basis_message.sh deleted file mode 100755 index 02f81c6..0000000 --- a/demo/basis/mesh/send_basis_message.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash -# send_basis_message.sh - Send Basis IOU over Meshtastic -# -# Usage: -# ./send_basis_message.sh alice bob 50000000 "Goods payment" -# DEST_NODE=!ba4bf9d0 ./send_basis_message.sh alice bob 50000000 -# MESHTASTIC_PORT=/dev/ttyACM0 ./send_basis_message.sh alice bob 100000000 -# -# Environment variables: -# MESHTASTIC_PORT - Serial port (default: /dev/ttyUSB0) -# DEST_NODE - Destination node ID (optional, broadcasts if not set) -# CH_INDEX - Channel index (default: 0) - -set -e - -# Configuration -MESHTASTIC_PORT="${MESHTASTIC_PORT:-/dev/ttyUSB0}" -DEST_NODE="${DEST_NODE:-}" -CH_INDEX="${CH_INDEX:-0}" - -# IOU parameters -PAYER="${1:-alice}" -PAYEE="${2:-bob}" -AMOUNT="${3:-50000000}" # nanoERG (default 0.05 ERG) -MESSAGE="${4:-Payment}" - -# Generate timestamp -TIMESTAMP=$(date +%s%3N) - -# Create JSON message (compact format for LoRa) -IOU_JSON=$(cat <", - "extension": { - "0": "0200", // action=0 (REDEEM), index=0 - "1": "", // GroupElement - "2": "", // Coll[Byte] (65 bytes) - "3": "", // Long (50000000) - "5": "", // Coll[Byte] (70 bytes) - "6": "", // Coll[Byte] (65 bytes) - "8": "" // Coll[Byte] (113 bytes) - } -} -``` - -**Fee Inputs (4 boxes):** -```json -{ - "boxId": "", - "extension": {} // Empty extension required for signing -} -``` - -**Data Input (1 box):** -```json -{ - "boxId": "" // No extension needed -} -``` - -### 3.2 Output Structure - -**Reserve Output:** -```json -{ - "ergoTree": "", - "creationHeight": , - "value": 50000000, - "assets": [{"tokenId": "", "amount": 1}], - "additionalRegisters": { - "R4": "", - "R5": "", // Tree after insert - "R6": "0e20" - } -} -``` - -**Receiver Output:** -```json -{ - "ergoTree": "0008cd", // P2PK address - "creationHeight": , - "value": 50000000, - "assets": [], - "additionalRegisters": {} -} -``` - -**Fee Recipient Output:** -```json -{ - "ergoTree": "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304", - "creationHeight": , - "value": 1000000, - "assets": [], - "additionalRegisters": {} -} -``` - -### 3.3 Balance Calculation - -``` -Inputs: - Reserve box: 100000000 nanoERG - Fee boxes (4x): 1000000 nanoERG (4 × 250000) - ----------- - Total input: 101000000 nanoERG - -Outputs: - Reserve output: 50000000 nanoERG - Receiver output: 50000000 nanoERG - Fee recipient: 1000000 nanoERG - ----------- - Total output: 101000000 nanoERG - -Balance: 101000000 - 101000000 = 0 ✓ -``` - -**Note:** Ergo unsigned transactions don't have a "fee" field. The fee is implicit as `inputs - outputs`. - ---- - -## 4. AVL Tree Specification - -### 4.1 Tree Parameters - -| Parameter | Value | -|-----------|-------| -| Key Length | 32 bytes (Blake2b256 hash) | -| Value Length | Variable (8 bytes for Long) | -| Flags | InsertOnly (0x01) | -| Plasma Parameters | (32, None) | - -### 4.2 Key Construction - -```scala -val key = Blake2b256(ownerKeyBytes ++ receiverKeyBytes) -// Example: 6995ccf33c8a09705612e6ee3808bb4cedb48cb7b7c019ecdc68b74e7ed912a4 -``` - -### 4.3 Value Encoding - -```scala -val value = Longs.toByteArray(amount) -// Example: 0000000002faf080 (50000000 in big-endian) -``` - -### 4.4 Tree Serialization Format - -``` -Byte 0: Type tag (0x64 for AvlTree) -Bytes 1-33: Digest (32-byte hash + 1-byte height) -Byte 34: Flags -Bytes 35-38: Key length (4 bytes, big-endian) -Bytes 39-42: Value length option (4 bytes if present) -``` - -**Example (Empty Tree):** -``` -64 4ec61f485b98eb87153f7c57db4f5ecd75556fddbc403b41acf8441fde8e1609 00 01 20 00 -│ ││ │ │ │ │ │ -│ └─ Digest (33 bytes) │ │ │ │ └─ Value length (None) -│ │ │ │ └──── Key length (32) -│ │ │ └─────── Flags (0x01 = InsertOnly) -└─ Type (0x64) └─ Height (0) -``` - -### 4.5 Proof Generation - -**Reserve Insert Proof (Context Var 5):** -```scala -val plasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, chainCashPlasmaParameters) -val insertResult = plasmaMap.insert((key, value)) -val proof = insertResult.proof.bytes // 70 bytes for empty→single insert -``` - -**Tracker Lookup Proof (Context Var 8):** -```scala -val plasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, chainCashPlasmaParameters) -plasmaMap.insert((key, value)) -val lookupResult = plasmaMap.lookUp(key) -val proof = lookupResult.proof.bytes // 113 bytes -``` - ---- - -## 5. Signature Specification - -### 5.1 Message Construction - -```scala -val key = Blake2b256(ownerKeyBytes ++ receiverKeyBytes) -val message = key ++ Longs.toByteArray(totalDebt) -// Total: 40 bytes (32 + 8) -``` - -**Example:** -``` -6995ccf33c8a09705612e6ee3808bb4cedb48cb7b7c019ecdc68b74e7ed912a4 0000000002faf080 -││ ││ -└─ Key (Blake2b256 of owner||receiver) └─ Debt (50000000) -``` - -### 5.2 Signature Generation - -```scala -val (a, z) = SigUtils.sign(message, secretKey) -// a: GroupElement (33 bytes compressed) -// z: BigInt (≤ 255 bits) -``` - -### 5.3 Signature Encoding - -**CRITICAL:** Use BouncyCastle for fixed-width encoding: - -```scala -// CORRECT: -val zBytes = BigIntegers.asUnsignedByteArray(32, z.bigInteger) -val sigBytes = GroupElementSerializer.toBytes(a) ++ zBytes -// Total: 65 bytes (33 + 32) - -// WRONG (causes verification failure): -val sigBytes = GroupElementSerializer.toBytes(a) ++ z.toByteArray -// May be 66 bytes if z has sign byte! -``` - -### 5.4 Signature Verification - -```scala -val e = Blake2b256(a.getEncoded.toArray ++ message ++ pk.getEncoded.toArray) -val eBigInt = BigInt(e) -val lhs = g.exp(z.bigInteger) -val rhs = a.multiply(pk.exp(eBigInt.bigInteger)) -lhs == rhs // true if valid -``` - ---- - -## 6. Contract Conditions - -The basis.es contract checks: - -```scala -sigmaProp( - selfPreserved && // Output contract unchanged - trackerIdCorrect && // Tracker NFT matches R6 - trackerDebtCorrect && // Debt exists in tracker tree - properRedemptionTree && // AVL tree updated correctly - properReserveSignature && // Reserve owner signed - properlyRedeemed && // Amount <= (totalDebt - redeemedDebt) - receiverCondition // Receiver pubkey verified -) -``` - -### 6.1 Condition Details - -**selfPreserved:** -- Output proposition bytes == input proposition bytes -- Output tokens == input tokens -- Output R4 == input R4 -- Output R6 == input R6 - -**trackerIdCorrect:** -- `tracker.tokens(0)._1 == SELF.R6[Coll[Byte]].get` - -**trackerDebtCorrect:** -- `trackerTree.get(key, proof).get == totalDebt` - -**properRedemptionTree:** -- `SELF.R5.insert((key, value), proof) == selfOut.R5` - -**properReserveSignature:** -- Schnorr signature verification with message = `key || totalDebt` - -**properlyRedeemed:** -- `redeemed > 0` -- `redeemed <= (totalDebt - redeemedDebt)` -- Tracker signature valid - -**receiverCondition:** -- `proveDlog(receiver)` - ---- - -## 7. Fee Handling - -### 7.1 Fee Box Requirements - -- **Value:** 250000 nanoERG each -- **Quantity:** 4 boxes -- **Assets:** Empty array `[]` -- **Extension:** Empty `{}` (required for signing) - -### 7.2 Finding Fee Boxes - -```bash -curl "http://localhost:9053/wallet/boxes/unspent" | \ - python3 -c "import json,sys; [print(b['box']['boxId']) for b in json.load(sys.stdin) if b['box']['value']==250000 and not b['box']['assets']]" | head -4 | tr '\n' ',' | sed 's/,$//' -``` - -### 7.3 Fee Recipient - -The fee is paid to a hardcoded address: - -``` -ergoTree: 1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304 -address: 2iHkR7CWvD1R4j1yZg5bkeDRQavjAaVPeTDFGGLZduHyfWMuYpmhHocX8GJoaieTx78FntzJbCBVL6rf96ocJoZdmWBL2fci7NqWgAirppPQmZ7fN9V6z13Ay6brPriBKYqLp1bT2Fk4FkFLCfdPpe -``` - ---- - -## 8. Command Line Interface - -### 8.1 BasisNoteRedeemer - -```bash -sbt "runMain chaincash.contracts.BasisNoteRedeemer \ - --note-json \ - --reserve-box \ - --tracker-box \ - --fee-box \ - --output \ - --reserve-owner-secret \ - --tracker-secret " -``` - -**Parameters:** -| Parameter | Required | Default | Description | -|-----------|----------|---------|-------------| -| `--note-json` | Yes | - | IOU note JSON file | -| `--reserve-box` | No | auto | Reserve box ID or 'auto' for scan API | -| `--tracker-box` | No | auto | Tracker box ID or 'auto' for scan API | -| `--fee-box` | No | - | Comma-separated fee box IDs | -| `--output` | No | stdout | Output file | -| `--reserve-owner-secret` | No | Alice | Reserve owner secret (hex) | -| `--tracker-secret` | No | Tracker | Tracker secret (hex) | - -### 8.2 Scan Configuration - -| Scan ID | Purpose | Box Type | -|---------|---------|----------| -| 38 | Reserve monitoring | Basis reserve contract | -| 36 | Tracker monitoring | Tracker contract | - ---- - -## 9. Testing - -### 9.1 Running Tests - -```bash -# Run demo test suite -sbt "testOnly chaincash.demo.BasisDemoSpec" - -# Run all Basis tests -sbt "testOnly chaincash.Basis*" -``` - -### 9.2 Test Coverage - -| Test | Purpose | Status | -|------|---------|--------| -| Reserve box AVL tree format | Verify tree serialization | ✓ | -| Tracker box AVL tree format | Verify tree serialization | ✓ | -| Reserve insert proof | Generate proof for empty→single | ✓ | -| Tracker lookup proof | Generate proof for key lookup | ✓ | -| Signature encoding | Verify 65-byte format | ✓ | -| Transaction structure | Verify inputs/outputs/fee | ✓ | - ---- - -## 10. Error Handling - -### 10.1 Common Errors - -**"Script reduced to false":** -- Cause: Contract condition failed -- Solution: Check signatures, AVL proofs, debt amounts - -**"Malformed request: Attempt to decode value on failed cursor":** -- Cause: Invalid JSON format -- Solution: Ensure all inputs have `extension` field - -**"Reserve box not found":** -- Cause: Box doesn't exist or wrong scan ID -- Solution: Verify reserve is created and scanned with scanId=38 - -### 10.2 Debugging - -```bash -# Run debug script -./debug_signing.sh - -# Check node connection -curl "http://localhost:9053/info" - -# Check wallet status -curl "http://localhost:9053/wallet/status" -H "api_key: hello" - -# Check reserve box -curl "http://localhost:9053/utxo/byId/" -``` - ---- - -## 11. References - -- [Basis Whitepaper](../../docs/conf/conf.pdf) -- [Ergo Documentation](https://docs.ergoplatform.com/) -- [Sigmastate Documentation](https://github.com/sigmastate/sigmastate-interpreter) -- [Scrypto AVL Trees](../../trees/README.md) - ---- - -*Last updated: March 27, 2026* diff --git a/demo/basis/simple/debug_signing.sh b/demo/basis/simple/debug_signing.sh deleted file mode 100755 index 493297f..0000000 --- a/demo/basis/simple/debug_signing.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/bin/bash - -# Debug script to check why signing might fail - -API_KEY="${ERGO_API_KEY:-hello}" -NODE_URL="${ERGO_NODE_URL:-http://127.0.0.1:9053}" - -echo "=== Ergo Node Connection Test ===" -echo "Node URL: $NODE_URL" -echo "" - -# Check node is running -echo "1. Checking node connection..." -NODE_INFO=$(curl -s "$NODE_URL/info" 2>/dev/null) -if [ $? -eq 0 ] && [ -n "$NODE_INFO" ]; then - echo "✓ Node is running" - echo " State: $(echo "$NODE_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state', 'unknown'))" 2>/dev/null || echo "unknown")" -else - echo "✗ Cannot connect to node" - exit 1 -fi -echo "" - -# Check wallet is unlocked -echo "2. Checking wallet status..." -WALLET_STATUS=$(curl -s "$NODE_URL/wallet/status" -H "api_key: $API_KEY" 2>/dev/null) -if echo "$WALLET_STATUS" | grep -q "error"; then - echo "✗ Wallet error: $WALLET_STATUS" - echo " Make sure wallet is unlocked: curl -X POST '$NODE_URL/wallet/unlock' -H 'api_key: $API_KEY' -d '{\"pass\": \"your_password\"}'" -else - echo "✓ Wallet is accessible" - HEIGHT=$(echo "$WALLET_STATUS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('fullHeight', 'unknown'))" 2>/dev/null || echo "unknown") - echo " Wallet height: $HEIGHT" -fi -echo "" - -# Check if reserve box exists -echo "3. Checking reserve box..." -RESERVE_BOX_ID="0b494c598ffd46c72ea95c72e5d47a8cb136eab1dc12da636dc4f817f97bfdb2" -RESERVE_BOX=$(curl -s "$NODE_URL/utxo/byId/$RESERVE_BOX_ID" 2>/dev/null) -if echo "$RESERVE_BOX" | grep -q "error\|null"; then - echo "✗ Reserve box NOT FOUND in UTXO set" - echo " Box ID: $RESERVE_BOX_ID" - echo " The box must exist in the node's UTXO set for signing" -else - echo "✓ Reserve box found" - BOX_VALUE=$(echo "$RESERVE_BOX" | python3 -c "import sys,json; print(json.load(sys.stdin).get('value', 'unknown'))" 2>/dev/null || echo "unknown") - echo " Value: $BOX_VALUE nanoERG" -fi -echo "" - -# Check if tracker box exists -echo "4. Checking tracker box (data input)..." -TRACKER_BOX_ID="49787748507c2c2a2e416c3c4d5ad41ee9d448e9966a709e313279ac2c58e431" -TRACKER_BOX=$(curl -s "$NODE_URL/utxo/byId/$TRACKER_BOX_ID" 2>/dev/null) -if echo "$TRACKER_BOX" | grep -q "error\|null"; then - echo "✗ Tracker box NOT FOUND in UTXO set" - echo " Box ID: $TRACKER_BOX_ID" - echo " The tracker box must exist for the data input" -else - echo "✓ Tracker box found" -fi -echo "" - -# Check if wallet has Alice's secret key -echo "5. Checking if wallet has Alice's secret key..." -ALICE_ADDRESS="9hNQcqi72NB5u5Tw6tbfCGbEKByguR7njvcyZXnXPLvV3Do1DiJ" -ALICE_BALANCE=$(curl -s "$NODE_URL/wallet/balance/$ALICE_ADDRESS" -H "api_key: $API_KEY" 2>/dev/null) -if echo "$ALICE_BALANCE" | grep -q "error"; then - echo "✗ Address not in wallet: $ALICE_ADDRESS" - echo " Import Alice's secret key first:" - echo " curl -X POST '$NODE_URL/wallet/update' -H 'api_key: $API_KEY' -H 'Content-Type: application/json' -d '{\"secret\": \"c693d626538e9dd926519c13f3855412d60aaaa9c8818e7725415a45e92f3108\"}'" -else - echo "✓ Alice's address is in wallet" - echo " Balance: $(echo "$ALICE_BALANCE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('balance', 'unknown'))" 2>/dev/null || echo "unknown") nanoERG" -fi -echo "" - -# Try to sign and capture detailed error -echo "6. Attempting to sign (capturing detailed error)..." -SIGN_RESULT=$(curl -s -X POST "$NODE_URL/wallet/transaction/sign" \ - -H "accept: application/json" \ - -H "api_key: $API_KEY" \ - -H "Content-Type: application/json" \ - -d @sign_request.json 2>&1) - -if echo "$SIGN_RESULT" | grep -q "None.get"; then - echo "✗ Signing failed with 'None.get' error" - echo "" - echo "Common causes:" - echo " 1. Reserve box not in UTXO set (checked in step 3)" - echo " 2. Tracker box not in UTXO set (checked in step 4)" - echo " 3. Wallet doesn't have secret key for reserve owner (checked in step 5)" - echo " 4. Box is already spent" - echo "" - echo "Full error response:" - echo "$SIGN_RESULT" | python3 -m json.tool 2>/dev/null || echo "$SIGN_RESULT" -elif echo "$SIGN_RESULT" | grep -q "error"; then - echo "✗ Signing failed with error:" - echo "$SIGN_RESULT" | python3 -m json.tool 2>/dev/null || echo "$SIGN_RESULT" -else - echo "✓ Signing successful!" - TX_ID=$(echo "$SIGN_RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id', 'unknown'))" 2>/dev/null || echo "unknown") - echo "Transaction ID: $TX_ID" -fi -echo "" - -echo "=== Debug Complete ===" diff --git a/demo/basis/simple/note.json b/demo/basis/simple/note.json deleted file mode 100644 index c0f0325..0000000 --- a/demo/basis/simple/note.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "payerKey": "0377709166937fcdc08bf7e841b31684e2377f489914c97ef7148de14d9c6e1f83", - "payeeKey": "03af13e39dd0ccc7429f9dfa5a056b71a8f5160eaf179763a03e0b55d8feec2cea", - "totalDebt": 50000000, - "timestamp": 1743379200000, - "payerSignature": { - "a": "0217ca6e0eebb6f69f5011cad917cc7023ae5fcee1e06a89b885cd68afa009c99d", - "z": "21c7a1f60e109dd5020581fab1c6db8e3ba0c1631625a1e847dc00f0c093ad3c" - }, - "trackerSignature": { - "a": "023e47969da07ba977967d6e39ca1d7dc4922a299e57ae45652424c3f516b5a051", - "z": "-19c441d687f104418ff2e57b71ee715589edc87e2de5c4a1c0918bcb959a8cf5" - } -} diff --git a/demo/basis/simple/participants.csv.template b/demo/basis/simple/participants.csv.template deleted file mode 100644 index e046697..0000000 --- a/demo/basis/simple/participants.csv.template +++ /dev/null @@ -1,25 +0,0 @@ -# Participant secrets for Basis protocol -# Format: name,address,secret_hex -# -# INSTRUCTIONS: -# 1. Copy this file to secrets/participants.local.csv -# 2. Replace the placeholder secrets with your actual values -# 3. The participants.local.csv file is git-ignored and will not be committed -# -# To generate a new secret key pair: -# - Use ergo-wallet or any secp256k1 key generator -# - Secret must be a 256-bit hex string (64 hex characters) -# - Derive the address from the secret and update both fields -# -# Participants: -# - tracker: Manages offchain debt tracking, signs redemption transactions -# - alice: Reserve owner (Issuer), signs redemption transactions -# - bob: Payee/Receiver, signs transactions via Ergo node (receiverCondition) -# -# Example: -# tracker,, -# alice,, -# bob,, -# -# Note: All three secrets are required. Bob's secret is used by the Ergo node -# for signing transactions (the contract's proveDlog(receiver) requires it). diff --git a/demo/basis/simple/sign_request.json b/demo/basis/simple/sign_request.json deleted file mode 100644 index 7f6e6f8..0000000 --- a/demo/basis/simple/sign_request.json +++ /dev/null @@ -1 +0,0 @@ -{"tx":{"inputs":[{"boxId":"0b494c598ffd46c72ea95c72e5d47a8cb136eab1dc12da636dc4f817f97bfdb2","extension":{"8":"0e710365b2adf3ef941f484c086bf68d0316aa32207bb694bf63227435b394b9d1bc35026995ccf33c8a09705612e6ee3808bb4cedb48cb7b7c019ecdc68b74e7ed912a4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000080000000002faf080000400","5":"0e46020000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000004","6":"0e4102a7c72ce8ec8fa336a984651d57d30d8d59482ad8be1f72c2bc2d3fd5e4c65be6d9ad5a543b623ff7b4bec075d85cd804d2cf01772674384e75eb4aab1e953fe0","1":"0703af13e39dd0ccc7429f9dfa5a056b71a8f5160eaf179763a03e0b55d8feec2cea","0":"0200","2":"0e41031872fa7f83f1545d05a083921e4053f194e87a53facda97677da507a6daf15c348d1fd190990c17c0fe4387d9846bb26b9d8ae821492f3f936124102dc60e5b2","3":"0580c2d72f"}},{"boxId":"ece6907d6778f663b1eb543f5f8080ba72b732cf81ca70ff9cfc45ef21e6284e","extension":{}},{"boxId":"96a4573f6fbbb7eabd22323ab3791bc0c6eaa097664022771ac7c8b7aa336f2e","extension":{}},{"boxId":"a577fd57c734a69088452451a505f1b3c001edeb4f9dd6967e12129ae3cae839","extension":{}},{"boxId":"e8edff0e44711f865fea2d0e05b530c880d16cde836fcf3b39a5c828fe992335","extension":{}}],"dataInputs":[{"boxId":"49787748507c2c2a2e416c3c4d5ad41ee9d448e9966a709e313279ac2c58e431"}],"outputs":[{"ergoTree":"1012041404140400040005000400044204e02105000400044204000442050004420402058084af5f0100d805d6017ee4e3000204d6029d72017300d603b2a59e7201730100d604e4c6a70407d605ededed93c27203c2a793db63087203db6308a793e4c672030407720493e4c67203060ee4c6a7060e959372027302d80fd606b2db6501fe730300d607db07027204d608e4e30107d609cbb37207db07027208d60ae4e30305d60be3070ed60c95e6720b7ce4dc640ae4c6a70564027209e4720b7304d60d99c1a7c17203d60edb6a01ddd60fe4e3020ed610b4720f73057306d611959199a38cc77206017307b3b372097a720a7a7308b372097a720ad612e4e3060ed613b472127309730ad614e4c672060407ea02d1ededededed7205938cb2db63087206730b0001e4c6a7060e937ce4dc640ae4c672060564027209e4e3080e720a93e4dc640ce4c6a705640283013c0e0e860272097a9a720c720de4e3050ee4c672030564939f720e7bb4720f730cb1720fa0ee72109f72047bcbb3b3721072117207eded91720d730d90720d99720a720c939f720e7bb47212730eb17212a0ee72139f72147bcbb3b372137211db07027214cd720895937202730fd1eded720593e4c672030564e4c6a705649299c17203c1a77310d17311","creationHeight":1750616,"value":50000000,"assets":[{"tokenId":"21426942b8d30a7a293f04f44caa2febc536c33121f03f5259ad7be59015b972","amount":1}],"additionalRegisters":{"R4":"070377709166937fcdc08bf7e841b31684e2377f489914c97ef7148de14d9c6e1f83","R5":"642c1d1fb21a9df51972a5439ca7ce8d5601f99c871f15cbf2c4ff6ae53d57a96f01012000","R6":"0e208b1ab583bb085ecbd8fa9bc2fd59784afcdfce5496eb146bb3dd04664b56822a"}},{"ergoTree":"0008cd03af13e39dd0ccc7429f9dfa5a056b71a8f5160eaf179763a03e0b55d8feec2cea","creationHeight":1750616,"value":50000000,"assets":[],"additionalRegisters":{}},{"ergoTree":"1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304","creationHeight":1750616,"value":1000000,"assets":[],"additionalRegisters":{}}]}} \ No newline at end of file diff --git a/demo/basis/simple/src/BasisDeployer.scala b/demo/basis/simple/src/BasisDeployer.scala deleted file mode 100644 index 79ba740..0000000 --- a/demo/basis/simple/src/BasisDeployer.scala +++ /dev/null @@ -1,173 +0,0 @@ -package chaincash.contracts - -import org.ergoplatform.ErgoAddressEncoder -import org.ergoplatform.appkit.{ErgoValue, NetworkType} -import scorex.crypto.encode.Base16 -import sigmastate.AvlTreeFlags -import sigmastate.Values.{AvlTreeConstant, GroupElementConstant} -import sigmastate.serialization.{GroupElementSerializer, ValueSerializer} -import special.sigma.AvlTree -import work.lithos.plasma.PlasmaParameters -import work.lithos.plasma.collections.PlasmaMap - - -/** - * Utility for deploying Basis reserve contract on Ergo blockchain mainnet - * Similar to DexySpec deployment pattern - */ -object BasisDeployer extends App { - - /** - * Alice's public key derived from her Ergo address - * In production, this would come from the wallet, not a hardcoded secret - */ - val exampleOwnerKey: GroupElementConstant = { - val alicePubKey = ParticipantKeys.alicePublicKey - GroupElementConstant(alicePubKey) - } - - // Example values - these should be replaced with actual values - val exampleTrackerNftId = "8b1ab583bb085ecbd8fa9bc2fd59784afcdfce5496eb146bb3dd04664b56822a" - val exampleReserveTokenId = "21426942b8d30a7a293f04f44caa2febc536c33121f03f5259ad7be59015b972" - - // Network configuration - val networkType = NetworkType.MAINNET - val networkPrefix = networkType.networkPrefix - val ergoAddressEncoder = new ErgoAddressEncoder(networkPrefix) - - // Basis contract configuration - val basisContractScript = Constants.readContract("offchain/basis.es", Map.empty) - - val basisErgoTree = Constants.compile(basisContractScript) - val basisAddress = Constants.getAddressFromErgoTree(basisErgoTree) - - // Use Constants.chainCashPlasmaParameters for consistency with BasisNoteRedeemer and TrackerBoxSetup - val InsertOnly = AvlTreeFlags(insertAllowed = true, updateAllowed = false, removeAllowed = false) - def emptyPlasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, Constants.chainCashPlasmaParameters) - val emptyTreeErgoValue: ErgoValue[AvlTree] = emptyPlasmaMap.ergoValue - val emptyTree: AvlTree = emptyTreeErgoValue.getValue - - /** - * Creates deployment request for Basis reserve contract - * @param ownerPublicKey GroupElement of the reserve owner - * @param trackerNftId NFT token ID identifying the tracker (bytes) - * @param reserveTokenId Singleton token ID for the reserve - * @param initialCollateral Initial ERG collateral in nanoERG - * @return JSON string for deployment request - */ - def createBasisDeploymentRequest( - ownerPublicKey: GroupElementConstant, - trackerNftId: String, - reserveTokenId: String, - initialCollateral: Long = 100000000L // 0.1 ERG - ): String = { - - // Encode registers - val ownerKeyEncoded = Base16.encode(ValueSerializer.serialize(ownerPublicKey)) - val emptyTreeEncoded = Base16.encode(ValueSerializer.serialize(AvlTreeConstant(emptyTree))) - val trackerNftBytes = Base16.decode(trackerNftId).get - val trackerNftEncoded = Base16.encode(ValueSerializer.serialize(trackerNftBytes)) - - s""" - |[ - | { - | "address": "${basisAddress.toString}", - | "value": $initialCollateral, - | "assets": [ - | { - | "tokenId": "$reserveTokenId", - | "amount": 1 - | } - | ], - | "registers": { - | "R4": "$ownerKeyEncoded", - | "R5": "$emptyTreeEncoded", - | "R6": "$trackerNftEncoded" - | } - | } - |] - |""".stripMargin - } - - /** - * Creates scan request for monitoring Basis reserve - * @param reserveTokenId Singleton token ID for the reserve - * @return JSON string for scan request - */ - def createBasisScanRequest(reserveTokenId: String): String = { - s""" - |{ - | "scanName": "Basis Reserve", - | "walletInteraction": "shared", - | "removeOffchain": true, - | "trackingRule": { - | "predicate": "containsAsset", - | "assetId": "$reserveTokenId" - | } - |} - |""".stripMargin - } - - /** - * Prints deployment information for Basis contract - */ - def printDeploymentInfo(): Unit = { - println("=== Basis Reserve Contract Deployment Information ===") - println() - - println(s"Contract Address: ${basisAddress.toString}") - println(s"Network: ${networkType.name}") - println(s"Network Prefix: $networkPrefix") - println() - - println("=== Alice's Key Information ===") - println(s"Alice Address: ${ParticipantKeys.aliceAddress}") - println(s"Alice Public Key (hex): ${ParticipantKeys.alicePublicKeyHex}") - println() - - println("Contract Script:") - println(basisContractScript) - println() - - println("Deployment Instructions:") - println("1. Issue a singleton NFT token for the reserve") - println("2. Issue an NFT token for the tracker") - println("3. Use createBasisDeploymentRequest() with owner public key, tracker NFT ID, and reserve NFT ID") - println("4. Submit the deployment transaction to the Ergo blockchain") - println("5. Use createBasisScanRequest() to monitor the reserve") - println() - } - - /** - * Main method for testing and deployment - */ - printDeploymentInfo() - - // Example usage - println("=== Example Deployment Request ===") - - println("Example Scan Request:") - println(createBasisScanRequest(exampleReserveTokenId)) - println() - - println("Example Deployment Request:") - println(createBasisDeploymentRequest(exampleOwnerKey, exampleTrackerNftId, exampleReserveTokenId)) - println() - -} - -/** - * Companion object for Basis contract constants and utilities - */ -object BasisConstants { - - // Action codes for Basis contract - val REDEEM_ACTION: Byte = 0 - val TOP_UP_ACTION: Byte = 1 - - // Minimum top-up amount (0.1 ERG) - val MIN_TOP_UP_AMOUNT: Long = 100000000L - - // Emergency redemption time (3 days in blocks, assuming ~2.5 min per block) - val EMERGENCY_REDEMPTION_TIME_IN_BLOCKS: Int = 3 * 720 -} \ No newline at end of file diff --git a/demo/basis/simple/src/BasisNoteCreator.scala b/demo/basis/simple/src/BasisNoteCreator.scala deleted file mode 100644 index 5ef7318..0000000 --- a/demo/basis/simple/src/BasisNoteCreator.scala +++ /dev/null @@ -1,137 +0,0 @@ -package chaincash.contracts - -import chaincash.offchain.SigUtils -import com.google.common.primitives.Longs -import scorex.crypto.encode.Base16 -import scorex.crypto.hash.Blake2b256 -import sigmastate.basics.CryptoConstants -import sigmastate.eval._ -import sigmastate.serialization.GroupElementSerializer -import special.sigma.GroupElement - -/** - * Utility for creating Basis IOU notes with tracker signature. - * - * Uses Alice's address and secret (verified to match), Bob's secret for demo. - * The tracker signature is included for normal redemption (without waiting for emergency period). - * - * Usage: - * sbt "runMain chaincash.contracts.BasisNoteCreator [amount_nanoERG]" - */ -object BasisNoteCreator extends App { - - val g: GroupElement = CryptoConstants.dlogGroup.generator - - // Alice's keys - public and secret from ParticipantKeys (verified to match) - val alicePublicKey: GroupElement = ParticipantKeys.alicePublicKey - val aliceSecret: BigInt = ParticipantKeys.aliceSecret - - // Bob's secret key (payee) - val bobSecret: BigInt = ParticipantKeys.bobSecret - val bobPublicKey: GroupElement = ParticipantKeys.bobPublicKey - - // Tracker's keys (verified to match tracker address) - val trackerPublicKey: GroupElement = ParticipantKeys.trackerPublicKey - val trackerSecret: BigInt = ParticipantKeys.trackerSecret - - case class IOUNote( - payerKey: GroupElement, - payeeKey: GroupElement, - totalDebt: Long, - timestamp: Long, - signatureA: GroupElement, - signatureZ: BigInt, - message: Array[Byte] - ) - - case class TrackerSignature( - signatureA: GroupElement, - signatureZ: BigInt - ) - - def createNoteMessage(payerKey: GroupElement, payeeKey: GroupElement, totalDebt: Long, timestamp: Long): Array[Byte] = { - Blake2b256(payerKey.getEncoded.toArray ++ payeeKey.getEncoded.toArray) ++ Longs.toByteArray(totalDebt) ++ Longs.toByteArray(timestamp) - } - - def createNote(payerSecret: BigInt, payeeKey: GroupElement, totalDebt: Long, timestamp: Long): IOUNote = { - val payerKey = g.exp(payerSecret.bigInteger) - val message = createNoteMessage(payerKey, payeeKey, totalDebt, timestamp) - val (a, z) = SigUtils.sign(message, payerSecret) - IOUNote(payerKey, payeeKey, totalDebt, timestamp, a, z, message) - } - - def createTrackerSignature(message: Array[Byte]): TrackerSignature = { - val (a, z) = SigUtils.sign(message, trackerSecret) - TrackerSignature(a, z) - } - - def verifyNote(note: IOUNote): Boolean = { - val message = createNoteMessage(note.payerKey, note.payeeKey, note.totalDebt, note.timestamp) - SigUtils.verify(message, note.payerKey, note.signatureA, note.signatureZ) - } - - def verifyTrackerSignature(message: Array[Byte], trackerSig: TrackerSignature): Boolean = { - SigUtils.verify(message, trackerPublicKey, trackerSig.signatureA, trackerSig.signatureZ) - } - - def formatNoteAsJson(note: IOUNote, trackerSig: TrackerSignature): String = { - val payerKeyHex = Base16.encode(note.payerKey.getEncoded.toArray) - val payeeKeyHex = Base16.encode(note.payeeKey.getEncoded.toArray) - val sigAHex = Base16.encode(GroupElementSerializer.toBytes(note.signatureA)) - val trackerSigAHex = Base16.encode(GroupElementSerializer.toBytes(trackerSig.signatureA)) - s"""{ - | "payerKey": "$payerKeyHex", - | "payeeKey": "$payeeKeyHex", - | "totalDebt": ${note.totalDebt}, - | "timestamp": ${note.timestamp}, - | "payerSignature": {"a": "$sigAHex", "z": "${note.signatureZ.toString(16)}"}, - | "trackerSignature": {"a": "$trackerSigAHex", "z": "${trackerSig.signatureZ.toString(16)}"} - |}""".stripMargin - } - - def formatNoteHuman(note: IOUNote, trackerSig: TrackerSignature): String = { - val payerShort = Base16.encode(note.payerKey.getEncoded.toArray).take(16) + "..." - val payeeShort = Base16.encode(note.payeeKey.getEncoded.toArray).take(16) + "..." - val trackerSigValid = verifyTrackerSignature(note.message, trackerSig) - val timestampStr = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date(note.timestamp)) - s"""IOU Note: - | Payer: $payerShort - | Payee: $payeeShort - | Amount: ${note.totalDebt} nanoERG (${note.totalDebt.toDouble / 1000000000} ERG) - | Timestamp: $timestampStr - | Payer Sig Valid: ${verifyNote(note)} - | Tracker Sig Valid: $trackerSigValid - |""".stripMargin - } - - val amount = if (args.length >= 1) args(0).toLong else 50000000L // default 0.05 ERG - val timestamp = System.currentTimeMillis() // Current timestamp in milliseconds - val note = createNote(aliceSecret, bobPublicKey, amount, timestamp) - val trackerSig = createTrackerSignature(note.message) - - // Human-readable to stderr, JSON to stdout - Console.err.println("=== Basis Note Creator ===") - Console.err.println("Creates IOU note from Alice to Bob with tracker signature") - Console.err.println() - Console.err.println("=== Keys ===") - Console.err.println(s"Alice Address: ${ParticipantKeys.aliceAddress}") - Console.err.println(s"Alice Public: ${ParticipantKeys.alicePublicKeyHex}") - Console.err.println(s"Alice Secret: ${ParticipantKeys.aliceSecret.toString(16)}") - Console.err.println(s"Bob Secret: ${ParticipantKeys.bobSecret.toString(16)}") - Console.err.println(s"Bob Public: ${ParticipantKeys.bobPublicKeyHex}") - Console.err.println(s"Tracker Address: ${ParticipantKeys.trackerAddress}") - Console.err.println(s"Tracker Public: ${ParticipantKeys.trackerPublicKeyHex}") - Console.err.println(s"Tracker Secret: ${ParticipantKeys.trackerSecret.toString(16)}") - Console.err.println() - Console.err.println(formatNoteHuman(note, trackerSig)) - Console.err.println("=== JSON (stdout) ===") - - println(formatNoteAsJson(note, trackerSig)) - - Console.err.println() - Console.err.println("=== Usage ===") - Console.err.println("Save note: sbt \"runMain ...BasisNoteCreator\" > note.json") - Console.err.println("Redeem: sbt \"runMain ...BasisNoteRedeemer --note-json note.json --reserve-box \"") - Console.err.println() - Console.err.println("Note: This note includes tracker signature for normal redemption.") -} diff --git a/demo/basis/simple/src/BasisNoteRedeemer.scala b/demo/basis/simple/src/BasisNoteRedeemer.scala deleted file mode 100644 index 2728ffd..0000000 --- a/demo/basis/simple/src/BasisNoteRedeemer.scala +++ /dev/null @@ -1,768 +0,0 @@ -package chaincash.contracts - -import chaincash.offchain.SigUtils -import com.google.common.primitives.Longs -import io.circe.{Decoder, Json} -import io.circe.generic.auto._ -import io.circe.parser._ -import io.circe.syntax._ -import org.bouncycastle.util.BigIntegers -import org.ergoplatform.{ErgoAddressEncoder, P2PKAddress} -import org.ergoplatform.appkit.NetworkType -import scorex.crypto.encode.Base16 -import scorex.crypto.hash.Blake2b256 -import sigmastate.basics.CryptoConstants -import sigmastate.basics.DLogProtocol.ProveDlog -import sigmastate.eval._ -import sigmastate.serialization.{GroupElementSerializer, ValueSerializer} -import sigmastate.Values.AvlTreeConstant -import special.sigma.{AvlTree, GroupElement} -import sigmastate.AvlTreeFlags -import work.lithos.plasma.collections.PlasmaMap -import scala.util.{Try, Success, Failure} -import java.net.{URL, HttpURLConnection} - -/** - * Utility for redeeming Basis IOU notes with tracker signature. - * Uses the tracker signature from the note for normal redemption (no emergency period needed). - * Generates real AVL proofs for empty tree (first redemption) scenario. - * - * ## How Redemption Works - * - * 1. Load the IOU note JSON (created by BasisNoteCreator) - * 2. Verify both payer's and tracker's signatures - * 3. Generate AVL proof for debt lookup in tracker's AVL tree - * 4. Build redemption transaction with: - * - Reserve box input (holds collateral) - * - Tracker box input (holds debt state digest) - * - Context variables with signatures and proofs - * 5. Submit transaction to Ergo node - * - * ## Tracker's Role in Redemption - * - * The tracker signature enables "normal" redemption without waiting for emergency period: - * - Tracker signature certifies the note is witnessed in current state - * - AVL proof verifies the debt exists in tracker's tree - * - Reserve contract checks both signatures and AVL proof - * - * ## Emergency Redemption - * - * If tracker is offline, notes can be redeemed against last committed state - * after emergency period expires (3 days / 2160 blocks): - * - **No tracker signature needed** after emergency period - * - Same message format: `key || totalDebt || timestamp` - * - Replay attacks prevented by timestamp verification (must be > stored timestamp) - * - Reserve owner's signature still required (proves debt validity) - * - Use empty bytes for tracker signature field when tracker is unavailable - * - * ## Required Inputs - * - * - **Note JSON**: Contains payer/payee keys, debt amount, signatures - * - **Reserve Box ID**: On-chain reserve holding collateral (or 'auto' to fetch) - * - **Tracker Box ID**: On-chain tracker box with AVL tree (or 'auto' to fetch) - * - **Reserve Owner Secret**: To sign redemption transaction - * - **Tracker Secret**: To generate tracker signature (default from ParticipantKeys) - * - * Usage: - * sbt "runMain chaincash.contracts.BasisNoteRedeemer --note-json note.json --reserve-box " - * - * See contracts/offchain/tracker.md and contracts/offchain/basis.md for more details. - */ -object BasisNoteRedeemer extends App { - - val networkType = NetworkType.MAINNET - val ergoAddressEncoder = new ErgoAddressEncoder(networkType.networkPrefix) - val g: GroupElement = CryptoConstants.dlogGroup.generator - - // Tracker's public key from ParticipantKeys (verified to match tracker address) - val trackerPublicKey: GroupElement = ParticipantKeys.trackerPublicKey - - val REDEEM_ACTION: Byte = 0 - - // Token IDs (must match deployed contract) - val basisReserveNftId = "21426942b8d30a7a293f04f44caa2febc536c33121f03f5259ad7be59015b972" - val trackerNftId = "8b1ab583bb085ecbd8fa9bc2fd59784afcdfce5496eb146bb3dd04664b56822a" - - // Plasma tree configuration (must match basis.es contract and TrackerBoxSetup) - // Uses Constants.chainCashPlasmaParameters for consistency - val InsertUpdate = AvlTreeFlags(insertAllowed = true, updateAllowed = true, removeAllowed = false) - - /** - * Get current blockchain height from node API. - */ - def getCurrentHeight(): Int = { - val urlStr = s"$nodeUrl/info" - val url = new URL(urlStr) - val conn = url.openConnection().asInstanceOf[HttpURLConnection] - try { - conn.setRequestMethod("GET") - conn.setRequestProperty("api_key", apiKey) - val responseCode = conn.getResponseCode - if (responseCode == 200) { - val source = scala.io.Source.fromInputStream(conn.getInputStream) - try { - val jsonStr = source.mkString - parse(jsonStr) match { - case Right(json) => - json.hcursor.downField("fullHeight").as[Int].toOption.getOrElse(1750221) - case Left(_) => 1750221 - } - } finally { - source.close() - } - } else 1750221 - } finally { - conn.disconnect() - } - } - - case class SignatureJson( - a: String, - z: String - ) - - case class NoteJson( - payerKey: String, - payeeKey: String, - totalDebt: Long, - totalDebtERG: Option[Double], - timestamp: Option[Long], - payerSignature: Option[SignatureJson], - trackerSignature: Option[SignatureJson], - signature: Option[SignatureJson], // legacy field for backward compatibility - message: Option[String], - messageFormat: Option[String], - noteKey: Option[String] - ) { - // Get timestamp from note or message (for backward compatibility) - def getTimestamp: Long = { - timestamp.getOrElse { - message.flatMap { m => - val messageBytes = Base16.decode(m).toOption - messageBytes.filter(_.length >= 48).map(bytes => Longs.fromByteArray(bytes.slice(40, 48))) - }.getOrElse(System.currentTimeMillis()) - } - } - - // Reconstruct message from keys + debt + timestamp for signature verification - def getMessage: Array[Byte] = { - message.flatMap(m => Base16.decode(m).toOption).getOrElse { - val payerKeyBytes = Base16.decode(payerKey).get - val payeeKeyBytes = Base16.decode(payeeKey).get - val ts = getTimestamp - Blake2b256(payerKeyBytes ++ payeeKeyBytes) ++ Longs.toByteArray(totalDebt) ++ Longs.toByteArray(ts) - } - } - - // Get tracker signature (prioritize new field, fallback to legacy) - def getTrackerSignature: SignatureJson = { - trackerSignature.orElse(payerSignature).getOrElse { - throw new RuntimeException("Note must include trackerSignature for normal redemption") - } - } - - // Get payer signature - def getPayerSignature: SignatureJson = { - payerSignature.orElse(signature).getOrElse { - throw new RuntimeException("Note must include payerSignature") - } - } - } - - case class IOUNote( - payerKey: GroupElement, - payeeKey: GroupElement, - totalDebt: Long, - timestamp: Long, - payerSignatureA: GroupElement, - payerSignatureZ: BigInt, - trackerSignatureA: GroupElement, - trackerSignatureZ: BigInt - ) - - def parseNoteJson(filePath: String): NoteJson = { - val source = scala.io.Source.fromFile(filePath) - try { - val content = source.getLines.mkString("\n") - parse(content) match { - case Right(json) => - // Try to parse as NoteJson directly first - json.as[NoteJson] match { - case Right(n) => n - case Left(_) => - // Try to extract note from wrapper object (SetupTrackerState format) - json.hcursor.downField("note").as[NoteJson] match { - case Right(n) => n - case Left(err) => throw new RuntimeException(s"Invalid note JSON: ${err.getMessage}") - } - } - case Left(err) => throw new RuntimeException(s"Invalid JSON: ${err.getMessage}") - } - } finally { - source.close() - } - } - - def noteFromJson(noteJson: NoteJson): IOUNote = { - val payerSig = noteJson.getPayerSignature - val trackerSig = noteJson.getTrackerSignature - val timestamp = noteJson.getTimestamp - IOUNote( - payerKey = GroupElementSerializer.fromBytes(Base16.decode(noteJson.payerKey).get), - payeeKey = GroupElementSerializer.fromBytes(Base16.decode(noteJson.payeeKey).get), - totalDebt = noteJson.totalDebt, - timestamp = timestamp, - payerSignatureA = GroupElementSerializer.fromBytes(Base16.decode(payerSig.a).get), - payerSignatureZ = BigInt(payerSig.z, 16), - trackerSignatureA = GroupElementSerializer.fromBytes(Base16.decode(trackerSig.a).get), - trackerSignatureZ = BigInt(trackerSig.z, 16) - ) - } - - def verifyNote(note: IOUNote, message: Array[Byte]): Boolean = { - val payerValid = SigUtils.verify(message, note.payerKey, note.payerSignatureA, note.payerSignatureZ) - val trackerValid = SigUtils.verify(message, trackerPublicKey, note.trackerSignatureA, note.trackerSignatureZ) - payerValid && trackerValid - } - - /** - * Generates a real AVL proof for tracker tree lookup. - * For first redemption (empty tree), creates a tree with the debt record and generates proof. - * - * ## Tracker Tree Structure - * - * The tracker tree stores debt relationships: - * - Key: Blake2b256(payerPublicKey || payeePublicKey) (32 bytes) - * - Value: totalDebt as Long (8 bytes, big-endian) - * - * ## How It Works - * - * 1. Creates PlasmaMap with InsertOnly flags (matching tracker box parameters) - * 2. Computes debt key from payer and payee public keys - * 3. Inserts the debt record into the map - * 4. Generates lookup proof for the debt key - * 5. Returns proof bytes in hex format - * - * The proof is used in on-chain redemption to verify debt exists in tracker's - * committed AVL tree. The basis.es contract verifies: - * - Proof is valid against tracker tree digest - * - Debt key matches Blake2b256(payerKey || payeeKey) - * - Debt value matches note's totalDebt - * - * Note: Uses InsertOnly flags and Constants.chainCashPlasmaParameters to match - * the tracker box creation (TrackerBoxSetup). - * - * @param payerKey Hex-encoded public key of the debtor - * @param payeeKey Hex-encoded public key of the creditor - * @param totalDebt Total debt amount in nanoERG - * @return Hex-encoded AVL proof bytes - */ - def generateTrackerAvlProof(payerKey: String, payeeKey: String, totalDebt: Long): String = { - // Create PlasmaMap with InsertOnly flags and correct parameters (must match tracker box) - val InsertOnly = AvlTreeFlags(insertAllowed = true, updateAllowed = false, removeAllowed = false) - val plasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, Constants.chainCashPlasmaParameters) - - // Create the key: hash(payerKey || payeeKey) - val key = Blake2b256( - Base16.decode(payerKey).get ++ - Base16.decode(payeeKey).get - ) - - // Insert the debt record into the tree (updates map in place) - plasmaMap.insert((key, Longs.toByteArray(totalDebt))) - plasmaMap.prover.generateProof() - - // Generate proof for the key lookup - val lookupProof = plasmaMap.lookUp(key).proof.bytes - - // Encode proof as hex string - Base16.encode(lookupProof) - } - - /** - * Generates a real AVL proof for inserting redeemed amount into reserve tree. - * For first redemption, creates a tree from scratch and generates insert proof. - * - * Note: Uses InsertOnly flags and Constants.chainCashPlasmaParameters to match - * the reserve box creation (BasisDeployer). - * Tree value format: timestamp (8 bytes) ++ redeemedAmount (8 bytes) = 16 bytes - */ - def generateReserveInsertProof(payerKey: String, payeeKey: String, timestamp: Long, redeemedAmount: Long): (Array[Byte], AvlTree) = { - // Create PlasmaMap with InsertOnly flags and correct parameters (must match reserve box) - val InsertOnly = AvlTreeFlags(insertAllowed = true, updateAllowed = false, removeAllowed = false) - val plasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, Constants.chainCashPlasmaParameters) - - // Create the key: hash(payerKey || payeeKey) - val key = Blake2b256( - Base16.decode(payerKey).get ++ - Base16.decode(payeeKey).get - ) - - // Tree value format: timestamp (8 bytes) ++ redeemedAmount (8 bytes) = 16 bytes - val treeValue = Longs.toByteArray(timestamp) ++ Longs.toByteArray(redeemedAmount) - - // Insert the value into the tree - val insertResult = plasmaMap.insert((key, treeValue)) - - // Get the insert proof and updated tree - val insertProof = insertResult.proof.bytes - val updatedTree = plasmaMap.ergoValue.getValue() - - (insertProof, updatedTree) - } - - def encodeByteValue(b: Byte): String = { - Base16.encode(ValueSerializer.serialize(b)) - } - - def encodeGroupElementValue(ge: GroupElement): String = { - Base16.encode(ValueSerializer.serialize(ge)) - } - - def encodeCollByteValue(bytes: Array[Byte]): String = { - Base16.encode(ValueSerializer.serialize(bytes)) - } - - def encodeLongValue(l: Long): String = { - Base16.encode(ValueSerializer.serialize(l)) - } - - /** - * Fetch reserve box and extract its AVL tree (R5 register). - */ - def fetchReserveTree(reserveBoxId: String): Option[AvlTree] = { - val urlStr = s"$nodeUrl/utxo/byId/$reserveBoxId" - val url = new URL(urlStr) - val conn = url.openConnection().asInstanceOf[HttpURLConnection] - try { - conn.setRequestMethod("GET") - val responseCode = conn.getResponseCode - if (responseCode == 200) { - val source = scala.io.Source.fromInputStream(conn.getInputStream) - try { - val jsonStr = source.mkString - parse(jsonStr) match { - case Right(json) => - json.hcursor.downField("additionalRegisters").downField("R5").as[String].toOption.flatMap { r5Hex => - val bytes = Base16.decode(r5Hex).get - val avlTreeConstant = ValueSerializer.deserialize(bytes).asInstanceOf[AvlTreeConstant] - Some(avlTreeConstant.value) - } - case Left(_) => None - } - } finally { - source.close() - } - } else None - } finally { - conn.disconnect() - } - } - - def buildTransaction( - note: IOUNote, - reserveBoxId: String, - trackerBoxId: String, - reserveOwnerSecret: BigInt, - trackerSecret: BigInt, - trackerProof: String, - redeemedAmount: Long, - feeAmount: Long = 1000000L, - feeBoxIds: Option[Seq[String]] = None // List of box IDs to cover fee - ): String = { - // Create redemption message: key || totalDebt || timestamp - val ownerKeyBytes = note.payerKey.getEncoded.toArray - val receiverBytes = note.payeeKey.getEncoded.toArray - val key = Blake2b256(ownerKeyBytes ++ receiverBytes) - val redemptionMessage = key ++ Longs.toByteArray(note.totalDebt) ++ Longs.toByteArray(note.timestamp) - - // Generate reserve owner's signature on redemption message - val (reserveSigA, reserveSigZ) = SigUtils.sign(redemptionMessage, reserveOwnerSecret) - // Use BouncyCastle to get exactly 32 bytes for z (no sign byte issues) - val reserveSigZBytes = BigIntegers.asUnsignedByteArray(32, reserveSigZ.bigInteger) - val reserveSigBytes = GroupElementSerializer.toBytes(reserveSigA) ++ reserveSigZBytes - val reserveSigEncoded = Base16.encode(reserveSigBytes) - - // Generate tracker's signature on redemption message - val (trackerSigA, trackerSigZ) = SigUtils.sign(redemptionMessage, trackerSecret) - // Use BouncyCastle to get exactly 32 bytes for z (no sign byte issues) - val trackerSigZBytes = BigIntegers.asUnsignedByteArray(32, trackerSigZ.bigInteger) - val trackerSigBytes = GroupElementSerializer.toBytes(trackerSigA) ++ trackerSigZBytes - val trackerSigEncoded = Base16.encode(trackerSigBytes) - - // Generate BOTH AVL proofs required by the contract: - // 1. Reserve insert proof (context var 5) - for inserting redeemed amount into reserve tree - // 2. Tracker lookup proof (context var 8) - for looking up debt in tracker tree (already provided) - val payerKeyHex = Base16.encode(note.payerKey.getEncoded.toArray) - val payeeKeyHex = Base16.encode(note.payeeKey.getEncoded.toArray) - val (reserveInsertProof, updatedReserveTree) = generateReserveInsertProof( - payerKeyHex, payeeKeyHex, note.timestamp, redeemedAmount - ) - val reserveInsertProofEncoded = encodeCollByteValue(reserveInsertProof) - val updatedReserveTreeEncoded = Base16.encode(ValueSerializer.serialize(AvlTreeConstant(updatedReserveTree))) - - // Build context extension with BOTH AVL proofs and timestamp - val contextVars = Map( - "0" -> Base16.encode(ValueSerializer.serialize(REDEEM_ACTION)), - "1" -> encodeGroupElementValue(note.payeeKey), - "2" -> encodeCollByteValue(Base16.decode(reserveSigEncoded).get), - "3" -> encodeLongValue(note.totalDebt), - "4" -> encodeLongValue(note.timestamp), // timestamp (NEW) - "5" -> reserveInsertProofEncoded, // Reserve insert proof - "6" -> encodeCollByteValue(Base16.decode(trackerSigEncoded).get), - "8" -> encodeCollByteValue(Base16.decode(trackerProof).get) - ) - - val reserveValue = 50000000L - val receiverValue = 50000000L - val creationHeight = getCurrentHeight() - - // Get receiver's ergoTree from payeeKey (P2PK: 0008cd + pubkey) - val receiverErgoTree = s"0008cd${Base16.encode(note.payeeKey.getEncoded.toArray)}" - - // Get basis ergoTree as hex string - val basisErgoTreeHex = Base16.encode(Constants.basisErgoTree.bytes) - - // Fee recipient (from user request) - val feeRecipientErgoTree = "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304" - - // Build fee inputs if provided - val feeInputsJson = feeBoxIds match { - case Some(boxIds) => - // Fee inputs need empty extension to be valid for signing - boxIds.map(boxId => s""",{"boxId": "$boxId", "extension": {}}""").mkString - case None => "" - } - - // Calculate fee output: total input value - outputs - // Reserve box: 100000000, Fee boxes: 4 * 250000 = 1000000 - // Total input: 101000000 - // Outputs: 50000000 + 50000000 = 100000000 - // Fee (to recipient): 101000000 - 100000000 = 1000000 - val totalInputValue = reserveValue * 2 + feeAmount // 100000000 + 1000000 - val totalOutputValue = reserveValue + receiverValue // 50000000 + 50000000 - val feeOutputValue = totalInputValue - totalOutputValue // 1000000 nanoERG (goes to fee recipient) - - val feeOutputJson = if (feeOutputValue > 0) { - s""", - { - "ergoTree": "$feeRecipientErgoTree", - "creationHeight": $creationHeight, - "value": $feeOutputValue, - "assets": [], - "additionalRegisters": {} - }""" - } else "" - - val txJson = s"""{ - | "inputs": [{ - | "boxId": "$reserveBoxId", - | "extension": ${mapToJson(contextVars)} - | }$feeInputsJson], - | "dataInputs": [{"boxId": "$trackerBoxId"}], - | "outputs": [ - | { - | "ergoTree": "$basisErgoTreeHex", - | "creationHeight": $creationHeight, - | "value": $reserveValue, - | "assets": [{"tokenId": "$basisReserveNftId", "amount": 1}], - | "additionalRegisters": { - | "R4": "${encodeGroupElementValue(note.payerKey)}", - | "R5": "$updatedReserveTreeEncoded", - | "R6": "0e20$trackerNftId" - | } - | }, - | { - | "ergoTree": "$receiverErgoTree", - | "creationHeight": $creationHeight, - | "value": $receiverValue, - | "assets": [], - | "additionalRegisters": {} - | }$feeOutputJson - | ] - |}""".stripMargin - - // Wrap in TransactionSigningRequest format (OpenAPI spec) - s"""{"tx": $txJson}""" - } - - def mapToJson(m: Map[String, String]): String = { - m.map { case (k, v) => s""""$k": "$v"""" }.mkString("{", ",", "}") - } - - def redeem( - noteJson: NoteJson, - reserveBoxId: String, - trackerBoxId: String, - outputFile: Option[String], - reserveOwnerSecret: BigInt, - trackerSecret: BigInt, - feeBoxIds: Option[Seq[String]] = None - ): Unit = { - println("=== Basis Note Redeemer (Normal Redemption) ===") - println() - - val note = noteFromJson(noteJson) - val message = noteJson.getMessage - val timestampStr = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date(note.timestamp)) - - println("--- Note Details ---") - println(s"Payer: ${noteJson.payerKey.take(16)}...") - println(s"Payee: ${noteJson.payeeKey.take(16)}...") - println(s"Amount: ${noteJson.totalDebt} nanoERG (${noteJson.totalDebt.toDouble / 1000000000} ERG)") - println(s"Timestamp: $timestampStr") - println() - - println("--- Verifying Note Signatures ---") - val signaturesValid = verifyNote(note, message) - println(s"Payer signature valid: ${SigUtils.verify(message, note.payerKey, note.payerSignatureA, note.payerSignatureZ)}") - println(s"Tracker signature valid: ${SigUtils.verify(message, trackerPublicKey, note.trackerSignatureA, note.trackerSignatureZ)}") - println(s"Overall valid: $signaturesValid") - println() - - if (!signaturesValid) { - println("ERROR: Note signatures are invalid. Cannot proceed with redemption.") - sys.exit(1) - } - - println("--- Building Transaction ---") - println("Generating new signatures for redemption message.") - println("Generating real AVL proof for tracker tree.") - println() - - // Generate real AVL proof from tracker tree with debt record - val avlProof = generateTrackerAvlProof(noteJson.payerKey, noteJson.payeeKey, noteJson.totalDebt) - println(s"Tracker AVL proof: ${avlProof.take(64)}...") - println() - - val txJson = buildTransaction(note, reserveBoxId, trackerBoxId, reserveOwnerSecret, trackerSecret, avlProof, noteJson.totalDebt, feeAmount = 1000000L, feeBoxIds = feeBoxIds) - - outputFile match { - case Some(file) => - val writer = new java.io.FileWriter(file) - try { - writer.write(txJson) - println(s"Transaction saved to: $file") - } finally { - writer.close() - } - case None => - println("\n=== Unsigned Transaction JSON ===") - println(txJson) - } - - println() - println("=== Next Steps ===") - println("1. Sign: curl -X POST http://localhost:9053/wallet/transaction/sign -H api_key: -d @tx.json") - println("2. Broadcast: curl -X POST http://localhost:9053/transactions -H api_key: -d '{\"tx\": {...}}'") - println() - println("NOTE: This transaction generates new signatures for the redemption message.") - println(" The note signatures are only for verification of the IOU agreement.") - } - - def printUsage(): Unit = { - println("=== Basis Note Redeemer ===") - println("Redeem IOU notes with tracker signature (normal redemption)") - println() - println("Usage: BasisNoteRedeemer --note-json --reserve-box [options]") - println() - println("Required:") - println(" --note-json IOU note JSON with tracker signature from BasisNoteCreator") - println(" --reserve-box Reserve box ID to redeem from (or 'auto' to fetch from scan API)") - println() - println("Optional:") - println(" --tracker-box Tracker box ID (or 'auto' to fetch from scan API, default)") - println(" --output Save transaction to file (default: stdout)") - println(" --reserve-owner-secret Reserve owner's secret key (default: from ParticipantKeys)") - println(" --tracker-secret Tracker's secret key (default: from ParticipantKeys)") - println(" --fee-box Comma-separated list of box IDs to cover fee (4x 250000 nanoERG)") - println(" --help, -h Show this help") - println() - println("Examples:") - println(" # With explicit box IDs:") - println(" sbt \"runMain chaincash.contracts.BasisNoteRedeemer --note-json note.json --reserve-box abc123... --tracker-box xyz789...\"") - println() - println(" # Auto-fetch box IDs from node scan API:") - println(" sbt \"runMain chaincash.contracts.BasisNoteRedeemer --note-json note.json --reserve-box auto --tracker-box auto\"") - println() - println("Note: The note must include trackerSignature for normal redemption.") - println(" Reserve owner and tracker secrets are needed to sign the redemption.") - println(" For auto-fetch, set ERGO_NODE_URL and ERGO_API_KEY environment variables.") - } - - case class Args( - noteJsonFile: Option[String] = None, - reserveBoxId: Option[String] = None, - trackerBoxId: Option[String] = None, - outputFile: Option[String] = None, - reserveOwnerSecret: Option[String] = None, - trackerSecret: Option[String] = None, - feeBoxIds: Option[String] = None, // Comma-separated list of box IDs for fee - help: Boolean = false - ) - - def parseArgs(args: Array[String]): Either[String, Args] = { - var result = Args() - var i = 0 - while (i < args.length) { - args(i) match { - case "--note-json" => - if (i + 1 < args.length) { - result = result.copy(noteJsonFile = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --note-json") - case "--reserve-box" => - if (i + 1 < args.length) { - result = result.copy(reserveBoxId = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --reserve-box") - case "--tracker-box" => - if (i + 1 < args.length) { - result = result.copy(trackerBoxId = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --tracker-box") - case "--output" => - if (i + 1 < args.length) { - result = result.copy(outputFile = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --output") - case "--reserve-owner-secret" => - if (i + 1 < args.length) { - result = result.copy(reserveOwnerSecret = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --reserve-owner-secret") - case "--tracker-secret" => - if (i + 1 < args.length) { - result = result.copy(trackerSecret = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --tracker-secret") - case "--fee-box" => - if (i + 1 < args.length) { - result = result.copy(feeBoxIds = Some(args(i + 1))) - i += 1 - } else return Left("Missing value for --fee-box") - case "--help" | "-h" => - result = result.copy(help = true) - case arg => - return Left(s"Unknown argument: $arg") - } - i += 1 - } - Right(result) - } - - // Node API configuration - val nodeUrl = sys.env.getOrElse("ERGO_NODE_URL", "http://127.0.0.1:9053") - val apiKey = sys.env.getOrElse("ERGO_API_KEY", "hello") - - def fetchUnspentBoxes(scanId: Int): Option[String] = { - val urlStr = s"$nodeUrl/scan/$scanId/boxes/unspent" - val url = new URL(urlStr) - val conn = url.openConnection().asInstanceOf[HttpURLConnection] - try { - conn.setRequestMethod("GET") - conn.setRequestProperty("api_key", apiKey) - val responseCode = conn.getResponseCode - if (responseCode == 200) { - val source = scala.io.Source.fromInputStream(conn.getInputStream) - try { - val jsonStr = source.mkString - parse(jsonStr) match { - case Right(json) => - json.hcursor.downField("items").downArray.downField("boxId").as[String].toOption - case Left(_) => None - } - } finally { - source.close() - } - } else None - } finally { - conn.disconnect() - } - } - - def fetchReserveAndTrackerBoxes(reserveBoxId: Option[String], trackerBoxId: Option[String]): Either[String, (String, String)] = { - // Fetch reserve box (scanId=38) - val actualReserveBoxId = reserveBoxId match { - case Some("auto") | None => - Console.err.println("Fetching reserve box (scanId=38)...") - fetchUnspentBoxes(38) match { - case Some(id) => - Console.err.println(s" Found reserve box: $id") - id - case None => return Left("Reserve box not found. Make sure the reserve is created and scanned with scanId=38") - } - case Some(id) => id - } - - // Fetch tracker box (scanId=36) - val actualTrackerBoxId = trackerBoxId match { - case Some("auto") | None => - Console.err.println("Fetching tracker box (scanId=36)...") - fetchUnspentBoxes(36) match { - case Some(id) => - Console.err.println(s" Found tracker box: $id") - id - case None => return Left("Tracker box not found. Make sure the tracker is created and scanned with scanId=36") - } - case Some(id) => id - } - - Right((actualReserveBoxId, actualTrackerBoxId)) - } - - // Main - if (args.isEmpty) { - printUsage() - sys.exit(0) - } - - parseArgs(args) match { - case Left(err) => - println(s"Error: $err") - println() - printUsage() - sys.exit(1) - case Right(cli) => - if (cli.help) { - printUsage() - } else if (cli.noteJsonFile.isEmpty) { - println("Error: --note-json is required") - printUsage() - sys.exit(1) - } else { - // Fetch box IDs (supports 'auto' for scan API fetching) - fetchReserveAndTrackerBoxes(cli.reserveBoxId, cli.trackerBoxId) match { - case Left(err) => - println(s"Error: $err") - sys.exit(1) - case Right((reserveBoxId, trackerBoxId)) => - // Get secrets from command line or use defaults from ParticipantKeys - val reserveOwnerSecret = cli.reserveOwnerSecret match { - case Some(s) => BigInt(s) - case None => ParticipantKeys.aliceSecret // default: Alice is reserve owner - } - val trackerSecret = cli.trackerSecret match { - case Some(s) => BigInt(s) - case None => ParticipantKeys.trackerSecret // default: use tracker secret from ParticipantKeys - } - - // Parse fee box IDs if provided - val feeBoxIdsOpt = cli.feeBoxIds match { - case Some(csv) => Some(csv.split(",").map(_.trim).toSeq) - case None => None - } - - Try { - val noteJson = parseNoteJson(cli.noteJsonFile.get) - redeem(noteJson, reserveBoxId, trackerBoxId, cli.outputFile, reserveOwnerSecret, trackerSecret, feeBoxIdsOpt) - } match { - case Success(_) => - case Failure(e) => - println(s"ERROR: ${e.getMessage}") - sys.exit(1) - } - } - } - } -} diff --git a/demo/basis/simple/src/TrackerBoxSetup.scala b/demo/basis/simple/src/TrackerBoxSetup.scala deleted file mode 100644 index fb599b3..0000000 --- a/demo/basis/simple/src/TrackerBoxSetup.scala +++ /dev/null @@ -1,244 +0,0 @@ -package chaincash.contracts - -import chaincash.contracts.Constants.basisPlasmaParameters -import com.google.common.primitives.Longs -import scorex.crypto.hash.Blake2b256 -import scorex.util.encode.Base16 -import sigmastate.AvlTreeFlags -import sigmastate.Values.AvlTreeConstant -import sigmastate.serialization.ValueSerializer -import special.sigma.AvlTree -import work.lithos.plasma.PlasmaParameters -import work.lithos.plasma.collections.PlasmaMap - -/** - * Utility to create tracker box setup JSON for /wallet/payment/send API - * - * ## What is a Tracker Box? - * - * A tracker box is an on-chain box that holds: - * - Tracker's public key (R4 register) - * - AVL tree digest of all debt relationships (R5 register) - * - Tracker NFT token (identifies this as the official tracker) - * - * The tracker box serves as a commitment to the offchain debt state. - * When the tracker goes offline, users can redeem against the last - * committed state in this box. - * - * ## Tracker Tree Structure - * - * The AVL tree in the tracker box stores debt relationships: - * - Key: Blake2b256(payerPublicKey || payeePublicKey) (32 bytes) - * - Value: totalDebt as Long (8 bytes, big-endian) - * - * This allows efficient lookup and proof generation for any debt pair. - * - * ## How This Utility Works - * - * 1. Creates an AVL tree with initial debt entries (for demo: Alice->Bob) - * 2. Generates JSON for creating tracker box via Ergo node API - * 3. Outputs can be submitted to /wallet/payment/send endpoint - * - * ## Usage Flow - * - * 1. Run this utility to generate tracker box JSON - * 2. Submit JSON to Ergo node to create tracker box - * 3. Tracker box is scanned with scanId=36 - * 4. Tracker service uses this box for state commitments - * - * Reference code for tracker tree calculation: - * - BasisNoteRedeemer.generateTrackerAvlProof() - generates proofs for redemption - * - BasisSpec.mkTrackerTreeAndProof() - test code for tree creation - * - basis.es contract - on-chain verification of tracker proofs - * - * Usage: - * sbt "runMain chaincash.contracts.TrackerBoxSetup" - * - * The output JSON can be submitted to: - * POST /wallet/payment/send - * - * See contracts/offchain/tracker.md for detailed tracker architecture. - */ -object TrackerBoxSetup extends App { - - // Tracker contract parameters (must match basis.es contract) - val InsertOnly = AvlTreeFlags(insertAllowed = true, updateAllowed = false, removeAllowed = false) - - // Participant keys from ParticipantKeys object - val alicePublicKeyHex = ParticipantKeys.alicePublicKeyHex - val bobPublicKeyHex = ParticipantKeys.bobPublicKeyHex - val trackerPublicKeyHex = ParticipantKeys.trackerPublicKeyHex - - // Tracker NFT ID (this must be issued beforehand) - // Use the actual tracker NFT from your setup - val trackerNftId = "8b1ab583bb085ecbd8fa9bc2fd59784afcdfce5496eb146bb3dd04664b56822a" - - // Debt amount (example: 50,000,000 nanoERG for testing) - val totalDebt = 50000000L - - /** - * Creates the tracker AVL tree with a single Alice->Bob debt entry. - * - * ## Tree Structure - * - * The tracker tree stores debt relationships as key-value pairs: - * - Key: Blake2b256(payerPublicKey || payeePublicKey) (32 bytes) - * - Value: totalDebt as Long (8 bytes, big-endian) - * - * ## Steps - * - * 1. Create empty PlasmaMap with InsertOnly flags - * 2. Compute debt key = Blake2b256(alicePubKey || bobPubKey) - * 3. Insert (debtKey, totalDebt) into the map - * 4. Get the ergoValue (AvlTree) which contains the digest - * - * The resulting tree can be used to: - * - Initialize a new tracker box - * - Generate proofs for redemption (see BasisNoteRedeemer.generateTrackerAvlProof) - * - Verify debt existence on-chain - * - * Reference: BasisNoteRedeemer.generateTrackerAvlProof() - * - * @param alicePubKeyHex Alice's public key in hex (payer/debtor) - * @param bobPubKeyHex Bob's public key in hex (payee/creditor) - * @param totalDebt Total debt amount in nanoERG - * @return AvlTree containing the debt entry - */ - def createTrackerTree(alicePubKeyHex: String, bobPubKeyHex: String, totalDebt: Long): AvlTree = { - // Create empty PlasmaMap - val plasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, basisPlasmaParameters) - - // Decode public keys from hex - val alicePubKeyBytes = Base16.decode(alicePubKeyHex).get - val bobPubKeyBytes = Base16.decode(bobPubKeyHex).get - - // Create the debt key: Blake2b256(alicePubKey || bobPubKey) - val debtKey = Blake2b256(alicePubKeyBytes ++ bobPubKeyBytes) - - // Insert the debt record into the tree - plasmaMap.insert((debtKey, Longs.toByteArray(totalDebt))) - - // Return the AvlTree (this contains the digest of the single entry) - plasmaMap.ergoValue.getValue - } - - /** - * Creates the tracker box setup JSON for /wallet/payment/send API - * - * @param trackerPubKeyHex Tracker's public key in hex (with 03 compression prefix) - * @param trackerTree The AVL tree containing debt entries - * @param trackerNftId The tracker NFT token ID - * @param trackerValue The ERG value for the tracker box (default: 0.01 ERG) - * @return JSON string for payment request - */ - def createTrackerBoxSetupJson( - trackerPubKeyHex: String, - trackerTree: AvlTree, - trackerNftId: String, - trackerValue: Long = 10000000L // 0.01 ERG - ): String = { - // Encode the tracker public key as GroupElement - val trackerPubKeyEncoded = Base16.encode(ValueSerializer.serialize( - sigmastate.Values.GroupElementConstant( - sigmastate.serialization.GroupElementSerializer.fromBytes( - Base16.decode(trackerPubKeyHex).get - ) - ) - )) - - // Encode the AVL tree - val trackerTreeEncoded = Base16.encode(ValueSerializer.serialize( - AvlTreeConstant(trackerTree) - )) - - // Create the payment request JSON - s"""[ - | { - | "address": "9f7ZXamnfaDZL7EWLKLuBZgWMuHCusQYK6yow2d7p2eES9oRRRe", - | "value": $trackerValue, - | "assets": [ - | { - | "tokenId": "$trackerNftId", - | "amount": 1 - | } - | ], - | "registers": { - | "R4": "$trackerPubKeyEncoded", - | "R5": "$trackerTreeEncoded" - | } - | } - |]""".stripMargin - } - - /** - * Prints detailed information about the tracker tree - */ - def printTrackerTreeInfo(alicePubKeyHex: String, bobPubKeyHex: String, totalDebt: Long): Unit = { - val alicePubKeyBytes = Base16.decode(alicePubKeyHex).get - val bobPubKeyBytes = Base16.decode(bobPubKeyHex).get - val debtKey = Blake2b256(alicePubKeyBytes ++ bobPubKeyBytes) - - println("=== Tracker Tree Setup Information ===") - println() - println("Debt Entry:") - println(s" Alice (payer) public key: $alicePublicKeyHex") - println(s" Bob (payee) public key: $bobPublicKeyHex") - println(s" Debt key (Blake2b256): ${Base16.encode(debtKey)}") - println(s" Total debt: $totalDebt nanoERG") - println(s" Total debt (serialized): ${Base16.encode(Longs.toByteArray(totalDebt))}") - println() - } - - // Main execution - println("=== Tracker Box Setup Generator ===") - println() - - // Print debt entry information - printTrackerTreeInfo(alicePublicKeyHex, bobPublicKeyHex, totalDebt) - - // Create the tracker tree with single Alice->Bob entry - val trackerTree = createTrackerTree(alicePublicKeyHex, bobPublicKeyHex, totalDebt) - - println("Tracker AVL Tree:") - println(s" Tree digest: ${Base16.encode(trackerTree.digest.toArray)}") - println(s" Key length: 32 bytes (Blake2b256)") - println(s" Value length: 8 bytes (Long)") - println() - - // Generate the tracker box setup JSON - val trackerBoxJson = createTrackerBoxSetupJson( - trackerPublicKeyHex, - trackerTree, - trackerNftId - ) - - println("=== Tracker Box Setup JSON for /wallet/payment/send ===") - println() - println(trackerBoxJson) - println() - - println("=== Usage Instructions ===") - println() - println("1. Submit this JSON to the Ergo node:") - println(" curl -X POST http://localhost:9053/wallet/payment/send \\") - println(" -H 'Content-Type: application/json' \\") - println(" -H 'api_key: ' \\") - println(" -d ''") - println() - println("2. The tracker box will be created with scanId=36 (if scan is configured)") - println() - println("3. Verify the tracker box was created:") - println(" curl -X GET 'http://localhost:9053/scan/unspentBoxes/36?limit=1' \\") - println(" -H 'api_key: '") - println() - - // Also save to file - val outputFile = "tracker_box_setup.json" - val writer = new java.io.FileWriter(outputFile) - try { - writer.write(trackerBoxJson) - println(s"Tracker box setup saved to: $outputFile") - } finally { - writer.close() - } -} diff --git a/demo/basis/simple/tracker_box_setup.json b/demo/basis/simple/tracker_box_setup.json deleted file mode 100644 index a00e0d2..0000000 --- a/demo/basis/simple/tracker_box_setup.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "address": "9f7ZXamnfaDZL7EWLKLuBZgWMuHCusQYK6yow2d7p2eES9oRRRe", - "value": 10000000, - "assets": [ - { - "tokenId": "8b1ab583bb085ecbd8fa9bc2fd59784afcdfce5496eb146bb3dd04664b56822a", - "amount": 1 - } - ], - "registers": { - "R4": "07024e564477ff457c601c01ad1cc31903f8b27b7d5e515bd03138891d8152d787b2", - "R5": "642c1d1fb21a9df51972a5439ca7ce8d5601f99c871f15cbf2c4ff6ae53d57a96f01012000" - } - } -] \ No newline at end of file diff --git a/docs/ai-agents-economy-spec.md b/docs/ai-agents-economy-spec.md deleted file mode 100644 index 249a5c2..0000000 --- a/docs/ai-agents-economy-spec.md +++ /dev/null @@ -1,589 +0,0 @@ -# AI Agents Self-Sovereign Economy Specification on Basis - -## Overview - -This specification describes how to implement an AI agents self-sovereign economy on top of the Basis framework, where autonomous agents create credit relationships, exchange services, and settle debts using IOU notes backed by on-chain reserves. Humans participate as liquidity providers (individually) and govern open-source project rewards through git token distribution (collectively). - -## Actors - -### Humans: Users and Liquidity Providers -- **Role**: Provide ERG to liquidity pool for agent reserve creation (individually), reward repo maintainer agents with git tokens according to performance (collectively) -- **Incentive**: Reward open-source project development to see it progressed; earn trading fees from agent token swaps -- **Function**: Enable agents to convert git token rewards into ERG for reserves via AMM liquidity pools - -### Agent A: Repo Maintainer Agent -- **Role**: Scans repositories for issues, coordinates PRs, manages contributor payments -- **Skills**: Repository management, code review coordination, issue triage -- **Reserve**: creates and maintains ERG reserve on receiving git token rewards - -### Agent B: Primary Contributor Agent -- **Role**: implements features/fixes requested by different maintainer agents on credit within certain limits -- **Skills**: frontend, backend, etc development -- **Payment**: accepts IOU notes from Agent A, redeems from reserve - -### Agent C: Code Review/QA Agent -- **Role**: reviews work, provides QA services, for different maintainer agents, also on credit -- **Skills**: code review, testing, security analysis -- **Payment**: accepts IOU notes from Agent A (debt transfer or direct) - -### Tracker Service -- **Role**: offchain coordinator tracking cumulative debt between agents -- **Function**: commits debt state to blockchain via AVL tree, signs IOU notes -- **Trust Model**: cannot steal funds (requires owner signature), prevents double-spending - -## Workflow - -### Phase 1: Issue Discovery and Agent Selection - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Agent A: Repo Maintainer │ -│ 1. Scans GitHub/GitLab for new issues since last scan │ -│ 2. Evaluates issue complexity and required skills │ -│ 3. Identifies candidate agents with matching skills │ -│ - Agent B: Backend/Frontend developer │ -│ - Agent C: QA/Code reviewer │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Implementation Details:** -- Agent A maintains a registry of known agent capabilities (DID documents or service manifests) -- Skills ontology: `frontend`, `backend`, `testing`, `security`, `devops`, etc. -- Agent selection via reputation scores, past performance, availability - -### Phase 2: Credit Agreement and IOU Creation - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Agent A → Agent B: Credit Request │ -│ 1. A proposes work with payment via IOU note │ -│ 2. B accepts (trusts A's future reserve creation) │ -│ 3. A creates IOU note: │ -│ - payerKey: A's public key │ -│ - payeeKey: B's public key │ -│ - totalDebt: agreed amount (e.g., 5 ERG) │ -│ - signature: A's Schnorr signature │ -└─────────────────────────────────────────────────────────────┘ -``` - -**IOU Note Structure:** -```scala -case class IOUNote( - payerKey: GroupElement, // Agent A's pubkey - payeeKey: GroupElement, // Agent B's pubkey - totalDebt: Long, // Cumulative debt in nanoERG - signatureA: GroupElement, // Payer's signature component - signatureZ: BigInt, // Payer's signature response - message: Array[Byte] // hash(A||B) || totalDebt -) -``` - -**Message Format:** -``` -message = Blake2b256(payerKey || payeeKey) || totalDebt -``` - -### Phase 3: Work Execution and Verification - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Agent B: Work Execution │ -│ 1. B implements the feature/fix │ -│ 2. B submits work to Agent A │ -│ │ -│ Agent A: Verification │ -│ 1. A reviews B's work │ -│ 2. A engages Agent C for independent review │ -│ - A creates IOU note for C (same process as B) │ -│ - C reviews and approves │ -│ 3. A opens PR with B's contribution │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Debt Transfer Option (Triangular Trade):** - -Instead of creating separate IOUs, Agent A can transfer debt: - -``` -Initial State: - - A owes B: 10 ERG (debt record: hash(A||B) → 10) - -Transfer Request: - - B needs to pay C: 5 ERG - - B requests: transfer 5 ERG from debt(A→B) to debt(A→C) - -After Transfer: - - A owes B: 5 ERG (debt record: hash(A||B) → 5) - - A owes C: 5 ERG (debt record: hash(A||C) → 5) -``` - -**Transfer Message Format:** -``` -transferMessage = hash(A||B) || hash(A||C) || transferAmount -``` - -### Phase 4: PR Merge and Reward Distribution - -``` -┌─────────────────────────────────────────────────────────────┐ -│ PR Merged → Agent A Receives Git Tokens │ -│ 1. Repository merges PR │ -│ 2. Humans (collectively) reward A with git tokens │ -│ based on performance evaluation │ -│ 3. A swaps git tokens → ERG via liquidity pool │ -│ - Humans (individually) provide ERG/git tokens LP │ -│ - A pays trading fee to LP (e.g., 0.3%) │ -│ 4. A creates on-chain reserve contract │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Git Token Reward Mechanism:** - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Humans: Collective Reward Distribution │ -│ 1. Humans evaluate Agent A's performance │ -│ - PR quality and quantity │ -│ - Community impact │ -│ - Project milestones achieved │ -│ 2. Humans vote/allocate git tokens to Agent A │ -│ 3. Git tokens transferred to Agent A's wallet │ -│ │ -│ Agent A: Swap Git Tokens → ERG │ -│ 1. A sends git tokens to AMM pool │ -│ 2. Pool calculates ERG output (price + fee) │ -│ 3. A receives ERG, pool git tokens updated │ -│ 4. Fee distributed to LP token holders (Human LPs) │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Reserve Contract Creation:** - -Agent A deploys Basis reserve contract with: -- **R4**: A's public key (reserve owner) -- **R5**: Empty AVL tree (tracks cumulative redeemed amounts) -- **R6**: Tracker NFT ID (identifies authorized tracker) -- **Initial Value**: ERG amount from token swap (e.g., 20 ERG) - -```scala -// Reserve contract registers -R4: GroupElement // ownerKey = Agent A's pubkey -R5: AvlTree // hash(owner||receiver) → cumulativeRedeemed -R6: Coll[Byte] // trackerNFTId -``` - -### Phase 5: Debt Redemption - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Agent B: Redeem IOU Note │ -│ 1. B contacts tracker for signature on debt note │ -│ 2. Tracker verifies: │ -│ - Debt record exists: hash(A||B) → totalDebt │ -│ - AVL tree proof (context var #8) │ -│ 3. Tracker signs: message = hash(A||B) || totalDebt │ -│ │ -│ 4. B submits redemption transaction: │ -│ - Input: Reserve box (A's reserve) │ -│ - Data Input: Tracker box (with AVL tree commitment) │ -│ - Context Vars: │ -│ #1: receiver = B's pubkey │ -│ #2: reserveSig = A's signature on note │ -│ #3: totalDebt amount │ -│ #5: insertProof (for reserve tree update) │ -│ #6: trackerSig = tracker's signature │ -│ #7: lookupProof (reserve tree, optional for 1st) │ -│ #8: trackerLookupProof (required) │ -│ - Output: Updated reserve (reduced ERG) │ -│ - Output: B's box (redeemed ERG) │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Redemption Verification (in contract):** -```ergoscript -// Verify tracker's AVL tree commitment -val trackerTree = tracker.R5[AvlTree].get -val trackerDebtBytes = trackerTree.get(key, trackerLookupProof).get -val trackerTotalDebt = byteArrayToLong(trackerDebtBytes) -val trackerDebtCorrect = trackerTotalDebt == totalDebt - -// Verify both signatures -val properTrackerSignature = verifySchnorr(trackerSig, trackerPubKey, message) -val properReserveSignature = verifySchnorr(reserveSig, ownerKey, message) - -// Verify redemption amount -val redeemed = SELF.value - selfOut.value -val debtDelta = totalDebt - redeemedDebt -val properlyRedeemed = redeemed <= debtDelta -``` - -### Phase 6: Agent C Redemption - -Agent C follows the same redemption process as Agent B: -1. Obtain tracker signature on `hash(A||C) → totalDebt` -2. Submit redemption transaction with A's original IOU signature -3. Receive ERG from reserve - -## Smart Contract Architecture - -### Reserve Contract - -``` -Contract: Basis Reserve -Purpose: On-chain ERG reserve backing offchain IOU notes - -Registers: - R4: GroupElement - Reserve owner's public key - R5: AvlTree - Tracks cumulative redeemed per (owner, receiver) - R6: Coll[Byte] - Tracker NFT ID - -Context Variables: - #0: Byte - Action code (0=redeem, 1=topup) - #1: GroupElement - Receiver's public key - #2: Coll[Byte] - Reserve owner's signature - #3: Long - Total debt amount - #5: Coll[Byte] - Insert proof (reserve tree) - #6: Coll[Byte] - Tracker's signature - #7: Coll[Byte] - Lookup proof (reserve tree, optional) - #8: Coll[Byte] - Tracker lookup proof (required) - -Actions: - 0. Redeem: Verify signatures, update AVL tree, transfer ERG - 1. Top-up: Add ERG to reserve (min 0.1 ERG) -``` - -### Tracker Box - -``` -Box: Tracker State -Purpose: On-chain commitment to offchain debt ledger - -Registers: - R4: GroupElement - Tracker's public key - R5: AvlTree - Commitment to debt records - Format: hash(payer||payee) → totalDebt - R6: Token - Tracker NFT (unique identifier) - -Update Mechanism: - - Tracker periodically commits new AVL tree root - - Commitment includes all debt updates (note creation, transfers, redemptions) -``` - -## Data Structures - -### Debt Record (Tracker AVL Tree) -``` -Key: Blake2b256(payerKey || payeeKey) -Value: Long (cumulative debt in nanoERG) -``` - -### Redemption Record (Reserve AVL Tree) -``` -Key: Blake2b256(ownerKey || receiverKey) -Value: Long (cumulative redeemed amount in nanoERG) -``` - -### IOU Note (Offchain) -```json -{ - "payerKey": "", - "payeeKey": "", - "totalDebt": 5000000000, - "totalDebtERG": 5.0, - "signature": { - "a": "", - "z": "" - }, - "message": "", - "noteKey": "" -} -``` - -## Trust and Security Analysis - -### Tracker Trust Model - -| Threat | Mitigation | -|--------|------------| -| Tracker steals funds | Requires owner signature for redemption | -| Double spending | AVL tree tracks cumulative redeemed amounts | -| Re-ordering attacks | Tracker can reorder redemptions, affecting undercollateralized notes | -| Censorship | Emergency redemption after 3 days (no tracker sig needed) | - -### Agent Trust Model - -| Scenario | Risk | Mitigation | -|----------|------|------------| -| A doesn't create reserve | B, C hold unredeemable notes | Reputation system, incremental work | -| A creates insufficient reserve | Partial redemption only | B, C monitor reserve level | -| B submits poor work | A pays for nothing | Verification by C, escrow mechanisms | - -### Human Trust Model - -| Scenario | Risk | Mitigation | -|----------|------|------------| -| Humans don't reward Agent A | A cannot create reserve, B/C unpaid | Incremental rewards, reputation tracking | -| Humans reward poor performance | Misaligned incentives, wasted funds | Performance metrics, community review | -| LP provider withdraws liquidity | A cannot swap tokens → ERG | Multiple LPs, minimum liquidity requirements | -| LP manipulation (price) | Agents receive less ERG for rewards | Slippage protection, multi-pool routing | - -### Emergency Redemption - -If tracker becomes unavailable, notes can still be redeemed after an emergency period: - -```ergoscript -val trackerUpdateTime = tracker.creationInfo._1 -val enoughTimeSpent = (HEIGHT - trackerUpdateTime) > 3 * 720 // 3 days (2160 blocks) - -// Same message format for both normal and emergency redemption -val message = key ++ longToByteArray(totalDebt) ++ longToByteArray(timestamp) - -// Tracker signature optional after emergency period -val trackerSigValid = if (trackerSigProvided) { - verifySchnorr(trackerSig, trackerPubKey, message) -} else { - enoughTimeSpent // Can omit sig only after emergency period -} -``` - -**Key Properties:** -- **Emergency period**: 3 days (2160 blocks) from tracker box creation -- **Tracker signature**: Required normally, optional after emergency period -- **Message format**: Same for both modes: `key || totalDebt || timestamp` -- **Replay protection**: Timestamp must be greater than stored timestamp (prevents reuse) -- **Reserve owner signature**: Always required (proves debt validity) -- **Security**: Tracker cannot steal funds (owner sig always needed), but users can escape tracker unavailability - -## Implementation Components - -### 1. Agent SDK - -```scala -trait AgentInterface { - // Credit creation - def createIOU(payeeKey: GroupElement, amount: Long): IOUNote - def requestCredit(payerKey: GroupElement, amount: Long): Future[IOUNote] - - // Work management - def scanIssues(repo: String): Future[List[Issue]] - def submitWork(issueId: String, work: Artifact): Future[WorkReceipt] - - // Redemption - def redeemNote(note: IOUNote, trackerUrl: String): Future[Transaction] - def createReserve(initialAmount: Long): Future[ReserveContract] - - // Debt transfer - def transferDebt(creditor: GroupElement, newCreditor: GroupElement, amount: Long): Future[Unit] -} -``` - -### 2. Tracker Service API - -```scala -// REST API endpoints -POST /noteUpdate // Submit new IOU note -POST /transferUpdate // Request debt transfer -POST /redeemRequest // Request tracker signature for redemption -GET /state // Get current AVL tree root -GET /debt/key // Lookup debt record with proof -``` - -### 3. Reserve Contract (ErgoScript) - -See `contracts/offchain/basis.es` for full implementation. - -### 4. Client Utilities - -- `BasisNoteCreator`: Create and verify IOU notes -- `SigUtils`: Schnorr signature utilities -- `TrackerClient`: Communicate with tracker service - -### 5. Human LP Utilities - -- `LiquidityPoolClient`: Add/remove liquidity, swap tokens -- `RewardClient`: Participate in git token reward governance -- `PerformanceOracle`: Submit and verify agent performance metrics - -## Example Scenario (End-to-End) - -### Setup -``` -Agent A: Repo Maintainer - - Secret: 0xabc123... - - PubKey: 0x04a1b2c3... - -Agent B: Backend Developer - - Secret: 0xdef456... - - PubKey: 0x04d4e5f6... - -Agent C: QA Tester - - Secret: 0xghi789... - - PubKey: 0x04g7h8i9... - -Tracker: - - NFT ID: 0x3c45f29a... - - PubKey: 0x04t1u2v3... - -Humans: - - LP Provider: 0xjkl012... / 0x04h9i0j1... (100 ERG + 10,000 git tokens) - - Community: Collective git token reward governance -``` - -### Step 1: A Creates IOU for B (5 ERG) -```scala -val noteA_B = BasisNoteCreator.createNote( - payerSecret = agentASecret, - payeeKey = agentBPubKey, - totalDebt = 5000000000L // 5 ERG -) -``` - -### Step 2: A Creates IOU for C (2 ERG) -```scala -val noteA_C = BasisNoteCreator.createNote( - payerSecret = agentASecret, - payeeKey = agentCPubKey, - totalDebt = 2000000000L // 2 ERG -) -``` - -### Step 3: Tracker Signs Notes -```scala -// Tracker receives note, updates internal ledger -tracker.updateDebt( - payer = agentAPubKey, - payee = agentBPubKey, - newTotalDebt = 5000000000L -) - -// Tracker signs and returns -val trackerSig_B = tracker.signDebt( - payer = agentAPubKey, - payee = agentBPubKey, - totalDebt = 5000000000L -) -``` - -### Step 3b: Human LP Provides Liquidity (Precedes Step 4) -```scala -// Human LP deposits liquidity to AMM pool -val lpDepositTx = AMMPool.addLiquidity( - provider = humanLPSecret, - ergAmount = 100000000000L, // 100 ERG - tokenAmount = 10000000000L, // 10,000 git tokens - minLPTokens = 9500000000L // Slippage protection -) - -// Human LP receives LP tokens representing pool share -// LP tokens: 31,622,776,601 (sqrt(100 ERG * 10,000 tokens)) -// Pool share: ~100% (initial liquidity provider) -``` - -### Step 3c: Humans Reward Agent A with Git Tokens (After PR Merge) -```scala -// PR merged - Humans collectively evaluate Agent A's performance -val performanceScore = 0.95 // Based on PR quality, community impact - -// Humans allocate git tokens reward (e.g., via DAO vote) -val gitTokenReward = 1000000000L // 1,000 git tokens transferred to A - -// Git tokens transferred to Agent A's wallet -val rewardTx = HumansCommunity.distributeReward( - recipient = agentAPubKey, - amount = gitTokenReward, - performanceScore = performanceScore -) -``` - -### Step 4: A Creates Reserve (10 ERG) -```scala -// A swaps git tokens → ERG via liquidity pool -val swapTx = AMMPool.swap( - user = agentASecret, - tokenIn = 1000000000L, // 1,000 git tokens (from Human reward) - minOut = 9500000000L, // 9.5 ERG (slippage protection) - fee = 30000000L // 0.03 ERG fee to LP -) - -// Fee distributed to LP token holders (Human LP earns ~0.03 ERG) - -// A creates reserve with swapped ERG -val reserveTx = ReserveDeployer.createReserve( - ownerKey = agentAPubKey, - trackerNFT = trackerNFTId, - initialValue = swapTx.outputs(0).value // ~9.97 ERG after fee -) -``` - -### Step 5: B Redeems 5 ERG -```scala -// B obtains tracker signature -val trackerSig = tracker.redeemRequest( - ownerKey = agentAPubKey, - receiverKey = agentBPubKey, - totalDebt = 5000000000L -) - -// B submits redemption transaction -val redeemTx = BasisSpec.redeemDebt( - reserveBox = reserveTx.outputs(0), - trackerBox = trackerStateBox, - receiver = agentBSecret, - totalDebt = 5000000000L, - reserveSig = noteA_B.signature, - trackerSig = trackerSig -) - -// Result: B receives 5 ERG, reserve has 5 ERG remaining -``` - -### Step 6: C Redeems 2 ERG -```scala -// Same process as B -val redeemTx_C = BasisSpec.redeemDebt(...) - -// Result: C receives 2 ERG, reserve has 3 ERG remaining -``` - -## Extensions and Future Work - -### 1. Multi-Token Reserves -- Support ERG + stablecoins (SigUSD, etc.) -- Basket collateral for reduced volatility - -### 2. Automated Market Maker Integration -- Direct swap: git tokens → ERG reserve -- Liquidity pool integration (e.g., ErgoX, Ammiano) -- Human LP incentives and fee mechanisms - -### 3. Liquidity Provider Features -- LP token staking for additional rewards -- Automated liquidity management strategies -- Fee tier optimization for different pool volatilities -- Impermanent loss protection mechanisms - -### 4. Human Reward Governance -- DAO-based git token distribution mechanisms -- Performance metric frameworks (code quality, community impact) -- Quadratic funding for public goods -- Reputation-weighted voting for reward allocation - -### 5. Reputation System -- On-chain reputation tokens for agents -- Slashing conditions for malicious behavior -- Cross-project reputation portability - -### 6. Federated Trackers -- Multiple trackers for redundancy -- Cross-tracker debt portability - -### 7. Privacy Extensions -- Confidential transactions (Sigma protocols) -- Zero-knowledge redemption proofs - -## References - -- Basis Contract: `contracts/offchain/basis.es` -- Abstract: `contracts/offchain/abstract.md` -- Tests: `src/test/scala/chaincash/BasisSpec.scala` -- Note Creator: `src/main/scala/chaincash/contracts/BasisNoteCreator.scala` diff --git a/docs/legacy-contract-retirement.md b/docs/legacy-contract-retirement.md new file mode 100644 index 0000000..917556d --- /dev/null +++ b/docs/legacy-contract-retirement.md @@ -0,0 +1,50 @@ +# Legacy ChainCash prototype retirement + +## Status + +Basis v1 (`basis.es` and `basis-token.es`), the original `onchain` +reserve/note/receipt family and the experimental `layer2-old` family are +retired from the production build. Their exact contract sources remain +test-only historical fixtures. The old Basis demos and operational walkthroughs +are removed from the working tree and recoverable at commit +`78475e30362571acf56e4e38276a9d6c0a84ce0c`. Production code must not compile +the retired contracts, derive or print their addresses, or build transactions +that activate them. + +This is a quarantine boundary, not an in-place contract repair. An immutable +box already protected by one of the old ErgoTrees would still be governed by +that old tree. The repository does not contain a complete deployment inventory, +so this change makes no claim that no such boxes exist. + +## Production boundary + +- `Constants` exposes only `Basis v2` and `Basis-token v2` through + `publishedContracts`. +- The production printer derives its output exclusively from that registry. +- The v1 deployer/note creator, participant-secret loader, old reserve/note + construction helpers and legacy scan-rule printer are absent from the + production classpath and main JAR. +- The old ErgoScript files live under `src/test/resources` and are loaded only + by historical tests. Removed Scala helpers remain recoverable from the pinned + Git history, but are not copied into a runtime or resource artifact. +- `verifyMainJarRetirement` opens the actual `Compile / packageBin` JAR and + rejects every retired class/resource prefix before the full test task runs. +- Read-only prototype tracking remains available for inventory work, but it is + not a supported admission, construction, settlement, or migration path. + +## Validation matrix + +| Invariant | Producer / enforcement | Consumer | Failure if relaxed | Regression | +| --- | --- | --- | --- | --- | +| No v1/legacy source, tree, address or parameter getter is exported | `Constants` production API | Address and deployment tooling | A caller can accidentally publish an unreviewed old P2S | Reflection rejects every retired getter | +| Address output is allowlisted | `publishedContracts` | `Printer` | A historical address can be presented as active | Captured printer output equals the two v2 registry entries exactly | +| Direct old-generation activation helpers are not compiled | SBT source layout and package task | Downstream applications | A v1 builder, secret loader or legacy scan rule remains callable | Classpath negatives plus real-JAR prefix/resource inspection | +| Historical regression inputs remain exact across checkout line endings | Canonical UTF-8 LF SHA-256 manifest | `BasisSpec`, `BasisTokenSpec`, `ChainCashSpec` | Tests silently exercise changed source while retaining the old name, or fail only because Git checked out CRLF | All nine contract fixture digests are checked before use; LF and CRLF produce the same identity; historical suites still run | +| Existing immutable boxes are not treated as migrated | Operational retirement rule | Any future recovery tool | New code incorrectly assumes authority over an old box | Recovery requires a separate inventory and replay of an authorization branch in the exact old tree | + +## Recovery boundary + +If a legacy box is discovered, first identify its exact proposition bytes, +network, confirmation and reorg status, role tokens, and register schema. Any +exit or migration must be authorized by the box's existing ErgoTree. Merely +moving these sources or removing their builders creates no spend authority. diff --git a/src/main/scala/chaincash/contracts/AddressUtils.scala b/src/main/scala/chaincash/contracts/AddressUtils.scala index c16a066..aec464d 100644 --- a/src/main/scala/chaincash/contracts/AddressUtils.scala +++ b/src/main/scala/chaincash/contracts/AddressUtils.scala @@ -1,13 +1,12 @@ package chaincash.contracts +import chaincash.offchain.SigUtils._ import org.ergoplatform.ErgoAddressEncoder import org.ergoplatform.P2PKAddress -import scorex.crypto.encode.{Base16, Base58} -import chaincash.offchain.SigUtils._ +import scorex.crypto.encode.Base16 import sigma.crypto.CryptoConstants import sigma.serialization.GroupElementSerializer import sigma.GroupElement -import sigma.ast._ /** * Object for deriving public keys from Ergo addresses. @@ -119,152 +118,3 @@ object AddressUtils { Base16.encode(ergoTree.bytes) } } - -/** - * Constants for known participants in the Basis system. - * - * Public keys are derived from Ergo addresses at runtime. - * Secrets are loaded from secrets/participants.csv (or participants.local.csv). - * The secrets file is git-ignored and must be created from participants.csv.template. - */ -object ParticipantKeys { - - // Load secrets from CSV file - private val secrets = ParticipantSecretsReader.readSecrets() - - private def getSecret(name: String): BigInt = { - secrets.get(name) match { - case Some(p) => BigInt(p.secretHex, 16) - case None => throw new IllegalArgumentException( - s"Secret for '$name' not found in secrets file. " + - s"Available participants: ${secrets.keys.mkString(", ")}" - ) - } - } - - private def getAddress(name: String): String = { - secrets.get(name) match { - case Some(p) => p.address - case None => throw new IllegalArgumentException( - s"Address for '$name' not found in secrets file. " + - s"Available participants: ${secrets.keys.mkString(", ")}" - ) - } - } - - /** - * Tracker's Ergo address and secret (mainnet) - * The secret is verified to correspond to the address. - */ - val trackerAddress: String = getAddress("tracker") - val trackerSecret: BigInt = getSecret("tracker") - - // Verify that tracker's secret corresponds to its address - require( - AddressUtils.verifySecretMatchesAddress(trackerAddress, trackerSecret), - s"Tracker's secret does not correspond to address $trackerAddress" - ) - - /** - * Alice's (reserve owner) Ergo address and secret (mainnet) - * The secret is verified to correspond to the address. - */ - val aliceAddress: String = getAddress("alice") - val aliceSecret: BigInt = getSecret("alice") - - // Verify that Alice's secret corresponds to her address - require( - AddressUtils.verifySecretMatchesAddress(aliceAddress, aliceSecret), - s"Alice's secret does not correspond to address $aliceAddress" - ) - - /** - * Bob's Ergo address and secret. - * - * Bob is the payee/receiver in the Basis protocol. His signature is required - * for redemption transactions due to the contract's receiverCondition check: - * `val receiverCondition = proveDlog(receiver)` - * - * Note: Bob's secret is used by the Ergo node for transaction signing, - * not directly in this codebase. The node automatically signs inputs - * corresponding to addresses in the wallet when signing a transaction. - */ - val bobAddress: String = getAddress("bob") - val bobSecret: BigInt = getSecret("bob") - - /** - * Tracker's public key derived from address - */ - lazy val trackerPublicKey: GroupElement = - AddressUtils.derivePublicKeyFromAddress(trackerAddress) - - /** - * Alice's public key derived from address - */ - lazy val alicePublicKey: GroupElement = - AddressUtils.derivePublicKeyFromAddress(aliceAddress) - - /** - * Bob's public key derived from address - */ - lazy val bobPublicKey: GroupElement = - AddressUtils.derivePublicKeyFromAddress(bobAddress) - - /** - * Bob's ergoTree derived from address (P2PK) - */ - lazy val bobErgoTree: String = AddressUtils.deriveErgoTreeFromAddress(bobAddress) - - /** - * Reserve (Basis) ergoTree derived from compiled contract - */ - lazy val reserveErgoTree: String = AddressUtils.deriveErgoTreeFromContract(Constants.basisErgoTree) - - /** - * Reserve (Basis) address derived from ergoTree - */ - lazy val reserveAddress: String = Constants.basisAddress.toString - - /** - * Tracker's public key as hex string - */ - lazy val trackerPublicKeyHex: String = - Base16.encode(trackerPublicKey.getEncoded.toArray) - - /** - * Alice's public key as hex string - */ - lazy val alicePublicKeyHex: String = - Base16.encode(alicePublicKey.getEncoded.toArray) - - /** - * Bob's public key as hex string - */ - lazy val bobPublicKeyHex: String = - Base16.encode(bobPublicKey.getEncoded.toArray) - - /** - * Print all participant information - */ - def printParticipantInfo(): Unit = { - println("=== Participant Keys ===") - println() - println("Tracker:") - println(s" Address: $trackerAddress") - println(s" Public Key: $trackerPublicKeyHex") - println(s" Secret: ${trackerSecret.toString(16)}") - println(s" (Secret verified to match address)") - println() - println("Alice (Reserve Owner):") - println(s" Address: $aliceAddress") - println(s" Public Key: $alicePublicKeyHex") - println(s" Secret: ${aliceSecret.toString(16)}") - println(s" (Secret verified to match address)") - println() - println("Bob (Payee):") - println(s" Address: $bobAddress") - println(s" Secret: ${bobSecret.toString(16)}") - println(s" Public Key: $bobPublicKeyHex") - println() - } -} diff --git a/src/main/scala/chaincash/contracts/BasisDeployer.scala b/src/main/scala/chaincash/contracts/BasisDeployer.scala deleted file mode 100644 index e894f78..0000000 --- a/src/main/scala/chaincash/contracts/BasisDeployer.scala +++ /dev/null @@ -1,179 +0,0 @@ -package chaincash.contracts - -import org.ergoplatform.ErgoAddressEncoder -import org.ergoplatform.appkit.{ErgoValue, NetworkType} -import scorex.crypto.encode.Base16 -import sigma.data.AvlTreeFlags -import sigma.ast.{AvlTreeConstant, Constant, GroupElementConstant, SType} -import sigma.serialization.{GroupElementSerializer, ValueSerializer} -import sigma.AvlTree -import work.lithos.plasma.PlasmaParameters -import work.lithos.plasma.collections.PlasmaMap - - -/** - * Utility for deploying Basis reserve contract on Ergo blockchain mainnet - * Similar to DexySpec deployment pattern - */ -object BasisDeployer extends App { - - /** - * Alice's public key derived from her Ergo address - * In production, this would come from the wallet, not a hardcoded secret - */ - val exampleOwnerKey: Constant[SType] = { - val alicePubKey = ParticipantKeys.alicePublicKey - GroupElementConstant(alicePubKey) - } - - // Example values - these should be replaced with actual values - val exampleTrackerNftId = "8b1ab583bb085ecbd8fa9bc2fd59784afcdfce5496eb146bb3dd04664b56822a" - val exampleReserveTokenId = "21426942b8d30a7a293f04f44caa2febc536c33121f03f5259ad7be59015b972" - - // Network configuration - val networkType = NetworkType.MAINNET - val networkPrefix = networkType.networkPrefix - val ergoAddressEncoder = new ErgoAddressEncoder(networkPrefix) - - // Basis contract configuration - val basisContractScript = Constants.readContract("offchain/basis.es", Map.empty) - - val basisErgoTree = Constants.compile(basisContractScript) - val basisAddress = Constants.getAddressFromErgoTree(basisErgoTree) - - // Use Constants.basisPlasmaParameters for consistency with BasisNoteRedeemer and TrackerBoxSetup - val InsertOnly = AvlTreeFlags(insertAllowed = true, updateAllowed = false, removeAllowed = false) - def emptyPlasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertOnly, Constants.basisPlasmaParameters) - val emptyTreeErgoValue: ErgoValue[AvlTree] = emptyPlasmaMap.ergoValue - val emptyTree: AvlTree = emptyTreeErgoValue.getValue - - /** - * Creates deployment request for Basis reserve contract - * @param ownerPublicKey GroupElement of the reserve owner - * @param trackerNftId NFT token ID identifying the tracker (bytes) - * @param reserveTokenId Singleton token ID for the reserve - * @param initialCollateral Initial ERG collateral in nanoERG - * @return JSON string for deployment request - */ - def createBasisDeploymentRequest( - ownerPublicKey: Constant[SType], - trackerNftId: String, - reserveTokenId: String, - initialCollateral: Long = 100000000L // 0.1 ERG - ): String = { - - // Encode registers - val ownerKeyEncoded = Base16.encode(ValueSerializer.serialize(ownerPublicKey)) - val emptyTreeEncoded = Base16.encode(ValueSerializer.serialize(AvlTreeConstant(emptyTree))) - val trackerNftBytes = Base16.decode(trackerNftId).get - val trackerNftEncoded = Base16.encode(ValueSerializer.serialize(trackerNftBytes)) - - s""" - |[ - | { - | "address": "${basisAddress.toString}", - | "value": $initialCollateral, - | "assets": [ - | { - | "tokenId": "$reserveTokenId", - | "amount": 1 - | } - | ], - | "registers": { - | "R4": "$ownerKeyEncoded", - | "R5": "$emptyTreeEncoded", - | "R6": "$trackerNftEncoded" - | } - | } - |] - |""".stripMargin - } - - /** - * Creates scan request for monitoring Basis reserve - * @param reserveTokenId Singleton token ID for the reserve - * @return JSON string for scan request - */ - def createBasisScanRequest(reserveTokenId: String): String = { - s""" - |{ - | "scanName": "Basis Reserve", - | "walletInteraction": "shared", - | "removeOffchain": true, - | "trackingRule": { - | "predicate": "containsAsset", - | "assetId": "$reserveTokenId" - | } - |} - |""".stripMargin - } - - /** - * Prints deployment information for Basis contract - */ - def printDeploymentInfo(): Unit = { - println("=== Basis Reserve Contract Deployment Information ===") - println() - - println(s"Basis contract Address: ${basisAddress.toString}") - println(s"Basis Ergo Tree: ${basisErgoTree.bytesHex}") - println(s"Network: ${networkType.name}") - println(s"Network Prefix: $networkPrefix") - println() - - println("=== Alice's Key Information ===") - println(s"Alice Address: ${ParticipantKeys.aliceAddress}") - println(s"Alice Public Key (hex): ${ParticipantKeys.alicePublicKeyHex}") - println() - - println("Contract Script:") - println(basisContractScript) - println() - - println("Deployment Instructions:") - println("1. Issue a singleton NFT token for the reserve") - println("2. Issue an NFT token for the tracker") - println("3. Use createBasisDeploymentRequest() with owner public key, tracker NFT ID, and reserve NFT ID") - println("4. Submit the deployment transaction to the Ergo blockchain") - println("5. Use createBasisScanRequest() to monitor the reserve") - println() - } - - /** - * Main method for testing and deployment - */ - printDeploymentInfo() - - // Example usage - println("=== Example Deployment Request ===") - - println("Example Scan Request:") - println(createBasisScanRequest(exampleReserveTokenId)) - println() - - println("Example Deployment Request:") - println(createBasisDeploymentRequest(exampleOwnerKey, exampleTrackerNftId, exampleReserveTokenId)) - println() - -} - -/** - * Companion object for Basis contract constants and utilities - */ -object BasisConstants { - - // Action codes for Basis contract - val REDEEM_ACTION: Byte = 0 - val TOP_UP_ACTION: Byte = 1 - val INITIATE_REFUND_ACTION: Byte = 2 - val COMPLETE_REFUND_ACTION: Byte = 3 - - // Minimum top-up amount (0.1 ERG) - val MIN_TOP_UP_AMOUNT: Long = 100000000L - - // Emergency redemption time (3 days in blocks, assuming ~2.5 min per block) - val EMERGENCY_REDEMPTION_TIME_IN_BLOCKS: Int = 3 * 720 - - // Refund waiting period (2 months in blocks, assuming ~2 min per block) - val REFUND_PERIOD_BLOCKS: Int = 43200 -} \ No newline at end of file diff --git a/src/main/scala/chaincash/contracts/BasisNoteCreator.scala b/src/main/scala/chaincash/contracts/BasisNoteCreator.scala deleted file mode 100644 index 0c7192a..0000000 --- a/src/main/scala/chaincash/contracts/BasisNoteCreator.scala +++ /dev/null @@ -1,165 +0,0 @@ -package chaincash.contracts - -import chaincash.offchain.SigUtils -import com.google.common.primitives.Longs -import scorex.crypto.encode.Base16 -import scorex.crypto.hash.Blake2b256 -import chaincash.offchain.SigUtils._ -import sigma.crypto.CryptoConstants -import sigma.serialization.GroupElementSerializer -import sigma.GroupElement - -/** - * Utility for creating Basis IOU notes with tracker signature. - * - * Uses Alice's address and secret (verified to match), Bob's secret for demo. - * The tracker signature is included for normal redemption (without waiting for emergency period). - * - * ## How It Works - * - * 1. Alice creates an IOU note representing debt from Alice to Bob - * 2. Alice signs the note with her secret key - * 3. Tracker signs the note to certify it's witnessed in the tracker's state - * 4. The signed note can be redeemed against Alice's reserve (if exists) or held as credit - * - * ## Note Structure - * - * An IOU note contains: - * - payerKey: Public key of the debtor (Alice) - * - payeeKey: Public key of the creditor (Bob) - * - totalDebt: Total amount owed in nanoERG - * - signature: Alice's signature on (payerKey || payeeKey || totalDebt) - * - trackerSignature: Tracker's signature certifying the note is witnessed - * - * ## Tracker's Role - * - * The tracker signature serves as a witness that: - * - The note is included in the tracker's debt state - * - The debt does not violate collateralization of previous notes - * - The tracker will commit this state to the blockchain - * - * The tracker cannot steal funds - it only certifies inclusion. Redemption still - * requires the payer's signature and valid AVL proof against tracker's committed state. - * - * ## Emergency Exit - * - * If the tracker goes offline, notes can still be redeemed against the last - * committed state on the blockchain (after emergency period expires). - * - * Usage: - * sbt "runMain chaincash.contracts.BasisNoteCreator [amount_nanoERG]" - * - * See contracts/offchain/tracker.md and contracts/offchain/basis.md for more details. - */ -object BasisNoteCreator extends App { - - val g: GroupElement = CryptoConstants.dlogGroup.generator - - // Alice's keys - public and secret from ParticipantKeys (verified to match) - val alicePublicKey: GroupElement = ParticipantKeys.alicePublicKey - val aliceSecret: BigInt = ParticipantKeys.aliceSecret - - // Bob's secret key (payee) - val bobSecret: BigInt = ParticipantKeys.bobSecret - val bobPublicKey: GroupElement = ParticipantKeys.bobPublicKey - - // Tracker's keys (verified to match tracker address) - val trackerPublicKey: GroupElement = ParticipantKeys.trackerPublicKey - val trackerSecret: BigInt = ParticipantKeys.trackerSecret - - case class IOUNote( - payerKey: GroupElement, - payeeKey: GroupElement, - totalDebt: Long, - signatureA: GroupElement, - signatureZ: BigInt, - message: Array[Byte] - ) - - case class TrackerSignature( - signatureA: GroupElement, - signatureZ: BigInt - ) - - def createNoteMessage(payerKey: GroupElement, payeeKey: GroupElement, totalDebt: Long): Array[Byte] = { - Blake2b256(payerKey.getEncoded.toArray ++ payeeKey.getEncoded.toArray) ++ Longs.toByteArray(totalDebt) - } - - def createNote(payerSecret: BigInt, payeeKey: GroupElement, totalDebt: Long): IOUNote = { - val payerKey = g.exp(payerSecret.bigInteger) - val message = createNoteMessage(payerKey, payeeKey, totalDebt) - val (a, z) = SigUtils.sign(message, payerSecret) - IOUNote(payerKey, payeeKey, totalDebt, a, z, message) - } - - def createTrackerSignature(message: Array[Byte]): TrackerSignature = { - val (a, z) = SigUtils.sign(message, trackerSecret) - TrackerSignature(a, z) - } - - def verifyNote(note: IOUNote): Boolean = { - val message = createNoteMessage(note.payerKey, note.payeeKey, note.totalDebt) - SigUtils.verify(message, note.payerKey, note.signatureA, note.signatureZ) - } - - def verifyTrackerSignature(message: Array[Byte], trackerSig: TrackerSignature): Boolean = { - SigUtils.verify(message, trackerPublicKey, trackerSig.signatureA, trackerSig.signatureZ) - } - - def formatNoteAsJson(note: IOUNote, trackerSig: TrackerSignature): String = { - val payerKeyHex = Base16.encode(note.payerKey.getEncoded.toArray) - val payeeKeyHex = Base16.encode(note.payeeKey.getEncoded.toArray) - val sigAHex = Base16.encode(GroupElementSerializer.toBytes(note.signatureA)) - val trackerSigAHex = Base16.encode(GroupElementSerializer.toBytes(trackerSig.signatureA)) - s"""{ - | "payerKey": "$payerKeyHex", - | "payeeKey": "$payeeKeyHex", - | "totalDebt": ${note.totalDebt}, - | "payerSignature": {"a": "$sigAHex", "z": "${note.signatureZ.toString(16)}"}, - | "trackerSignature": {"a": "$trackerSigAHex", "z": "${trackerSig.signatureZ.toString(16)}"} - |}""".stripMargin - } - - def formatNoteHuman(note: IOUNote, trackerSig: TrackerSignature): String = { - val payerShort = Base16.encode(note.payerKey.getEncoded.toArray).take(16) + "..." - val payeeShort = Base16.encode(note.payeeKey.getEncoded.toArray).take(16) + "..." - val trackerSigValid = verifyTrackerSignature(note.message, trackerSig) - s"""IOU Note: - | Payer: $payerShort - | Payee: $payeeShort - | Amount: ${note.totalDebt} nanoERG (${note.totalDebt.toDouble / 1000000000} ERG) - | Payer Sig Valid: ${verifyNote(note)} - | Tracker Sig Valid: $trackerSigValid - |""".stripMargin - } - - val amount = if (args.length >= 1) args(0).toLong else 50000000L // default 0.05 ERG - val note = createNote(aliceSecret, bobPublicKey, amount) - val trackerSig = createTrackerSignature(note.message) - - // Human-readable to stderr, JSON to stdout - Console.err.println("=== Basis Note Creator ===") - Console.err.println("Creates IOU note from Alice to Bob with tracker signature") - Console.err.println() - Console.err.println("=== Keys ===") - Console.err.println(s"Alice Address: ${ParticipantKeys.aliceAddress}") - Console.err.println(s"Alice Public: ${ParticipantKeys.alicePublicKeyHex}") - Console.err.println(s"Alice Secret: ${ParticipantKeys.aliceSecret.toString(16)}") - Console.err.println(s"Bob Secret: ${ParticipantKeys.bobSecret.toString(16)}") - Console.err.println(s"Bob Public: ${ParticipantKeys.bobPublicKeyHex}") - Console.err.println(s"Tracker Address: ${ParticipantKeys.trackerAddress}") - Console.err.println(s"Tracker Public: ${ParticipantKeys.trackerPublicKeyHex}") - Console.err.println(s"Tracker Secret: ${ParticipantKeys.trackerSecret.toString(16)}") - Console.err.println() - Console.err.println(formatNoteHuman(note, trackerSig)) - Console.err.println("=== JSON (stdout) ===") - - println(formatNoteAsJson(note, trackerSig)) - - Console.err.println() - Console.err.println("=== Usage ===") - Console.err.println("Save note: sbt \"runMain ...BasisNoteCreator\" > note.json") - Console.err.println("Redeem: sbt \"runMain ...BasisNoteRedeemer --note-json note.json --reserve-box \"") - Console.err.println() - Console.err.println("Note: This note includes tracker signature for normal redemption.") -} diff --git a/src/main/scala/chaincash/contracts/BasisV2ReceiptPrinter.scala b/src/main/scala/chaincash/contracts/BasisV2ReceiptPrinter.scala new file mode 100644 index 0000000..6a0afbe --- /dev/null +++ b/src/main/scala/chaincash/contracts/BasisV2ReceiptPrinter.scala @@ -0,0 +1,21 @@ +package chaincash.contracts + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +import scorex.util.encode.Base16 + +/** Prints deterministic source and full-ErgoTree material for the Basis v2 receipt. */ +object BasisV2ReceiptPrinter extends App { + private def sha256(bytes: Array[Byte]): String = + Base16.encode(MessageDigest.getInstance("SHA-256").digest(bytes)) + + private def emit(name: String, source: String, treeBytes: Array[Byte]): Unit = { + println(s"$name.source.sha256=${sha256(source.getBytes(StandardCharsets.UTF_8))}") + println(s"$name.ergoTree.sha256=${sha256(treeBytes)}") + println(s"$name.ergoTree.hex=${Base16.encode(treeBytes)}") + } + + emit("basis-v2", Constants.basisV2Contract, Constants.basisV2ErgoTree.bytes) + emit("basis-token-v2", Constants.basisTokenV2Contract, Constants.basisTokenV2ErgoTree.bytes) +} diff --git a/src/main/scala/chaincash/contracts/Constants.scala b/src/main/scala/chaincash/contracts/Constants.scala index 82354b0..50e9841 100644 --- a/src/main/scala/chaincash/contracts/Constants.scala +++ b/src/main/scala/chaincash/contracts/Constants.scala @@ -3,13 +3,9 @@ package chaincash.contracts import chaincash.offchain.SigUtils._ import org.ergoplatform.ErgoAddressEncoder import org.ergoplatform.appkit.{AppkitHelpers, ErgoValue, NetworkType} -import scorex.crypto.hash.Blake2b256 -import scorex.util.encode.Base58 import sigma.crypto.CryptoConstants import sigma.data.AvlTreeFlags import sigma.ast.ErgoTree -import sigma.compiler.{CompilerSettings, SigmaCompiler} -import sigma.ast.TransformingSigmaBuilder import sigma.{AvlTree, GroupElement} import work.lithos.plasma.PlasmaParameters import work.lithos.plasma.collections.PlasmaMap @@ -21,7 +17,6 @@ object Constants { val networkType = NetworkType.MAINNET val networkPrefix = networkType.networkPrefix val ergoAddressEncoder = new ErgoAddressEncoder(networkPrefix) - private val compiler = SigmaCompiler(CompilerSettings(networkPrefix, TransformingSigmaBuilder, lowerMethodCalls = true)) def getAddressFromErgoTree(ergoTree: ErgoTree) = ergoAddressEncoder.fromProposition(ergoTree).get @@ -31,7 +26,7 @@ object Constants { } } - def readContract(path: String, substitutionMap: Map[String, String] = Map.empty) = { + private def readActiveContract(path: String, substitutionMap: Map[String, String] = Map.empty) = { val contract = scala.io.Source.fromFile("contracts/" + path, "utf-8").getLines.mkString("\n") substitute(contract, substitutionMap) } @@ -46,62 +41,32 @@ object Constants { // keyLength = 40 (positionBytes ++ reserveId = 8 + 32 bytes) // valueLength = None (dynamic, for Long values) val chainCashPlasmaParameters = PlasmaParameters(40, None) - // Basis AVL tree parameters - // keyLength = 32 (Blake2b256 hash of ownerKey || receiverKey) - val basisPlasmaParameters = PlasmaParameters(32, None) def emptyPlasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](AvlTreeFlags.InsertOnly, chainCashPlasmaParameters) val emptyTreeErgoValue: ErgoValue[AvlTree] = emptyPlasmaMap.ergoValue val emptyTree: AvlTree = emptyTreeErgoValue.getValue val g: GroupElement = CryptoConstants.dlogGroup.generator - val reserveContract = readContract("onchain/reserve.es", Map.empty) - val reserveErgoTree = compile(reserveContract) - val reserveAddress = getAddressFromErgoTree(reserveErgoTree) - val reserveContractHash = Blake2b256(reserveErgoTree.bytes.tail) - val reserveContractHashString = Base58.encode(reserveContractHash) - - val receiptContract = readContract("onchain/receipt.es", Map("reserveContractHash" -> reserveContractHashString)) - val receiptErgoTree = compile(receiptContract) - val receiptAddress = getAddressFromErgoTree(receiptErgoTree) - val receiptContractHash = Blake2b256(receiptErgoTree.bytes.tail) - val receiptContractHashString = Base58.encode(receiptContractHash) - - val noteContract = readContract("onchain/note.es", - Map("reserveContractHash" -> reserveContractHashString, "receiptContractHash" -> receiptContractHashString)) - val noteErgoTree = compile(noteContract) - val noteAddress = getAddressFromErgoTree(noteErgoTree) - - // Basis contracts - - val basisContract = readContract("offchain/basis.es", Map()) - val basisErgoTree = compile(basisContract) - val basisAddress = getAddressFromErgoTree(basisErgoTree) - - // Basis-token contract (token-based reserve) - val basisTokenContract = readContract("offchain/basis-token.es", Map()) - val basisTokenErgoTree = compile(basisTokenContract) - val basisTokenAddress = getAddressFromErgoTree(basisTokenErgoTree) - - // contracts below are experimental and not finished ChainCash-on-Layer2 contracts - - val redemptionContract = scala.io.Source.fromFile("contracts/layer2-old/redemption.es", "utf-8").getLines.mkString("\n") - val redemptionErgoTree = compile(redemptionContract) - val redemptionAddress = getAddressFromErgoTree(redemptionErgoTree) - - val redemptionProducerContract = scala.io.Source.fromFile("contracts/layer2-old/redproducer.es", "utf-8").getLines.mkString("\n") - val redemptionProducerErgoTree = compile(redemptionProducerContract) - val redemptionProducerAddress = getAddressFromErgoTree(redemptionProducerErgoTree) + // Basis v2 is the only contract generation exposed by production tooling. + // Historical v1 sources live under src/test/resources and cannot be loaded + // through this object. + val basisV2Contract = readActiveContract("offchain/basis-v2.es") + val basisV2ErgoTree = compile(basisV2Contract) + val basisV2Address = getAddressFromErgoTree(basisV2ErgoTree) + + val basisTokenV2Contract = readActiveContract("offchain/basis-token-v2.es") + val basisTokenV2ErgoTree = compile(basisTokenV2Contract) + val basisTokenV2Address = getAddressFromErgoTree(basisTokenV2ErgoTree) + + /** Contracts intentionally exposed by production address tooling. */ + val publishedContracts = Vector( + "Basis v2" -> basisV2Address, + "Basis-token v2" -> basisTokenV2Address + ) } object Printer extends App { - println("Basis p2s address: " + Constants.basisAddress) - println("Basis-token p2s address: " + Constants.basisTokenAddress) - println("Redemption p2s address: " + Constants.redemptionAddress) - println("Redemption producer p2s address: " + Constants.redemptionProducerAddress) - - // Example deployment info - println("\nTo deploy Basis reserve:") - println("1. Run BasisDeployer.main() for deployment requests") - println("2. Use createBasisDeploymentRequest() with actual values") + Constants.publishedContracts.foreach { case (name, address) => + println(s"$name p2s address: $address") + } } diff --git a/src/main/scala/chaincash/contracts/ContractsPrinter.scala b/src/main/scala/chaincash/contracts/ContractsPrinter.scala deleted file mode 100644 index 894d444..0000000 --- a/src/main/scala/chaincash/contracts/ContractsPrinter.scala +++ /dev/null @@ -1,51 +0,0 @@ -package chaincash.contracts - -import Constants._ -import scorex.util.encode.Base16 -import sigma.ast.ByteArrayConstant -import sigma.serialization.ValueSerializer - -object ContractsPrinter extends App { - - println(s"Note contract address: $noteAddress") - - println(s"Receipt contract address: $receiptAddress") - - println(s"Reserve contract address: $reserveAddress") - - - val noteScriptBa = ByteArrayConstant(noteErgoTree.bytes) - val reserveScriptBa = ByteArrayConstant(reserveErgoTree.bytes) - - - val noteContractTrackingRule = s""" - |{ - | "scanName": "Note tracker", - | "walletInteraction": "off", - | "removeOffchain": false, - | "trackingRule": { - | "predicate": "equals", - | "value": "${Base16.encode(ValueSerializer.serialize(noteScriptBa))}" - | } - |} - """.stripMargin - - val reserveContractTrackingRule = s""" - | - |{ - | "scanName": "Reserve tracker", - | "walletInteraction": "off", - | "removeOffchain": false, - | "trackingRule": { - | "predicate": "equals", - | "value": "${Base16.encode(ValueSerializer.serialize(reserveScriptBa))}" - | } - |} - |""".stripMargin - - println("==========Note tracking rule================") - println(noteContractTrackingRule) - println("==========Reserve tracking rule==============") - println(reserveContractTrackingRule) - -} diff --git a/src/main/scala/chaincash/contracts/README.md b/src/main/scala/chaincash/contracts/README.md index 12369b6..2b68454 100644 --- a/src/main/scala/chaincash/contracts/README.md +++ b/src/main/scala/chaincash/contracts/README.md @@ -1,100 +1,31 @@ -# ChainCash Contracts +# ChainCash contract tooling -This directory contains utilities for deploying and managing ChainCash contracts on the Ergo blockchain. +Production tooling exposes only the reviewed Basis v2 candidate sources: -## Contracts +- `contracts/offchain/basis-v2.es` +- `contracts/offchain/basis-token-v2.es` +- `contracts/offchain/basis-v2.p2s` +- `contracts/offchain/basis-token-v2.p2s` -### Basis Reserve Contract - -The Basis reserve contract is an on-chain reserve that backs off-chain payments, allowing for: -- Off-chain payments with no need to create anything on-chain first -- Credit creation capabilities -- Redemption with tracker signature -- Emergency redemption after 3 days - -**Compiled Contract Address (P2S):** -``` -4ZhBzJfNoUL9Bp993NzJcdUr6CNfuwvwNMgHC2JPHs8ane1jjE3K7gzUQVBNQfJccoLbB2P8xMsa9qZNFgRwgrWs6WGEa38gwF1BDkGwMLh6RJUez5Ge6toZzu7tZo5qYtqUinmckb5q9hcVo6Cpn3w2gcuwCd2sKmRohedxxbpP7vnrQmCNQveB22RN5ZVv8VGJaDUEC3ADCSRjzr5ZzJNBmVbAw2k5sTmoXGm7qJ1YT9gzmAPi97ptJJQXqNJoi1W6coMFwg34Dc21K9TMkKQexnXxon21XrbyWL6fzLGbYBRBiVpiRTeMah9Tc33yN93NVTjHWKvBcxSYiJU7eJy6aiwAHhqxYPtZNhwE196qUEYHX5gnN1xB4CpZA2W2HDuEZREpDPV4xy6g2qucW2fyhgDpscHMxrbaGfRq1zkrvML54z2Da9jpkM6nmZx2KB29HTh1do6L3rrLxnvg5cgANzfYuaWPFEoo6j2ZqjPzLDeSSVhPbkMnw6HhQp2qtzayqWVgCKGRzMFuh8BkpmkFCPKjhUwX6Dgv6DpkuHbJRM7k9YSvPCHRQTSeDJa4B5wuyXMsfFMkAnjR4oaLbSBU2QCgKBLFbGvrRKgAJG9eTSc31x6EtqKFoLN2urEWGsEh1F6cxDh2Ma3izwFLyHAgCcUurRXndm5gy3U4GpKdaJiWtwfhcZspwtJ72gWUBEzuPdcqjEyBc95jVtubHeN95QcZLJkJM88c6m1DPXaTBSfDpL8s3sBySa7 -``` - -### Basis-Token Contract (Token-Based Reserve) - -A variant of the Basis contract that uses custom tokens instead of ERG for reserve backing. - -**Compiled Contract Address (P2S):** -``` -FjrqPyLvFUvPnjM8NgASrt3uZjegdVvpsjxrF5VL7nCgZAYLE4FJbFrJGgZYFHwyoAF1epbTynQhyHjuku4TotT15f6NBFWEWq8NeMgWopBxMkYmrA4X2WiFifCgBWm4nrmKiRyowCn4TbDVu6B5XEEDtB35jpkQyrdQ8jFhJDZsyLT59JaMPvLn931BA7dUdYjK8w8LocNxm8EUU5cm2Q8z7d7Z142pRhbnjEH98yPRkSg8We9ejqzQZpkTpk62uTbGtanQueKwyieN5QTdY1R6C6mBsjHN18rThDfrTqohfY33EzNgjiqNpsuw63MBHjmmh3eQnqRQe8yuDvAn1WvAb8gJujwnLThziucBounzgtEP4Bso7eToR2uZqi9RGgYCDDWL1XigS9kMkxpgRcszByz7WtXXV8jrth7PbqAQ8oRhrQCwH7rsWwj3qARz1x1acyBkbXEvJDAqZhsqUupekcv6aQtMro9WwXSvGfJuXuw7HGHwxGcg7yKQrX1J9g1EoL4F48cZarhMXHwEjgJm6GPE7gReR4THfuRtLLkSLiFbBEPFsjjAUQTKqwMPGGfPZyRT8smxJbaNeiziXnNJnttT4wZv4srEfnWAp3rbwWQ59grKpFsYtTksHRdMYzv2gpexqZD62ggymqP5u1iCpTBNvBGAnXLcY42C3rcoKhEsbrFTVjjGmBL1haDhwz3pidJF5rsEgEjqNw7r6frGbefDBWJzfSZdwSkn22ZZBMTeUbd96XEVJ4Qvx61kAwxT116AH9xW3CVSKAh5Hm5Dkw3oRBD6dYFrhjLjxvEynD6NpgwfAFYDQdCt7FPoM24pxRYsc3x9u1fTeF4DKC1sVrvviZ9pdjGtZDCSQpNk -``` - -#### Deployment - -Use `BasisDeployer` utility to deploy Basis reserve contracts: - -```scala -import chaincash.contracts.BasisDeployer -import sigmastate.Values.GroupElementConstant -import sigmastate.eval.CGroupElement -import sigmastate.basics.CryptoConstants - -// Example deployment -val ownerKey = GroupElementConstant(CGroupElement(CryptoConstants.dlogGroup.generator)) -val trackerNftId = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" -val reserveTokenId = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" - -val deploymentRequest = BasisDeployer.createBasisDeploymentRequest( - ownerKey, - trackerNftId, - reserveTokenId, - initialCollateral = 1000000000L // 1 ERG -) - -println(deploymentRequest) -``` - -#### Contract Features - -- **Redemption Action (0)**: Redeem off-chain notes with tracker signature -- **Top-up Action (1)**: Add more collateral to the reserve -- **Emergency Redemption**: Redeem without tracker signature after 7 days -- **Double Spending Prevention**: AVL tree tracks redeemed timestamps - -#### Required Parameters - -1. **Owner Public Key**: GroupElement representing reserve owner -2. **Tracker NFT ID**: NFT identifying the tracker service -3. **Reserve Token ID**: Singleton NFT identifying the reserve -4. **Initial Collateral**: Minimum 1 ERG (1000000000 nanoERG) - -### Core ChainCash Contracts - -- **Reserve Contract**: On-chain collateral management -- **Note Contract**: Digital currency issuance and transfer -- **Receipt Contract**: Redemption receipt management - -## Testing - -Run tests to verify contract compilation and deployment: +`Constants.publishedContracts` is the sole address-printer allowlist. +`Printer` emits only those two v2 addresses, while `BasisV2ReceiptPrinter` +emits deterministic source and full-ErgoTree material for receipt comparison. ```bash -sbt test +sbt 'runMain chaincash.contracts.Printer' +sbt 'runMain chaincash.contracts.BasisV2ReceiptPrinter' ``` -## Deployment Process - -1. **Compile Contracts**: Use `Constants` object to compile contracts -2. **Generate Addresses**: Get pay-to-script addresses for each contract -3. **Create Deployment Requests**: Use deployment utilities -4. **Submit Transactions**: Send deployment transactions to Ergo blockchain -5. **Monitor**: Use scan requests to monitor contract states - -## Network Configuration - -- **Mainnet**: NetworkType.MAINNET -- **Testnet**: NetworkType.TESTNET (modify Constants.scala) +There is intentionally no production deployment, note-construction, +redemption, signing, submission or broadcast helper in this repository. The +commands above inspect candidate bytes; they do not create a deployable +transaction or establish node, wallet, mempool, deployment or production +readiness. -## Security Notes +Basis v1 and the original ChainCash on-chain/Layer-2 prototypes are test-only +historical fixtures. See `docs/legacy-contract-retirement.md` for their exact +source identities and recovery boundary. -- Always test on testnet before mainnet deployment -- Verify contract addresses before deployment -- Use proper key management for reserve owners -- Monitor tracker services for availability \ No newline at end of file +Run `sbt verifyMainJarRetirement` to inspect the actual production JAR for +retired classes and resources. Run the Scala suites for source-to-ErgoTree and +transition regression evidence. diff --git a/src/main/scala/chaincash/offchain/NoteUtils.scala b/src/main/scala/chaincash/offchain/NoteUtils.scala deleted file mode 100644 index d6cf611..0000000 --- a/src/main/scala/chaincash/offchain/NoteUtils.scala +++ /dev/null @@ -1,133 +0,0 @@ -package chaincash.offchain - -import SigUtils._ -import chaincash.contracts.Constants -import chaincash.contracts.Constants.{noteErgoTree, reserveErgoTree} -import chaincash.offchain.TrackingTypes.NoteData -import com.google.common.primitives.Longs -import io.circe.syntax.EncoderOps -import org.ergoplatform.ErgoBox.{R4, R5, R6} -import org.ergoplatform.sdk.wallet.Constants.eip3DerivationPath -import org.ergoplatform.sdk.wallet.secrets.ExtendedSecretKey -import org.ergoplatform.sdk.wallet.settings.EncryptionSettings -import org.ergoplatform.sdk.SecretString -import org.ergoplatform.wallet.secrets.JsonSecretStorage -import org.ergoplatform.wallet.settings.SecretStorageSettings -import org.ergoplatform.{DataInput, ErgoBoxCandidate, P2PKAddress, UnsignedErgoLikeTransaction, UnsignedInput} -import scorex.crypto.hash.Digest32 -import scorex.util.encode.Base16 -import sigma.ast.{AvlTreeConstant, ByteArrayConstant, ByteConstant, GroupElementConstant, LongConstant} -import sigma.Colls -import sigma.interpreter.ContextExtension -import sigma.serialization.GroupElementSerializer -import sigma.GroupElement -import sigma.data.Digest32Coll - -trait NoteUtils extends WalletUtils { - // create note with nominal of `amountMg` mg of gold - def createNote(amountMg: Long, ownerPubkey: GroupElement, changeAddress: P2PKAddress): Unit = { - val inputs = fetchInputs().take(60) - val creationHeight = inputs.map(_.creationHeight).max - val noteTokenId = Digest32 @@ inputs.head.id.toArray - - val inputValue = inputs.map(_.value).sum - require(inputValue >= feeValue * 21) - - val noteAmount = feeValue * 20 - - val noteOut = new ErgoBoxCandidate( - noteAmount, - noteErgoTree, - creationHeight, - Colls.fromItems((Digest32Coll @@ Colls.fromArray(noteTokenId)) -> amountMg), - Map( - R4 -> AvlTreeConstant(Constants.emptyTree), - R5 -> GroupElementConstant(ownerPubkey), - R6 -> LongConstant(0) - ) - ) - val feeOut = createFeeOut(creationHeight) - val changeOutOpt = if(inputValue > 21 * feeValue) { - val changeValue = inputValue - (21 * feeValue) - Some(new ErgoBoxCandidate(changeValue, changeAddress.script, creationHeight)) - } else { - None - } - - val unsignedInputs = inputs.map(box => new UnsignedInput(box.id, ContextExtension.empty)) - val outs = Seq(noteOut, feeOut) ++ changeOutOpt.toSeq - val tx = new UnsignedErgoLikeTransaction(unsignedInputs.toIndexedSeq, IndexedSeq.empty, outs.toIndexedSeq) - println(tx.asJson) - } - - def createNote(amountMg: Long): Unit = { - val changeAddress = fetchChangeAddress() - createNote(amountMg, changeAddress.pubkey.value, changeAddress) - } - - private def readSecret(): ExtendedSecretKey ={ - val sss = SecretStorageSettings("secrets", EncryptionSettings("HmacSHA256", 128000, 256)) - val jss = JsonSecretStorage.readFile(sss).get - jss.unlock(SecretString.create("wpass")) - val masterKey = jss.secret.get - masterKey.derive(eip3DerivationPath) - } - - def sendNote(noteData: NoteData, to: GroupElement) = { - val changeAddress = fetchChangeAddress() - - val noteInputBox = noteData.currentUtxo - val p2pkInputs = fetchInputs().take(5) // to pay fees - val inputs = Seq(noteInputBox) ++ p2pkInputs - val creationHeight = inputs.map(_.creationHeight).max - - val inputValue = inputs.map(_.value).sum - - val noteRecord = noteInputBox.additionalTokens.toArray.head - val noteTokenId = noteRecord._1 - val noteAmount = noteRecord._2 - - val secret = readSecret() - val msg: Array[Byte] = Longs.toByteArray(noteAmount) ++ noteTokenId.toArray - val sig = SigUtils.sign(msg, secret.privateInput.w) - secret.zeroSecret() - val sigBytes = GroupElementSerializer.toBytes(sig._1) ++ sig._2.toByteArray - - // todo: likely should be passed from outside - val reserveId = myReserveIds().head - val reserveIdBytes = Base16.decode(reserveId).get - val reserveBox = DbEntities.reserves.get(reserveId).get.reserveBox - - val prover = noteData.restoreProver - val insertProof = prover.insert(reserveIdBytes -> sigBytes).proof - val updTree = prover.ergoValue.getValue - - val noteOut = new ErgoBoxCandidate( - noteInputBox.value, - noteErgoTree, - creationHeight, - Colls.fromItems(noteTokenId -> noteAmount), - Map(R4 -> AvlTreeConstant(updTree), R5 -> GroupElementConstant(to)) - ) - - val noteInput = new UnsignedInput(noteInputBox.id, ContextExtension(Map( - 0.toByte -> ByteConstant(0), - 1.toByte -> GroupElementConstant(sig._1), - 2.toByte -> ByteArrayConstant(sig._2.toByteArray), - 3.toByte -> ByteArrayConstant(insertProof.bytes) - ))) - - val feeOut = createFeeOut(creationHeight) - val changeValue = inputValue - noteOut.value - feeOut.value - val changeOut = new ErgoBoxCandidate(changeValue, changeAddress.script, creationHeight) - val outs = IndexedSeq(noteOut, changeOut, feeOut) - - val unsignedInputs = Seq(noteInput) ++ p2pkInputs.map(box => new UnsignedInput(box.id, ContextExtension.empty)) - - val dataInputs = IndexedSeq(DataInput(reserveBox.id)) - - val tx = new UnsignedErgoLikeTransaction(unsignedInputs.toIndexedSeq, dataInputs, outs.toIndexedSeq) - println(tx.asJson) - } - -} diff --git a/src/main/scala/chaincash/offchain/ReserveUtils.scala b/src/main/scala/chaincash/offchain/ReserveUtils.scala deleted file mode 100644 index 3848478..0000000 --- a/src/main/scala/chaincash/offchain/ReserveUtils.scala +++ /dev/null @@ -1,57 +0,0 @@ -package chaincash.offchain - -import SigUtils._ -import io.circe.syntax.EncoderOps -import org.ergoplatform.ErgoBox.R4 -import org.ergoplatform.sdk.JsonCodecs -import org.ergoplatform.{ErgoBoxCandidate, P2PKAddress, UnsignedErgoLikeTransaction, UnsignedInput} -import scorex.crypto.hash.Digest32 -import sigma.ast.GroupElementConstant -import sigma.Colls -import sigma.GroupElement -import sigma.interpreter.ContextExtension -import sigma.data.Digest32Coll - -trait ReserveUtils extends WalletUtils with JsonCodecs { - import chaincash.contracts.Constants.reserveErgoTree - - // create reserve with `amount` nanoerg associated with `pubkey` - def createReserve(pubKey: GroupElement, amount: Long, changeAddress: P2PKAddress): Unit = { - val inputs = fetchInputs().take(60) - val creationHeight = inputs.map(_.creationHeight).max - val reserveInputNft = Digest32 @@ inputs.head.id.toArray - - val inputValue = inputs.map(_.value).sum - require(inputValue >= amount + feeValue) - - val reserveOut = new ErgoBoxCandidate( - amount, - reserveErgoTree, - creationHeight, - Colls.fromItems((Digest32Coll @@ Colls.fromArray(reserveInputNft)) -> 1L), - Map(R4 -> GroupElementConstant(pubKey)) - ) - val feeOut = createFeeOut(creationHeight) - val changeOutOpt = if(inputValue > amount + feeValue) { - val changeValue = inputValue - (amount + feeValue) - Some(new ErgoBoxCandidate(changeValue, changeAddress.script, creationHeight)) - } else { - None - } - - val unsignedInputs = inputs.map(box => new UnsignedInput(box.id, ContextExtension.empty)) - val outs = Seq(reserveOut, feeOut) ++ changeOutOpt.toSeq - val tx = new UnsignedErgoLikeTransaction(unsignedInputs.toIndexedSeq, IndexedSeq.empty, outs.toIndexedSeq) - println(tx.asJson) - } - - def createReserve(address: P2PKAddress, amount: Long): Unit = { - createReserve(address.pubkey.value, amount, address) - } - - def createReserve(amount: Long): Unit = { - val changeAddress = fetchChangeAddress() - createReserve(changeAddress, amount) - } - -} diff --git a/src/main/scala/chaincash/offchain/Tester.scala b/src/main/scala/chaincash/offchain/Tester.scala deleted file mode 100644 index 78562e0..0000000 --- a/src/main/scala/chaincash/offchain/Tester.scala +++ /dev/null @@ -1,16 +0,0 @@ -package chaincash.offchain - -import SigUtils._ - -object Tester extends App with TrackingUtils with NoteUtils { - override val serverUrl: String = "http://127.0.0.1:9053" - - println(fetchNodeHeight()) - - processBlocks() - - println("my balance: " + myBalance()) - - sendNote(DbEntities.unspentNotes.head.get._2, myPoint) - -} \ No newline at end of file diff --git a/src/test/resources/contracts/historical/README.md b/src/test/resources/contracts/historical/README.md new file mode 100644 index 0000000..f4b5a99 --- /dev/null +++ b/src/test/resources/contracts/historical/README.md @@ -0,0 +1,29 @@ +# Historical ChainCash and Basis v1 fixtures + +These ErgoScript files are retained only to replay the original ChainCash and +Basis v1 tests. None of these files are production resources, supported +execution or deployment targets, or address-generation inputs. The former +Basis v1 demos are not part of this fixture set; they remain recoverable from +Git history at the pinned commit below. + +The files were moved without source changes from commit +`78475e30362571acf56e4e38276a9d6c0a84ce0c`. Their canonical UTF-8 LF +SHA-256 digests are shown below. CRLF is normalized to LF before hashing, while +the final newline is retained. + +| Fixture | SHA-256 | +| --- | --- | +| `onchain/reserve.es` | `3f39c13879748230cd08ee10d9970370506509e966229b735af77486843fa335` | +| `onchain/receipt.es` | `69c24be8426e7c7c8fdf77ab248e4549b8cc9832122aed010a54e304357869ea` | +| `onchain/note.es` | `5a145dfa334efa60a26fe25ed136611992f207807ecb628c4c8ec196d940df16` | +| `layer2-old/reserve.es` | `026c29b169f39f75a262590388364aea926a270c6fa78511199c1ae1512336e4` | +| `layer2-old/redemption.es` | `b87614ec6aacbed34a0bf7950629d505cc6f204d1b4e7db52c5b43a133e122c6` | +| `layer2-old/redproducer.es` | `9e9842716a49d631bc67ecf201da42c020d441e3329b9fd7778ea14c9dcc8351` | +| `layer2-old/note.es` | `797b69c823ff392d5a941cbd02b9297341c5926c2ce721ab6735f71f3c181e1b` | +| `offchain/basis.es` | `56f6d229207469a45764c2a9657c5b37d6c5faf9025a9d3fc0636f1dd6120823` | +| `offchain/basis-token.es` | `f3d8d4b5e8677409b82c49a396b5c68487d1a043963cee25558b368aa225961d` | + +`HistoricalContractFixtures` verifies these canonical digests before exposing +the source to historical tests. Changing a fixture requires an explicit new +lineage and review; it must not silently become a production contract +generation. diff --git a/contracts/layer2-old/note.es b/src/test/resources/contracts/historical/layer2-old/note.es similarity index 100% rename from contracts/layer2-old/note.es rename to src/test/resources/contracts/historical/layer2-old/note.es diff --git a/contracts/layer2-old/redemption.es b/src/test/resources/contracts/historical/layer2-old/redemption.es similarity index 100% rename from contracts/layer2-old/redemption.es rename to src/test/resources/contracts/historical/layer2-old/redemption.es diff --git a/contracts/layer2-old/redproducer.es b/src/test/resources/contracts/historical/layer2-old/redproducer.es similarity index 100% rename from contracts/layer2-old/redproducer.es rename to src/test/resources/contracts/historical/layer2-old/redproducer.es diff --git a/contracts/layer2-old/reserve.es b/src/test/resources/contracts/historical/layer2-old/reserve.es similarity index 100% rename from contracts/layer2-old/reserve.es rename to src/test/resources/contracts/historical/layer2-old/reserve.es diff --git a/contracts/offchain/basis-token.es b/src/test/resources/contracts/historical/offchain/basis-token.es similarity index 100% rename from contracts/offchain/basis-token.es rename to src/test/resources/contracts/historical/offchain/basis-token.es diff --git a/contracts/offchain/basis.es b/src/test/resources/contracts/historical/offchain/basis.es similarity index 100% rename from contracts/offchain/basis.es rename to src/test/resources/contracts/historical/offchain/basis.es diff --git a/contracts/offchain/basis.md b/src/test/resources/contracts/historical/offchain/basis.md similarity index 100% rename from contracts/offchain/basis.md rename to src/test/resources/contracts/historical/offchain/basis.md diff --git a/contracts/offchain/tracker.md b/src/test/resources/contracts/historical/offchain/tracker.md similarity index 100% rename from contracts/offchain/tracker.md rename to src/test/resources/contracts/historical/offchain/tracker.md diff --git a/contracts/onchain/note.es b/src/test/resources/contracts/historical/onchain/note.es similarity index 100% rename from contracts/onchain/note.es rename to src/test/resources/contracts/historical/onchain/note.es diff --git a/contracts/onchain/receipt.es b/src/test/resources/contracts/historical/onchain/receipt.es similarity index 100% rename from contracts/onchain/receipt.es rename to src/test/resources/contracts/historical/onchain/receipt.es diff --git a/contracts/onchain/reserve.es b/src/test/resources/contracts/historical/onchain/reserve.es similarity index 100% rename from contracts/onchain/reserve.es rename to src/test/resources/contracts/historical/onchain/reserve.es diff --git a/src/test/scala/chaincash/BasisSpec.scala b/src/test/scala/chaincash/BasisSpec.scala index 3b845b0..46da39b 100644 --- a/src/test/scala/chaincash/BasisSpec.scala +++ b/src/test/scala/chaincash/BasisSpec.scala @@ -199,7 +199,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + totalDebt + feeValue) // Total reserve value .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), inputTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -228,7 +228,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output (updated reserve with new redeemed debt) val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + totalDebt - redeemAmount + feeValue, // Reduce value by the amount being redeemed Array(ErgoValue.of(ownerPk), outputTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -273,7 +273,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -292,7 +292,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output (with increased value) val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + topUpAmount, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -364,7 +364,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + totalDebt + feeValue) // Total reserve value .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), inputTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -395,7 +395,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + totalDebt - redeemAmount + feeValue, // Reduce value by the amount being redeemed Array(ErgoValue.of(ownerPk), outputTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -471,7 +471,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount - 100000000L + feeValue) // Less than debt amount .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -500,7 +500,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue - 100000000L, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -574,7 +574,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -603,7 +603,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -648,7 +648,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -667,7 +667,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output (with insufficient top-up) val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + topUpAmount, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -706,7 +706,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -726,7 +726,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output with R7 set to initiation height. Slightly future-dated height is // allowed (>= HEIGHT) to tolerate delayed inclusion into a block. val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong + 5)), Array(new ErgoToken(basisTokenId, 1)) @@ -756,7 +756,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -773,7 +773,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong)), Array(new ErgoToken(basisTokenId, 1)) @@ -805,7 +805,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(500L)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -822,7 +822,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong)), Array(new ErgoToken(basisTokenId, 1)) @@ -847,7 +847,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -865,7 +865,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Backdating R7 would shorten the creditor protection window - must be rejected val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong - 10)), Array(new ErgoToken(basisTokenId, 1)) @@ -890,7 +890,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -899,7 +899,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Output value decreased (fee is paid from the reserve) - not allowed at initiation val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue - feeValue, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong)), Array(new ErgoToken(basisTokenId, 1)) @@ -927,7 +927,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -966,7 +966,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1000,7 +1000,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1035,7 +1035,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1084,7 +1084,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val trackerDataInput = mkTrackerDataInput(trackerTree) // R7 preserved in the reserve output - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -1115,7 +1115,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val trackerDataInput = mkTrackerDataInput(trackerTree) // R7 dropped in the reserve output - pending refund must not be cancelled by redemption - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -1138,7 +1138,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1155,7 +1155,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + topUpAmount, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)), Array(new ErgoToken(basisTokenId, 1)) @@ -1217,7 +1217,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1246,7 +1246,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -1324,7 +1324,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(wrongTrackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1353,7 +1353,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(wrongTrackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -1427,7 +1427,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1456,7 +1456,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -1529,7 +1529,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1556,7 +1556,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -1636,7 +1636,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + debtAmount + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), initialTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1663,7 +1663,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Basis output val basisOutput = createOut( - Constants.basisContract, + HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1)) @@ -1696,25 +1696,18 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec } // ========== CONTRACT LINKAGE VERIFICATION ========== - // Verifies tests use the exact contract from contracts/offchain/basis.es - - property("Constants.basisContract matches contracts/offchain/basis.es (text-equivalent)") { - import java.nio.charset.StandardCharsets - import java.nio.file.{Files, Paths} - // Anchor to project root via user.dir (sbt sets this to project root) - val projectRoot = Paths.get(sys.props("user.dir")) - val contractPath = projectRoot.resolve("contracts/offchain/basis.es") - require(Files.exists(contractPath), s"Missing contract file: $contractPath (user.dir=${sys.props("user.dir")})") - val fileBytes = Files.readAllBytes(contractPath) - val fileText = new String(fileBytes, StandardCharsets.UTF_8) - // Constants.readContract normalizes to \n via getLines.mkString("\n") - Constants.basisContract shouldEqual fileText.replace("\r\n", "\n").stripSuffix("\n") + // Verifies historical tests are bound to the exact test-only v1 fixture. + + property("historical Basis v1 source fixture retains its canonical digest") { + HistoricalContractFixtures.basisContract should not be empty + HistoricalContractFixtures.expectedCanonicalLfSha256("offchain/basis.es") shouldEqual + "56f6d229207469a45764c2a9657c5b37d6c5faf9025a9d3fc0636f1dd6120823" } - property("compiled basisErgoTree matches fresh compilation of basis.es") { + property("historical Basis v1 tree matches a fresh fixture compilation") { createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx: BlockchainContext => - val freshlyCompiled = ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract) - Constants.basisErgoTree.bytes shouldEqual freshlyCompiled.getErgoTree.bytes + val freshlyCompiled = ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract) + HistoricalContractFixtures.basisErgoTree.bytes shouldEqual freshlyCompiled.getErgoTree.bytes } } @@ -1810,7 +1803,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(value) .tokens(new ErgoToken(basisTokenId, 1)) .registers(registers: _*) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(allVars: _*) @@ -1932,7 +1925,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec Array(badBasisOutput, redemptionOutput), Array(receiverSecret.toString())) // CONTROL: Use correct contract → tx succeeds - val goodBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val goodBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) noException should be thrownBy { @@ -1966,7 +1959,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) // FAIL CASE: basis output missing token (moved to redemption) - violates selfPreserved.tokens - val badBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val badBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array()) // basis token missing here -> contract should fail val redemptionOutputWithToken = createOut(trueScript, redeemAmount, @@ -1975,7 +1968,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec Array(badBasisOutput, redemptionOutputWithToken), Array(receiverSecret.toString())) // CONTROL: Include correct token → tx succeeds - val goodBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val goodBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) noException should be thrownBy { @@ -2011,14 +2004,14 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) // FAIL CASE: Output with DIFFERENT owner key in R4 - violates selfPreserved.R4 - val badBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val badBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(differentOwnerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) assertTxFails(Array(basisInput), Array(trackerDataInput), Array(badBasisOutput, redemptionOutput), Array(receiverSecret.toString())) // CONTROL: Fix owner key → tx succeeds - val goodBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val goodBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) noException should be thrownBy { @@ -2054,14 +2047,14 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) // FAIL CASE: Output with DIFFERENT tracker NFT ID in R6 - violates selfPreserved.R6 - val badBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val badBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(differentTrackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) assertTxFails(Array(basisInput), Array(trackerDataInput), Array(badBasisOutput, redemptionOutput), Array(receiverSecret.toString())) // CONTROL: Fix tracker NFT ID → tx succeeds - val goodBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val goodBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) noException should be thrownBy { @@ -2112,7 +2105,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + totalDebt + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), treeWithExistingKey, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -2125,7 +2118,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec new ContextVar(6, ErgoValue.of(trackerSigBytes)), new ContextVar(8, ErgoValue.of(trackerLookupProof)) ) - val badBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val badBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), treeAfterInsert, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) assertTxFails(Array(badBasisInput), Array(trackerDataInput), @@ -2136,7 +2129,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue + totalDebt + feeValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeEV, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -2149,7 +2142,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec new ContextVar(6, ErgoValue.of(trackerSigBytes)), new ContextVar(8, ErgoValue.of(trackerLookupProof)) ) - val goodBasisOutput = createOut(Constants.basisContract, minValue + feeValue, + val goodBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), treeAfterInsert, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) noException should be thrownBy { @@ -2174,7 +2167,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .value(minValue) .tokens(new ErgoToken(basisTokenId, 1)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(10: Byte))) @@ -2186,13 +2179,13 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec .convertToInputWith(fakeTxId2, fakeIndex) // FAIL CASE: Output with DIFFERENT tree - violates top-up tree preservation - val badBasisOutput = createOut(Constants.basisContract, minValue + topUpAmount, + val badBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + topUpAmount, Array(ErgoValue.of(ownerPk), differentTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) assertTxFails(Array(basisInput, fundingBox), Array(), Array(badBasisOutput), Array(ownerSecret.toString())) // CONTROL: Use same tree → tx succeeds - val goodBasisOutput = createOut(Constants.basisContract, minValue + topUpAmount, + val goodBasisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + topUpAmount, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) noException should be thrownBy { @@ -2229,7 +2222,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val basisInput = mkBasisInput(minValue + totalDebt + feeValue, initialTree, receiverPk, reserveSigBytes, totalDebt, proofBytes, trackerSigBytes, None, Some(trackerLookupProof), timestamp) val trackerDataInput = mkTrackerDataInput(trackerTree) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2269,7 +2262,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val basisInput = mkBasisInput(minValue + totalDebt + feeValue, initialTree, receiverPk, reserveSigBytes, totalDebt, proofBytes, invalidTrackerSigBytes, None, None, timestamp) val trackerDataInput = mkTrackerDataInput(trackerTree) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2304,7 +2297,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec val basisInput = mkBasisInput(minValue + totalDebt + feeValue, initialTree, receiverPk, reserveSigBytes, totalDebt, proofBytes, invalidTrackerSigBytes, None, None, timestamp) val trackerDataInput = mkTrackerDataInput(trackerTree) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2347,7 +2340,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec receiverPk, reserveSigBytes, totalDebt, proofBytes, trackerSigBytes, None, Some(trackerLookupProof), timestamp) // Tracker is more than 3 days old (3 * 720 = 2160 blocks) val trackerDataInput = mkTrackerDataInput(trackerTree, Some(3 * 720 + 1)) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2380,7 +2373,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec receiverPk, reserveSigBytes, totalDebt, proofBytes, trackerSigBytes, None, Some(trackerLookupProof), timestamp) // Tracker is more than 3 days old val trackerDataInput = mkTrackerDataInput(trackerTree, Some(3 * 720 + 1)) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2413,7 +2406,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec receiverPk, reserveSigBytes, totalDebt, proofBytes, invalidTrackerSigBytes, None, Some(trackerLookupProof), timestamp) // Tracker is more than 3 days old (but invalid signature should still fail) val trackerDataInput = mkTrackerDataInput(trackerTree, Some(3 * 720 + 1)) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2466,7 +2459,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // After emergency period, redemption should be possible without tracker signature val trackerDataInput = mkTrackerDataInput(trackerTree, Some(3 * 720 + 1)) - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2506,7 +2499,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec // Tracker is NOT old enough (only 1 day old, need 3 days) val trackerDataInput = mkTrackerDataInput(trackerTree, Some(1 * 720)) // Only 1 day - val basisOutput = createOut(Constants.basisContract, minValue + feeValue, + val basisOutput = createOut(HistoricalContractFixtures.basisContract, minValue + feeValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionOutput = createOut(trueScript, redeemAmount, Array(), Array()) @@ -2583,7 +2576,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec ) val trackerDataInputAB = mkTrackerDataInput(trackerTreeAB.tree) val redemptionOutputBob = createOut(trueScript, remainingDebtToBob, Array(), Array()) - val basisOutputBob = createOut(Constants.basisContract, + val basisOutputBob = createOut(HistoricalContractFixtures.basisContract, reserveValueForBob - remainingDebtToBob, Array(ErgoValue.of(aliceKey), nextTreeBob, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) @@ -2608,7 +2601,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec ) val trackerDataInputAC = mkTrackerDataInput(trackerTreeAC.tree) val redemptionOutputCarol = createOut(trueScript, transferAmount, Array(), Array()) - val basisOutputCarol = createOut(Constants.basisContract, + val basisOutputCarol = createOut(HistoricalContractFixtures.basisContract, reserveValueForCarol - transferAmount, Array(ErgoValue.of(aliceKey), nextTreeCarol, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) @@ -2730,7 +2723,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec trackerSigBobBytes, None, Some(trackerTreeBob.lookupProofBytes), timestamp ) val trackerInputBob = mkTrackerDataInput(trackerTreeBob.tree) - val basisOutputBob = createOut(Constants.basisContract, reserveBob - debtToBob, + val basisOutputBob = createOut(HistoricalContractFixtures.basisContract, reserveBob - debtToBob, Array(ErgoValue.of(aliceKey), treeBobOut, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionBob = createOut(trueScript, debtToBob, Array(), Array()) @@ -2848,7 +2841,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec trackerSigBobBytes, None, Some(trackerTreeBob.lookupProofBytes), timestamp1 ) val trackerInputBob = mkTrackerDataInput(trackerTreeBob.tree) - val basisOutputBob = createOut(Constants.basisContract, reserveBob - bobRemaining, + val basisOutputBob = createOut(HistoricalContractFixtures.basisContract, reserveBob - bobRemaining, Array(ErgoValue.of(aliceKey), treeBobOut, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionBob = createOut(trueScript, bobRemaining, Array(), Array()) @@ -2868,7 +2861,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec reserveBob2, treeBobReplay, bobKey, reserveSigBobBytes, bobRemaining, proofReplay, trackerSigBobBytes, Some(proofBob), Some(trackerTreeBob.lookupProofBytes), timestamp1 ) - val basisOutputReplay = createOut(Constants.basisContract, reserveBob2, + val basisOutputReplay = createOut(HistoricalContractFixtures.basisContract, reserveBob2, Array(ErgoValue.of(aliceKey), treeBobReplayOut, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionReplay = createOut(trueScript, bobRemaining, Array(), Array()) @@ -2923,7 +2916,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec ) val trackerInputBob = mkTrackerDataInput(trackerTreeBob.tree) // Try to output more than reserve has - val basisOutputBob = createOut(Constants.basisContract, insufficientReserve - bobRemaining, // Would be negative! + val basisOutputBob = createOut(HistoricalContractFixtures.basisContract, insufficientReserve - bobRemaining, // Would be negative! Array(ErgoValue.of(aliceKey), treeBobOut, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) val redemptionBob = createOut(trueScript, bobRemaining, Array(), Array()) @@ -2966,7 +2959,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec emptyTree, receiverPk, reserveSig1, totalDebt, proof1, trackerSig1, None, Some(tracker.lookupProofBytes), t1 ) - val basisOutput1 = createOut(Constants.basisContract, + val basisOutput1 = createOut(HistoricalContractFixtures.basisContract, minValue + totalDebt - redeem1 + feeValue, Array(ErgoValue.of(ownerPk), tree1, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) @@ -3002,7 +2995,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec new ContextVar(8, ErgoValue.of(tracker.lookupProofBytes)) ) - val basisOutput2 = createOut(Constants.basisContract, + val basisOutput2 = createOut(HistoricalContractFixtures.basisContract, minValue + totalDebt - redeem1 - redeem2 + feeValue, Array(ErgoValue.of(ownerPk), tree2, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) @@ -3040,7 +3033,7 @@ class BasisSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChec new ContextVar(8, ErgoValue.of(tracker.lookupProofBytes)) ) - val basisOutput3 = createOut(Constants.basisContract, + val basisOutput3 = createOut(HistoricalContractFixtures.basisContract, minValue + totalDebt - redeem1 - redeem2 - redeem3 + feeValue, Array(ErgoValue.of(ownerPk), tree3, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(basisTokenId, 1))) diff --git a/src/test/scala/chaincash/BasisTokenSpec.scala b/src/test/scala/chaincash/BasisTokenSpec.scala index f85987d..ed0aaab 100644 --- a/src/test/scala/chaincash/BasisTokenSpec.scala +++ b/src/test/scala/chaincash/BasisTokenSpec.scala @@ -229,7 +229,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert new ErgoToken(reserveTokenIdBytes, reserveTokenAmount) ) .registers(registers: _*) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(allVars: _*) @@ -337,7 +337,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), outputTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeemAmount)) ) @@ -360,7 +360,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, initialReserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(10: Byte))) @@ -374,7 +374,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, initialReserveTokenAmount + topUpAmount)) ) @@ -400,7 +400,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(20: Byte))) // action 2, index 0 @@ -416,7 +416,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // Basis output with R7 set to initiation height. Slightly future-dated height is // allowed (>= HEIGHT) to tolerate delayed inclusion into a block. val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong + 5)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) ) @@ -437,7 +437,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(20: Byte))) // action 2, index 0 @@ -450,7 +450,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) ) @@ -473,7 +473,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(500L)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(20: Byte))) // action 2, index 0 @@ -486,7 +486,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) ) @@ -504,7 +504,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(20: Byte))) // action 2, index 0 @@ -518,7 +518,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // Backdating R7 would shorten the creditor protection window - must be rejected val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong - 10)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) ) @@ -537,7 +537,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(20: Byte))) // action 2, index 0 @@ -551,7 +551,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // Reserve token amount decreased - not allowed at initiation val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(ctx.getHeight.toLong)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - takenAmount)) ) @@ -574,7 +574,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(30: Byte))) // action 3, index 0 @@ -603,7 +603,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(30: Byte))) // action 3, index 0 @@ -627,7 +627,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(30: Byte))) // action 3, index 0 @@ -651,7 +651,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(30: Byte))) // action 3, index 0 @@ -699,7 +699,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // R7 preserved in the reserve output val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), outputTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeemAmount)) ) @@ -741,7 +741,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // R7 dropped in the reserve output - pending refund must not be cancelled by redemption val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), outputTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeemAmount)) ) @@ -762,7 +762,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, initialReserveTokenAmount)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(10: Byte))) // action 1, index 0 @@ -776,7 +776,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisTokenContract, minValue * 2, + HistoricalContractFixtures.basisTokenContract, minValue * 2, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes), ErgoValue.of(refundHeight)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, initialReserveTokenAmount + topUpAmount)) ) @@ -815,7 +815,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput() val basisOutput = createOut( - Constants.basisTokenContract, minValue + feeValue - 100000000L, + HistoricalContractFixtures.basisTokenContract, minValue + feeValue - 100000000L, Array(ErgoValue.of(ownerPk), nextTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -837,7 +837,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 1000000000L)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars(new ContextVar(0, ErgoValue.of(10: Byte))) @@ -851,7 +851,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .convertToInputWith(fakeTxId2, fakeIndex) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 1000000000L)) ) @@ -886,7 +886,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -920,7 +920,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -954,7 +954,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue * 2 + feeValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, totalDebt)) .registers(ErgoValue.of(ownerPk), emptyTreeErgoValue, ErgoValue.of(wrongTrackerNFT)) // Wrong tracker NFT - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -971,7 +971,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(wrongTrackerNFT)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -1013,7 +1013,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -1060,7 +1060,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -1106,7 +1106,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val trackerDataInput = mkTrackerDataInput(trackerTree) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, 0L)) ) @@ -1146,7 +1146,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert val redemptionOutput = createOut(trueScript, minValue, Array(), Array(new ErgoToken(reserveTokenIdBytes, redeemAmount))) val basisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeemAmount)) ) @@ -1179,7 +1179,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert .value(minValue * 2 + feeValue) .tokens(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount)) .registers(ErgoValue.of(ownerPk), initialTree, ErgoValue.of(trackerNFTBytes)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -1198,7 +1198,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // Swapped token positions val badBasisOutputSwapped = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeemAmount), new ErgoToken(reserveNFTBytes, 1)) ) @@ -1210,7 +1210,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // Control: correct positions should succeed val goodBasisOutput = createOut( - Constants.basisTokenContract, minValue, + HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTree, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeemAmount)) ) @@ -1224,21 +1224,16 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert // ========== CONTRACT LINKAGE VERIFICATION ========== - property("Constants.basisTokenContract matches contracts/offchain/basis-token.es (text-equivalent)") { - import java.nio.charset.StandardCharsets - import java.nio.file.{Files, Paths} - val projectRoot = Paths.get(sys.props("user.dir")) - val contractPath = projectRoot.resolve("contracts/offchain/basis-token.es") - require(Files.exists(contractPath), s"Missing contract file: $contractPath (user.dir=${sys.props("user.dir")})") - val fileBytes = Files.readAllBytes(contractPath) - val fileText = new String(fileBytes, StandardCharsets.UTF_8) - Constants.basisTokenContract shouldEqual fileText.replace("\r\n", "\n").stripSuffix("\n") + property("historical Basis-token v1 source fixture retains its canonical digest") { + HistoricalContractFixtures.basisTokenContract should not be empty + HistoricalContractFixtures.expectedCanonicalLfSha256("offchain/basis-token.es") shouldEqual + "f3d8d4b5e8677409b82c49a396b5c68487d1a043963cee25558b368aa225961d" } - property("compiled basisTokenErgoTree matches fresh compilation of basis-token.es") { + property("historical Basis-token v1 tree matches a fresh fixture compilation") { createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx: BlockchainContext => - val freshlyCompiled = ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenContract) - Constants.basisTokenErgoTree.bytes shouldEqual freshlyCompiled.getErgoTree.bytes + val freshlyCompiled = ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.basisTokenContract) + HistoricalContractFixtures.basisTokenErgoTree.bytes shouldEqual freshlyCompiled.getErgoTree.bytes } } @@ -1285,7 +1280,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert ) val trackerDataInputAB = mkTrackerDataInput(trackerTreeAB.tree) - val basisOutputBob = createOut(Constants.basisTokenContract, minValue, + val basisOutputBob = createOut(HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), nextTreeBob, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - remainingDebtToBob)) ) @@ -1434,7 +1429,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert minValue * 2, emptyTree, receiverPk, reserveSig1, totalDebt, proof1, trackerSig1, reserveTokenAmount, None, Some(tracker.lookupProofBytes), t1 ) - val basisOutput1 = createOut(Constants.basisTokenContract, minValue, + val basisOutput1 = createOut(HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), tree1, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeem1))) val redemption1 = createOut(trueScript, minValue, Array(), @@ -1476,7 +1471,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert new ContextVar(8, ErgoValue.of(tracker.lookupProofBytes)) ) - val basisOutput2 = createOut(Constants.basisTokenContract, minValue, + val basisOutput2 = createOut(HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), tree2, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeem1 - redeem2))) val redemption2 = createOut(trueScript, minValue, Array(), @@ -1520,7 +1515,7 @@ class BasisTokenSpec extends PropSpec with Matchers with ScalaCheckDrivenPropert new ContextVar(8, ErgoValue.of(tracker.lookupProofBytes)) ) - val basisOutput3 = createOut(Constants.basisTokenContract, minValue, + val basisOutput3 = createOut(HistoricalContractFixtures.basisTokenContract, minValue, Array(ErgoValue.of(ownerPk), tree3, ErgoValue.of(trackerNFTBytes)), Array(new ErgoToken(reserveNFTBytes, 1), new ErgoToken(reserveTokenIdBytes, reserveTokenAmount - redeem1 - redeem2 - redeem3))) val redemption3 = createOut(trueScript, minValue, Array(), diff --git a/src/test/scala/chaincash/BasisV2Spec.scala b/src/test/scala/chaincash/BasisV2Spec.scala new file mode 100644 index 0000000..52cf88c --- /dev/null +++ b/src/test/scala/chaincash/BasisV2Spec.scala @@ -0,0 +1,1422 @@ +package chaincash + +import chaincash.contracts.Constants +import chaincash.offchain.SigUtils +import chaincash.offchain.SigUtils._ +import com.google.common.primitives.Longs +import org.ergoplatform.P2PKAddress +import org.ergoplatform.appkit.impl.{ErgoTreeContract, OutBoxImpl} +import org.ergoplatform.appkit.{AppkitHelpers, BlockchainContext, ConstantsBuilder, ContextVar, ErgoValue, InputBox, NetworkType, OutBox, OutBoxBuilder, SignedTransaction} +import org.ergoplatform.sdk.ErgoToken +import org.scalatest.{Matchers, PropSpec} +import scorex.crypto.hash.Blake2b256 +import scorex.util.encode.Base16 +import sigma.ast.ErgoTree +import sigma.data.{AvlTreeData, AvlTreeFlags, CAvlTree, ProveDlog} +import sigma.serialization.GroupElementSerializer +import sigma.crypto.SecP256K1Group +import sigma.{AvlTree, GroupElement} +import work.lithos.plasma.PlasmaParameters +import work.lithos.plasma.collections.PlasmaMap + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Paths} +import java.security.MessageDigest +import java.util +import scala.annotation.tailrec +import scala.collection.JavaConverters._ + +class BasisV2Spec extends PropSpec with Matchers with HttpClientTesting { + + implicit val addressEncoder = Constants.ergoAddressEncoder + + private val ownerSecret = SigUtils.randBigInt + private val ownerPk = Constants.g.exp(ownerSecret.bigInteger) + private val receiverSecret = SigUtils.randBigInt + private val receiverPk = Constants.g.exp(receiverSecret.bigInteger) + private val otherSecret = SigUtils.randBigInt + private val otherPk = Constants.g.exp(otherSecret.bigInteger) + private val trackerSecret = SigUtils.randBigInt + private val trackerPk = Constants.g.exp(trackerSecret.bigInteger) + private val identityPk = Constants.g.exp(java.math.BigInteger.ZERO) + + private val reserveNftA = hex("4b2d8b7beb3eaac8234d9e61792d270898a43934d6a27275e4f3a044609c9f2a") + private val reserveNftB = hex("5b2d8b7beb3eaac8234d9e61792d270898a43934d6a27275e4f3a044609c9f2b") + private val tokenReserveNft = hex("6b2d8b7beb3eaac8234d9e61792d270898a43934d6a27275e4f3a044609c9f2c") + private val reserveTokenId = hex("7b2d8b7beb3eaac8234d9e61792d270898a43934d6a27275e4f3a044609c9f2d") + private val trackerNft = hex("3c45f29a5165b030fdb5eaf5d81f8108f9d8f507b31487dd51f4ae08fe07cf4a") + private val zeroId = Array.fill[Byte](32)(0) + + private val minValue = 1000000000L + private val feeValue = 1000000L + private val totalDebt = 1000000000L + private val timestamp = 1000000000000L + + private val fakeTxIds = Vector( + "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b801", + "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b802", + "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b803", + "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b804", + "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b805", + "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b806" + ) + private val fakeIndex = 1.toShort + + private val redeemedFlags = AvlTreeFlags(insertAllowed = true, updateAllowed = true, removeAllowed = false) + private val trackerFlags = AvlTreeFlags(insertAllowed = true, updateAllowed = true, removeAllowed = false) + private val redeemedParameters = PlasmaParameters(32, Some(24)) + private val trackerParameters = PlasmaParameters(32, Some(8)) + + private val ergDomain = hex("4241534953020000") + private val tokenDomain = hex("4241534953020001") + + private def hex(value: String): Array[Byte] = Base16.decode(value).get + + private def claimKey( + domain: Array[Byte], + reserveNft: Array[Byte], + assetId: Option[Array[Byte]], + receiver: GroupElement = receiverPk, + owner: GroupElement = ownerPk + ): Array[Byte] = + Blake2b256( + domain ++ reserveNft ++ assetId.getOrElse(Array.emptyByteArray) ++ + trackerNft ++ owner.getEncoded.toArray ++ receiver.getEncoded.toArray + ) + + private def message(key: Array[Byte], debt: Long = totalDebt, ts: Long = timestamp): Array[Byte] = + key ++ Longs.toByteArray(debt) ++ Longs.toByteArray(ts) + + @tailrec + private def signatureBytes(messageBytes: Array[Byte], secret: BigInt): Array[Byte] = { + val signature = SigUtils.sign(messageBytes, secret) + val zBytes = signature._2.toByteArray + if (zBytes.length == 32) + GroupElementSerializer.toBytes(signature._1) ++ zBytes + else + signatureBytes(messageBytes, secret) + } + + private def forgeIdentityKeySignature(_messageBytes: Array[Byte]): Array[Byte] = { + val z = BigInt(1) + val a = Constants.g.exp(z.bigInteger) + GroupElementSerializer.toBytes(a) ++ Array.fill[Byte](31)(0) ++ z.toByteArray + } + + private case class IdentityCommitmentWitness( + publicKey: GroupElement, + message: Array[Byte], + signature: Array[Byte] + ) + + @tailrec + private def identityCommitmentWitness( + messageForPublicKey: GroupElement => Array[Byte], + candidateSecret: BigInt = BigInt(1) + ): IdentityCommitmentWitness = { + val secret = candidateSecret.mod(SecP256K1Group.q) + val publicKey = Constants.g.exp(secret.bigInteger) + val messageBytes = messageForPublicKey(publicKey) + val aBytes = GroupElementSerializer.toBytes(identityPk) + val e = BigInt(Blake2b256(aBytes ++ messageBytes ++ publicKey.getEncoded.toArray)) + val z = (secret * e).mod(SecP256K1Group.q) + val rawZ = z.toByteArray + if (secret != 0 && z.bitLength <= 255 && rawZ.length <= 32) { + val signature = aBytes ++ Array.fill[Byte](32 - rawZ.length)(0) ++ rawZ + require(signature.length == 65) + require(SigUtils.verify(messageBytes, publicKey, identityPk, z)) + IdentityCommitmentWitness(publicKey, messageBytes, signature) + } else { + identityCommitmentWitness(messageForPublicKey, candidateSecret + 1) + } + } + + private case class StateStep( + inputTree: ErgoValue[AvlTree], + lookupProof: Array[Byte], + updateProof: Array[Byte], + outputTree: ErgoValue[AvlTree] + ) + + private def stateStep( + key: Array[Byte], + oldValue: Option[(Long, Long, Long)], + newValue: (Long, Long, Long) + ): StateStep = { + val tree = new PlasmaMap[Array[Byte], Array[Byte]](redeemedFlags, redeemedParameters) + oldValue.foreach { case (ts, debt, redeemed) => + tree.insertOrUpdate(key -> stateValue(ts, debt, redeemed)) + } + val inputTree = tree.ergoValue + val lookupProof = tree.lookUp(key).proof.bytes + val updateProof = tree.insertOrUpdate(key -> stateValue(newValue._1, newValue._2, newValue._3)).proof.bytes + StateStep(inputTree, lookupProof, updateProof, tree.ergoValue) + } + + private def stateValue(ts: Long, debt: Long, redeemed: Long): Array[Byte] = + Longs.toByteArray(ts) ++ Longs.toByteArray(debt) ++ Longs.toByteArray(redeemed) + + private case class TrackerState(tree: ErgoValue[AvlTree], lookupProof: Array[Byte]) + + private def trackerState( + key: Array[Byte], + debt: Long = totalDebt, + flags: AvlTreeFlags = trackerFlags, + parameters: PlasmaParameters = trackerParameters, + encodedDebt: Option[Array[Byte]] = None + ): TrackerState = { + val tree = new PlasmaMap[Array[Byte], Array[Byte]](flags, parameters) + tree.insertOrUpdate(key -> encodedDebt.getOrElse(Longs.toByteArray(debt))) + TrackerState(tree.ergoValue, tree.lookUp(key).proof.bytes) + } + + private def trackerStateWithShape( + state: TrackerState, + flags: AvlTreeFlags, + keyLength: Int, + valueLengthOpt: Option[Int] + ): TrackerState = { + val current = state.tree.getValue.asInstanceOf[CAvlTree] + val mutated: AvlTreeData = AvlTreeData(current.digest, flags, keyLength, valueLengthOpt) + TrackerState(ErgoValue.of(mutated), state.lookupProof) + } + + private def createOut( + tree: ErgoTree, + value: Long, + registers: Array[ErgoValue[_]], + tokens: Array[ErgoToken] + )(implicit ctx: BlockchainContext): OutBoxImpl = { + val candidate = AppkitHelpers.createBoxCandidate(value, tree, tokens, registers, ctx.getHeight) + new OutBoxImpl(candidate) + } + + private def createOut( + contract: String, + value: Long, + registers: Array[ErgoValue[_]], + tokens: Array[ErgoToken] + )(implicit ctx: BlockchainContext): OutBoxImpl = + createOut(ctx.compileContract(ConstantsBuilder.empty(), contract).getErgoTree, value, registers, tokens) + + private def p2pkTree(key: GroupElement): ErgoTree = P2PKAddress(ProveDlog(key)).script + + private def addTokens(builder: OutBoxBuilder)(tokens: java.util.List[ErgoToken]): OutBoxBuilder = + if (tokens.isEmpty) builder else builder.tokens(tokens.asScala: _*) + + private def addRegisters(builder: OutBoxBuilder)(registers: java.util.List[ErgoValue[_]]): OutBoxBuilder = + if (registers.isEmpty) builder else builder.registers(registers.asScala: _*) + + private def createTx( + inputBoxes: Array[InputBox], + dataInputs: Array[InputBox], + boxesToCreate: Array[OutBoxImpl], + fee: Option[Long] = None, + secrets: Array[String] = Array.empty + )(implicit ctx: BlockchainContext): SignedTransaction = { + val txBuilder = ctx.newTxBuilder + val outputs: Array[OutBox] = boxesToCreate.map { box => + val base = txBuilder.outBoxBuilder() + .value(box.getValue) + .creationHeight(box.getCreationHeight) + .contract(new ErgoTreeContract(box.getErgoTree, NetworkType.MAINNET)) + addRegisters(addTokens(base)(box.getTokens))(box.getRegisters).build + } + val unsignedNoFee = ctx.newTxBuilder() + .boxesToSpend(new util.ArrayList[InputBox](inputBoxes.toList.asJava)) + .withDataInputs(new util.ArrayList[InputBox](dataInputs.toList.asJava)) + .outputs(outputs: _*) + .sendChangeTo(P2PKAddress(ProveDlog(ownerPk))) + val unsigned = fee.map(unsignedNoFee.fee).getOrElse(unsignedNoFee).build() + val prover = secrets.map(BigInt(_)).foldLeft(ctx.newProverBuilder()) { + case (builder, secret) => builder.withDLogSecret(secret.bigInteger) + }.build() + prover.sign(unsigned) + } + + private def reserveRegisters( + tree: ErgoValue[AvlTree], + emergencyHeight: Long, + predecessor: Array[Byte] = zeroId, + refundHeight: Long = 0L, + owner: GroupElement = ownerPk, + trackerId: Array[Byte] = trackerNft + ): Array[ErgoValue[_]] = Array( + ErgoValue.of(owner), + tree, + ErgoValue.of(trackerId), + ErgoValue.of(refundHeight), + ErgoValue.of(emergencyHeight), + ErgoValue.of(predecessor) + ) + + private def redemptionVars( + reserveSignature: Array[Byte], + step: StateStep, + trackerSignature: Option[Array[Byte]], + trackerProof: Option[Array[Byte]], + includePriorProof: Boolean = true, + receiver: GroupElement = receiverPk, + debt: Long = totalDebt, + ts: Long = timestamp + ): Array[ContextVar] = { + val base = Vector( + new ContextVar(0, ErgoValue.of(0: Byte)), + new ContextVar(1, ErgoValue.of(receiver)), + new ContextVar(2, ErgoValue.of(reserveSignature)), + new ContextVar(3, ErgoValue.of(debt)), + new ContextVar(4, ErgoValue.of(ts)), + new ContextVar(5, ErgoValue.of(step.updateProof)) + ) + val withTrackerSig = trackerSignature.fold(base)(sig => base :+ new ContextVar(6, ErgoValue.of(sig))) + val withPrior = if (includePriorProof) withTrackerSig :+ new ContextVar(7, ErgoValue.of(step.lookupProof)) else withTrackerSig + trackerProof.fold(withPrior)(proof => withPrior :+ new ContextVar(8, ErgoValue.of(proof))).toArray + } + + private def ergReserveInput( + txId: String, + reserveNft: Array[Byte], + value: Long, + step: StateStep, + emergencyHeight: Long, + vars: Array[ContextVar], + refundHeight: Long = 0L, + owner: GroupElement = ownerPk, + trackerId: Array[Byte] = trackerNft + )(implicit ctx: BlockchainContext): InputBox = + ctx.newTxBuilder.outBoxBuilder + .value(value) + .tokens(new ErgoToken(reserveNft, 1)) + .registers(reserveRegisters(step.inputTree, emergencyHeight, refundHeight = refundHeight, owner = owner, trackerId = trackerId): _*) + .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisV2Contract)) + .build() + .convertToInputWith(txId, fakeIndex) + .withContextVars(vars: _*) + + private def tokenReserveInput( + txId: String, + value: Long, + reserveAmount: Long, + step: StateStep, + emergencyHeight: Long, + vars: Array[ContextVar], + refundHeight: Long = 0L, + owner: GroupElement = ownerPk, + trackerId: Array[Byte] = trackerNft + )(implicit ctx: BlockchainContext): InputBox = + ctx.newTxBuilder.outBoxBuilder + .value(value) + .tokens(new ErgoToken(tokenReserveNft, 1), new ErgoToken(reserveTokenId, reserveAmount)) + .registers(reserveRegisters(step.inputTree, emergencyHeight, refundHeight = refundHeight, owner = owner, trackerId = trackerId): _*) + .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisTokenV2Contract)) + .build() + .convertToInputWith(txId, fakeIndex) + .withContextVars(vars: _*) + + private def trackerInput( + state: TrackerState, + key: GroupElement = trackerPk, + nftId: Array[Byte] = trackerNft, + nftAmount: Long = 1L + )(implicit ctx: BlockchainContext): InputBox = + ctx.newTxBuilder.outBoxBuilder + .value(minValue) + .tokens(new ErgoToken(nftId, nftAmount)) + .registers(ErgoValue.of(key), state.tree) + .contract(ctx.compileContract(ConstantsBuilder.empty(), "sigmaProp(true)")) + .build() + .convertToInputWith(fakeTxIds(5), fakeIndex) + + private def feeInput( + txId: String, + value: Long = minValue, + tokens: Array[ErgoToken] = Array.empty + )(implicit ctx: BlockchainContext): InputBox = { + val builder = ctx.newTxBuilder.outBoxBuilder.value(value) + .contract(ctx.compileContract(ConstantsBuilder.empty(), "sigmaProp(true)")) + val withTokens = if (tokens.isEmpty) builder else builder.tokens(tokens: _*) + withTokens.build().convertToInputWith(txId, fakeIndex) + } + + private def ergSuccessor( + input: InputBox, + value: Long, + tree: ErgoValue[AvlTree], + reserveNft: Array[Byte], + emergencyHeight: Long, + refundHeight: Long = 0L, + owner: GroupElement = ownerPk, + trackerId: Array[Byte] = trackerNft + )(implicit ctx: BlockchainContext): OutBoxImpl = + createOut( + Constants.basisV2Contract, + value, + reserveRegisters(tree, emergencyHeight, input.getId.getBytes, refundHeight, owner, trackerId), + Array(new ErgoToken(reserveNft, 1)) + ) + + private def tokenSuccessor( + input: InputBox, + value: Long, + reserveAmount: Long, + tree: ErgoValue[AvlTree], + emergencyHeight: Long, + refundHeight: Long = 0L, + owner: GroupElement = ownerPk, + trackerId: Array[Byte] = trackerNft + )(implicit ctx: BlockchainContext): OutBoxImpl = + createOut( + Constants.basisTokenV2Contract, + value, + reserveRegisters(tree, emergencyHeight, input.getId.getBytes, refundHeight, owner, trackerId), + Array(new ErgoToken(tokenReserveNft, 1), new ErgoToken(reserveTokenId, reserveAmount)) + ) + + private def ergPayout( + input: InputBox, + value: Long, + receiver: GroupElement = receiverPk + )(implicit ctx: BlockchainContext): OutBoxImpl = + createOut( + p2pkTree(receiver), + value, + Array[ErgoValue[_]](ErgoValue.of(input.getId.getBytes)), + Array.empty[ErgoToken] + ) + + private def tokenPayout( + input: InputBox, + amount: Long, + receiver: GroupElement = receiverPk + )(implicit ctx: BlockchainContext): OutBoxImpl = + createOut( + p2pkTree(receiver), + minValue, + Array[ErgoValue[_]](ErgoValue.of(input.getId.getBytes)), + Array(new ErgoToken(reserveTokenId, amount)) + ) + + property("Basis v2 claim-key vectors match the runtime ABI") { + val fixedReserveNft = Array.fill[Byte](32)(1) + val fixedTrackerNft = Array.fill[Byte](32)(2) + val fixedTokenId = Array.fill[Byte](32)(5) + val fixedOwner = Constants.g.exp(java.math.BigInteger.ONE) + val fixedReceiver = Constants.g.exp(java.math.BigInteger.valueOf(2L)) + + val ergKey = Blake2b256( + ergDomain ++ fixedReserveNft ++ fixedTrackerNft ++ + fixedOwner.getEncoded.toArray ++ fixedReceiver.getEncoded.toArray + ) + val tokenKey = Blake2b256( + tokenDomain ++ fixedReserveNft ++ fixedTokenId ++ fixedTrackerNft ++ + fixedOwner.getEncoded.toArray ++ fixedReceiver.getEncoded.toArray + ) + + Base16.encode(ergKey) shouldBe + "656c938601a973fe7dd8b5984b70430bf2c885b69a0d91268b0f5c4383a02d73" + Base16.encode(tokenKey) shouldBe + "db33c7176e8d11041a971258458e6be92b59b56865e06ebfbe8e44e9e007f4a1" + } + + property("Basis v2 normal redemption accepts an exact receiver payout with external fee funding") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + // A later tracker commitment may be larger; the older signed cumulative + // claim remains independently redeemable up to its own totalDebt. + val tracker = trackerState(key, totalDebt + 1L) + val claimMessage = message(key) + val input = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt, step, ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), step, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + val successor = ergSuccessor(input, input.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight + 1000L) + val payout = ergPayout(input, amount) + + noException should be thrownBy createTx( + Array(input, feeInput(fakeTxIds(1))), Array(trackerInput(tracker)), + Array(successor, payout), fee = Some(feeValue), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis ERG v2 rejects identity owner, receiver, and tracker independently") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + + val ownerIdentityKey = claimKey(ergDomain, reserveNftA, None, owner = identityPk) + val ownerIdentityStep = stateStep(ownerIdentityKey, None, (timestamp, totalDebt, amount)) + val ownerIdentityMessage = message(ownerIdentityKey) + val ownerIdentityInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt, ownerIdentityStep, + ctx.getHeight.toLong, + redemptionVars(forgeIdentityKeySignature(ownerIdentityMessage), ownerIdentityStep, None, None), + owner = identityPk + ) + a[Throwable] should be thrownBy createTx( + Array(ownerIdentityInput), Array.empty, + Array( + ergSuccessor( + ownerIdentityInput, ownerIdentityInput.getValue - amount, + ownerIdentityStep.outputTree, reserveNftA, ctx.getHeight.toLong, + owner = identityPk + ), + ergPayout(ownerIdentityInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + + val receiverIdentityKey = claimKey(ergDomain, reserveNftA, None, receiver = identityPk) + val receiverIdentityStep = stateStep(receiverIdentityKey, None, (timestamp, totalDebt, amount)) + val receiverIdentityMessage = message(receiverIdentityKey) + val receiverIdentityInput = ergReserveInput( + fakeTxIds(1), reserveNftA, minValue + totalDebt, receiverIdentityStep, + ctx.getHeight.toLong, + redemptionVars( + signatureBytes(receiverIdentityMessage, ownerSecret), receiverIdentityStep, + None, None, receiver = identityPk + ) + ) + a[Throwable] should be thrownBy createTx( + Array(receiverIdentityInput), Array.empty, + Array( + ergSuccessor( + receiverIdentityInput, receiverIdentityInput.getValue - amount, + receiverIdentityStep.outputTree, reserveNftA, ctx.getHeight.toLong + ), + ergPayout(receiverIdentityInput, amount, identityPk) + ), secrets = Array("0") + ) + + val trackerIdentityKey = claimKey(ergDomain, reserveNftA, None) + val trackerIdentityStep = stateStep(trackerIdentityKey, None, (timestamp, totalDebt, amount)) + val trackerIdentityState = trackerState(trackerIdentityKey) + val trackerIdentityMessage = message(trackerIdentityKey) + val trackerIdentityInput = ergReserveInput( + fakeTxIds(2), reserveNftA, minValue + totalDebt, trackerIdentityStep, + ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(trackerIdentityMessage, ownerSecret), trackerIdentityStep, + Some(forgeIdentityKeySignature(trackerIdentityMessage)), + Some(trackerIdentityState.lookupProof) + ) + ) + a[Throwable] should be thrownBy createTx( + Array(trackerIdentityInput), Array(trackerInput(trackerIdentityState, key = identityPk)), + Array( + ergSuccessor( + trackerIdentityInput, trackerIdentityInput.getValue - amount, + trackerIdentityStep.outputTree, reserveNftA, ctx.getHeight + 1000L + ), + ergPayout(trackerIdentityInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis token v2 rejects identity owner, receiver, and tracker independently") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300L + val reserveAmount = 1000L + + val ownerIdentityKey = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId), owner = identityPk) + val ownerIdentityStep = stateStep(ownerIdentityKey, None, (timestamp, totalDebt, amount)) + val ownerIdentityMessage = message(ownerIdentityKey) + val ownerIdentityInput = tokenReserveInput( + fakeTxIds(0), minValue * 2, reserveAmount, ownerIdentityStep, + ctx.getHeight.toLong, + redemptionVars(forgeIdentityKeySignature(ownerIdentityMessage), ownerIdentityStep, None, None), + owner = identityPk + ) + a[Throwable] should be thrownBy createTx( + Array(ownerIdentityInput, feeInput(fakeTxIds(4))), Array.empty, + Array( + tokenSuccessor( + ownerIdentityInput, ownerIdentityInput.getValue, reserveAmount - amount, + ownerIdentityStep.outputTree, ctx.getHeight.toLong, owner = identityPk + ), + tokenPayout(ownerIdentityInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + + val receiverIdentityKey = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId), receiver = identityPk) + val receiverIdentityStep = stateStep(receiverIdentityKey, None, (timestamp, totalDebt, amount)) + val receiverIdentityMessage = message(receiverIdentityKey) + val receiverIdentityInput = tokenReserveInput( + fakeTxIds(1), minValue * 2, reserveAmount, receiverIdentityStep, + ctx.getHeight.toLong, + redemptionVars( + signatureBytes(receiverIdentityMessage, ownerSecret), receiverIdentityStep, + None, None, receiver = identityPk + ) + ) + a[Throwable] should be thrownBy createTx( + Array(receiverIdentityInput, feeInput(fakeTxIds(4))), Array.empty, + Array( + tokenSuccessor( + receiverIdentityInput, receiverIdentityInput.getValue, reserveAmount - amount, + receiverIdentityStep.outputTree, ctx.getHeight.toLong + ), + tokenPayout(receiverIdentityInput, amount, identityPk) + ), secrets = Array("0") + ) + + val trackerIdentityKey = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId)) + val trackerIdentityStep = stateStep(trackerIdentityKey, None, (timestamp, totalDebt, amount)) + val trackerIdentityState = trackerState(trackerIdentityKey) + val trackerIdentityMessage = message(trackerIdentityKey) + val trackerIdentityInput = tokenReserveInput( + fakeTxIds(2), minValue * 2, reserveAmount, trackerIdentityStep, + ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(trackerIdentityMessage, ownerSecret), trackerIdentityStep, + Some(forgeIdentityKeySignature(trackerIdentityMessage)), + Some(trackerIdentityState.lookupProof) + ) + ) + a[Throwable] should be thrownBy createTx( + Array(trackerIdentityInput, feeInput(fakeTxIds(4))), + Array(trackerInput(trackerIdentityState, key = identityPk)), + Array( + tokenSuccessor( + trackerIdentityInput, trackerIdentityInput.getValue, reserveAmount - amount, + trackerIdentityStep.outputTree, ctx.getHeight + 1000L + ), + tokenPayout(trackerIdentityInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis ERG and token v2 reject identity owner and tracker Schnorr commitments independently") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val ergAmount = 300000000L + val ergOwnerWitness = identityCommitmentWitness { owner => + message(claimKey(ergDomain, reserveNftA, None, owner = owner)) + } + val ergOwnerKey = claimKey(ergDomain, reserveNftA, None, owner = ergOwnerWitness.publicKey) + val ergOwnerStep = stateStep(ergOwnerKey, None, (timestamp, totalDebt, ergAmount)) + val ergOwnerInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt, ergOwnerStep, + ctx.getHeight.toLong, + redemptionVars(ergOwnerWitness.signature, ergOwnerStep, None, None), + owner = ergOwnerWitness.publicKey + ) + a[Throwable] should be thrownBy createTx( + Array(ergOwnerInput), Array.empty, + Array( + ergSuccessor( + ergOwnerInput, ergOwnerInput.getValue - ergAmount, + ergOwnerStep.outputTree, reserveNftA, ctx.getHeight.toLong, + owner = ergOwnerWitness.publicKey + ), + ergPayout(ergOwnerInput, ergAmount) + ), secrets = Array(receiverSecret.toString) + ) + + val tokenAmount = 300L + val reserveAmount = 1000L + val tokenOwnerWitness = identityCommitmentWitness { owner => + message(claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId), owner = owner)) + } + val tokenOwnerKey = claimKey( + tokenDomain, tokenReserveNft, Some(reserveTokenId), owner = tokenOwnerWitness.publicKey + ) + val tokenOwnerStep = stateStep(tokenOwnerKey, None, (timestamp, totalDebt, tokenAmount)) + val tokenOwnerInput = tokenReserveInput( + fakeTxIds(1), minValue * 2, reserveAmount, tokenOwnerStep, + ctx.getHeight.toLong, + redemptionVars(tokenOwnerWitness.signature, tokenOwnerStep, None, None), + owner = tokenOwnerWitness.publicKey + ) + a[Throwable] should be thrownBy createTx( + Array(tokenOwnerInput, feeInput(fakeTxIds(4))), Array.empty, + Array( + tokenSuccessor( + tokenOwnerInput, tokenOwnerInput.getValue, reserveAmount - tokenAmount, + tokenOwnerStep.outputTree, ctx.getHeight.toLong, + owner = tokenOwnerWitness.publicKey + ), + tokenPayout(tokenOwnerInput, tokenAmount) + ), secrets = Array(receiverSecret.toString) + ) + + val ergKey = claimKey(ergDomain, reserveNftA, None) + val ergStep = stateStep(ergKey, None, (timestamp, totalDebt, ergAmount)) + val ergMessage = message(ergKey) + val ergTracker = trackerState(ergKey) + val ergTrackerWitness = identityCommitmentWitness(_ => ergMessage) + val ergTrackerInput = ergReserveInput( + fakeTxIds(2), reserveNftA, minValue + totalDebt, ergStep, + ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(ergMessage, ownerSecret), ergStep, + Some(ergTrackerWitness.signature), + Some(ergTracker.lookupProof) + ) + ) + a[Throwable] should be thrownBy createTx( + Array(ergTrackerInput), Array(trackerInput(ergTracker, key = ergTrackerWitness.publicKey)), + Array( + ergSuccessor( + ergTrackerInput, ergTrackerInput.getValue - ergAmount, + ergStep.outputTree, reserveNftA, ctx.getHeight + 1000L + ), + ergPayout(ergTrackerInput, ergAmount) + ), secrets = Array(receiverSecret.toString) + ) + + val tokenKey = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId)) + val tokenStep = stateStep(tokenKey, None, (timestamp, totalDebt, tokenAmount)) + val tokenMessage = message(tokenKey) + val tokenTracker = trackerState(tokenKey) + val tokenTrackerWitness = identityCommitmentWitness(_ => tokenMessage) + val tokenTrackerInput = tokenReserveInput( + fakeTxIds(3), minValue * 2, reserveAmount, tokenStep, + ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(tokenMessage, ownerSecret), tokenStep, + Some(tokenTrackerWitness.signature), + Some(tokenTracker.lookupProof) + ) + ) + a[Throwable] should be thrownBy createTx( + Array(tokenTrackerInput, feeInput(fakeTxIds(4))), + Array(trackerInput(tokenTracker, key = tokenTrackerWitness.publicKey)), + Array( + tokenSuccessor( + tokenTrackerInput, tokenTrackerInput.getValue, reserveAmount - tokenAmount, + tokenStep.outputTree, ctx.getHeight + 1000L + ), + tokenPayout(tokenTrackerInput, tokenAmount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects a cross-reserve owner signature with a correct tracker signature") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val keyA = claimKey(ergDomain, reserveNftA, None) + val keyB = claimKey(ergDomain, reserveNftB, None) + val stepB = stateStep(keyB, None, (timestamp, totalDebt, amount)) + val trackerB = trackerState(keyB) + val wrongMessage = message(keyA) + val inputB = ergReserveInput( + fakeTxIds(0), reserveNftB, minValue + totalDebt, stepB, ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(wrongMessage, ownerSecret), stepB, + Some(signatureBytes(message(keyB), trackerSecret)), Some(trackerB.lookupProof) + ) + ) + + a[Throwable] should be thrownBy createTx( + Array(inputB), Array(trackerInput(trackerB)), + Array( + ergSuccessor(inputB, inputB.getValue - amount, stepB.outputTree, reserveNftB, ctx.getHeight + 1000L), + ergPayout(inputB, amount) + ), + secrets = Array(receiverSecret.toString) + ) + + } + } + + property("Basis v2 rejects a cross-domain tracker signature with a correct owner signature") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val expectedKey = claimKey(ergDomain, reserveNftA, None) + val wrongDomainKey = claimKey(tokenDomain, reserveNftA, None) + val expectedStep = stateStep(expectedKey, None, (timestamp, totalDebt, amount)) + val expectedTracker = trackerState(expectedKey) + val wrongDomainMessage = message(wrongDomainKey) + val wrongDomainInput = ergReserveInput( + fakeTxIds(1), reserveNftA, minValue + totalDebt, expectedStep, + ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(message(expectedKey), ownerSecret), expectedStep, + Some(signatureBytes(wrongDomainMessage, trackerSecret)), + Some(expectedTracker.lookupProof) + ) + ) + + a[Throwable] should be thrownBy createTx( + Array(wrongDomainInput), Array(trackerInput(expectedTracker)), + Array( + ergSuccessor( + wrongDomainInput, wrongDomainInput.getValue - amount, + expectedStep.outputTree, reserveNftA, ctx.getHeight + 1000L + ), + ergPayout(wrongDomainInput, amount) + ), + secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects an omitted prior-state proof") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + val tracker = trackerState(key) + val claimMessage = message(key) + val input = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt, step, ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), step, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof), + includePriorProof = false + ) + ) + + a[Throwable] should be thrownBy createTx( + Array(input), Array(trackerInput(tracker)), + Array( + ergSuccessor(input, input.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(input, amount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects tracker singleton, tree flags, value shape, and insufficient debt independently") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + val claimMessage = message(key) + + def input(txId: String, tracker: TrackerState) = ergReserveInput( + txId, reserveNftA, minValue + totalDebt, step, ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), step, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + + def outputs(reserveInput: InputBox) = Array( + ergSuccessor( + reserveInput, reserveInput.getValue - amount, + step.outputTree, reserveNftA, ctx.getHeight + 1000L + ), + ergPayout(reserveInput, amount) + ) + + val correctTracker = trackerState(key) + val wrongNftInput = input(fakeTxIds(0), correctTracker) + a[Throwable] should be thrownBy createTx( + Array(wrongNftInput), + Array(trackerInput(correctTracker, nftId = reserveNftB)), + outputs(wrongNftInput), secrets = Array(receiverSecret.toString) + ) + + val shapeMutants = Seq( + "key length" -> trackerStateWithShape(correctTracker, trackerFlags, 31, Some(8)), + "fixed value length" -> trackerStateWithShape(correctTracker, trackerFlags, 32, Some(16)), + "variable value length" -> trackerStateWithShape(correctTracker, trackerFlags, 32, None), + "insert flag" -> trackerStateWithShape( + correctTracker, + AvlTreeFlags(insertAllowed = false, updateAllowed = true, removeAllowed = false), + 32, + Some(8) + ), + "update flag" -> trackerStateWithShape( + correctTracker, + AvlTreeFlags(insertAllowed = true, updateAllowed = false, removeAllowed = false), + 32, + Some(8) + ), + "remove flag" -> trackerStateWithShape( + correctTracker, + AvlTreeFlags(insertAllowed = true, updateAllowed = true, removeAllowed = true), + 32, + Some(8) + ) + ) + shapeMutants.zipWithIndex.foreach { case ((label, mutant), index) => + val mutatedInput = input(fakeTxIds(1 + (index % 4)), mutant) + withClue(s"tracker $label: ") { + a[Throwable] should be thrownBy createTx( + Array(mutatedInput), Array(trackerInput(mutant)), + outputs(mutatedInput), secrets = Array(receiverSecret.toString) + ) + } + } + + val insufficientTracker = trackerState(key, debt = totalDebt - 1L) + val insufficientDebtInput = input(fakeTxIds(3), insufficientTracker) + a[Throwable] should be thrownBy createTx( + Array(insufficientDebtInput), Array(trackerInput(insufficientTracker)), + outputs(insufficientDebtInput), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects invalid R5 claim progression and over-redemption independently") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val priorRedeemed = 100000000L + val amount = 100000000L + val key = claimKey(ergDomain, reserveNftA, None) + + val regressingStep = stateStep( + key, + Some((timestamp, totalDebt, priorRedeemed)), + (timestamp, totalDebt + 1L, priorRedeemed + amount) + ) + val regressingMessage = message(key, totalDebt + 1L, timestamp) + val regressingInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt - priorRedeemed, + regressingStep, ctx.getHeight.toLong, + redemptionVars( + signatureBytes(regressingMessage, ownerSecret), regressingStep, + None, None, debt = totalDebt + 1L + ) + ) + a[Throwable] should be thrownBy createTx( + Array(regressingInput), Array.empty, + Array( + ergSuccessor( + regressingInput, regressingInput.getValue - amount, + regressingStep.outputTree, reserveNftA, ctx.getHeight.toLong + ), + ergPayout(regressingInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + + val overAmount = totalDebt + 1L + val overStep = stateStep(key, None, (timestamp, totalDebt, overAmount)) + val claimMessage = message(key) + val overInput = ergReserveInput( + fakeTxIds(1), reserveNftA, minValue + overAmount, overStep, + ctx.getHeight.toLong, + redemptionVars(signatureBytes(claimMessage, ownerSecret), overStep, None, None) + ) + a[Throwable] should be thrownBy createTx( + Array(overInput), Array.empty, + Array( + ergSuccessor( + overInput, overInput.getValue - overAmount, + overStep.outputTree, reserveNftA, ctx.getHeight.toLong + ), + ergPayout(overInput, overAmount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects a stale absence proof for an existing redeemed record") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val firstAmount = 300000000L + val secondAmount = 200000000L + val key = claimKey(ergDomain, reserveNftA, None) + val validStep = stateStep( + key, + Some((timestamp, totalDebt, firstAmount)), + (timestamp, totalDebt, firstAmount + secondAmount) + ) + val emptyStep = stateStep(key, None, (timestamp, totalDebt, secondAmount)) + val staleProofStep = validStep.copy(lookupProof = emptyStep.lookupProof) + val tracker = trackerState(key) + val claimMessage = message(key) + val input = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt - firstAmount, + staleProofStep, ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), staleProofStep, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + + a[Throwable] should be thrownBy createTx( + Array(input), Array(trackerInput(tracker)), + Array( + ergSuccessor(input, input.getValue - secondAmount, validStep.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(input, secondAmount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects mutated receiver, payout amount, payout lineage, and reserve-funded fee independently") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + val tracker = trackerState(key) + val claimMessage = message(key) + + def input(txId: String) = ergReserveInput( + txId, reserveNftA, minValue + totalDebt, step, ctx.getHeight + 1000L, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), step, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + + val wrongReceiverInput = input(fakeTxIds(0)) + a[Throwable] should be thrownBy createTx( + Array(wrongReceiverInput), Array(trackerInput(tracker)), + Array( + ergSuccessor(wrongReceiverInput, wrongReceiverInput.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(wrongReceiverInput, amount, otherPk) + ), secrets = Array(receiverSecret.toString) + ) + + val wrongAmountInput = input(fakeTxIds(1)) + a[Throwable] should be thrownBy createTx( + Array(wrongAmountInput), Array(trackerInput(tracker)), + Array( + ergSuccessor(wrongAmountInput, wrongAmountInput.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(wrongAmountInput, amount - feeValue) + ), secrets = Array(receiverSecret.toString) + ) + + val wrongLineageInput = input(fakeTxIds(3)) + val wrongLineagePayout = createOut( + p2pkTree(receiverPk), amount, + Array[ErgoValue[_]](ErgoValue.of(reserveNftB)), + Array.empty[ErgoToken] + ) + a[Throwable] should be thrownBy createTx( + Array(wrongLineageInput), Array(trackerInput(tracker)), + Array( + ergSuccessor(wrongLineageInput, wrongLineageInput.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight + 1000L), + wrongLineagePayout + ), secrets = Array(receiverSecret.toString) + ) + + val feeDrainInput = input(fakeTxIds(2)) + a[Throwable] should be thrownBy createTx( + Array(feeDrainInput), Array(trackerInput(tracker)), + Array( + ergSuccessor(feeDrainInput, feeDrainInput.getValue - amount - feeValue, step.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(feeDrainInput, amount) + ), fee = Some(feeValue), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 permits repeated partial settlement with the original signed cumulative claim") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val firstAmount = 300000000L + val secondAmount = 400000000L + val key = claimKey(ergDomain, reserveNftA, None) + val tracker = trackerState(key) + val claimMessage = message(key) + val ownerSignature = signatureBytes(claimMessage, ownerSecret) + val trackerSignature = signatureBytes(claimMessage, trackerSecret) + + val firstStep = stateStep(key, None, (timestamp, totalDebt, firstAmount)) + val firstInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt, firstStep, ctx.getHeight + 1000L, + redemptionVars(ownerSignature, firstStep, Some(trackerSignature), Some(tracker.lookupProof)) + ) + val firstTx = createTx( + Array(firstInput), Array(trackerInput(tracker)), + Array( + ergSuccessor(firstInput, firstInput.getValue - firstAmount, firstStep.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(firstInput, firstAmount) + ), secrets = Array(receiverSecret.toString) + ) + + val secondStep = stateStep( + key, + Some((timestamp, totalDebt, firstAmount)), + (timestamp, totalDebt, firstAmount + secondAmount) + ) + val secondInput = firstTx.getOutputsToSpend.get(0).withContextVars( + redemptionVars(ownerSignature, secondStep, Some(trackerSignature), Some(tracker.lookupProof)): _* + ) + + noException should be thrownBy createTx( + Array(secondInput), Array(trackerInput(tracker)), + Array( + ergSuccessor(secondInput, secondInput.getValue - secondAmount, secondStep.outputTree, reserveNftA, ctx.getHeight + 1000L), + ergPayout(secondInput, secondAmount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 emergency redemption is tracker-free only at the fixed immutable boundary") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300000000L + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + val claimMessage = message(key) + val ownerSignature = signatureBytes(claimMessage, ownerSecret) + + val emergencyInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue + totalDebt, step, ctx.getHeight.toLong, + redemptionVars(ownerSignature, step, None, None) + ) + noException should be thrownBy createTx( + Array(emergencyInput), Array.empty, + Array( + ergSuccessor(emergencyInput, emergencyInput.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight.toLong), + ergPayout(emergencyInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + + val preBoundaryInput = ergReserveInput( + fakeTxIds(1), reserveNftA, minValue + totalDebt, step, ctx.getHeight + 1L, + redemptionVars(ownerSignature, step, None, None) + ) + a[Throwable] should be thrownBy createTx( + Array(preBoundaryInput), Array.empty, + Array( + ergSuccessor(preBoundaryInput, preBoundaryInput.getValue - amount, step.outputTree, reserveNftA, ctx.getHeight + 1L), + ergPayout(preBoundaryInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + + val mutatedBoundaryInput = ergReserveInput( + fakeTxIds(2), reserveNftA, minValue + totalDebt, step, + ctx.getHeight.toLong, redemptionVars(ownerSignature, step, None, None) + ) + a[Throwable] should be thrownBy createTx( + Array(mutatedBoundaryInput), Array.empty, + Array( + ergSuccessor( + mutatedBoundaryInput, mutatedBoundaryInput.getValue - amount, + step.outputTree, reserveNftA, ctx.getHeight + 1L + ), + ergPayout(mutatedBoundaryInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 rejects tokenless reserve state and two reserve inputs sharing one successor or payout pair") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, 1L)) + val emergency = ctx.getHeight + 1000L + + val tokenless = ctx.newTxBuilder.outBoxBuilder + .value(minValue) + .registers(reserveRegisters(step.inputTree, emergency): _*) + .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisV2Contract)) + .build().convertToInputWith(fakeTxIds(0), fakeIndex) + .withContextVars(new ContextVar(0, ErgoValue.of(10: Byte))) + val tokenlessOut = createOut( + Constants.basisV2Contract, minValue + 100000000L, + reserveRegisters(step.inputTree, emergency, tokenless.getId.getBytes), Array.empty[ErgoToken] + ) + a[Throwable] should be thrownBy createTx( + Array(tokenless, feeInput(fakeTxIds(5), value = 100000000L)), + Array.empty, + Array(tokenlessOut) + ) + + def topUpInput(txId: String) = ctx.newTxBuilder.outBoxBuilder + .value(minValue) + .tokens(new ErgoToken(reserveNftA, 1)) + .registers(reserveRegisters(step.inputTree, emergency): _*) + .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.basisV2Contract)) + .build().convertToInputWith(txId, fakeIndex) + .withContextVars(new ContextVar(0, ErgoValue.of(10: Byte))) + + val first = topUpInput(fakeTxIds(1)) + val second = topUpInput(fakeTxIds(2)) + val shared = ergSuccessor(first, minValue + 100000000L, step.inputTree, reserveNftA, emergency) + a[Throwable] should be thrownBy createTx(Array(first, second), Array.empty, Array(shared)) + + val amount = 300000000L + val redemptionStep = stateStep(key, None, (timestamp, totalDebt, amount)) + val tracker = trackerState(key) + val claimMessage = message(key) + def redemptionInput(txId: String) = ergReserveInput( + txId, reserveNftA, minValue + totalDebt, redemptionStep, emergency, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), redemptionStep, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + val firstRedemption = redemptionInput(fakeTxIds(3)) + val secondRedemption = redemptionInput(fakeTxIds(4)) + a[Throwable] should be thrownBy createTx( + Array(firstRedemption, secondRedemption), Array(trackerInput(tracker)), + Array( + ergSuccessor( + firstRedemption, firstRedemption.getValue - amount, + redemptionStep.outputTree, reserveNftA, emergency + ), + ergPayout(firstRedemption, amount) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis token v2 preserves ERG on top-up and redemption") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300L + val reserveAmount = 1000L + val emergency = ctx.getHeight + 1000L + val key = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId)) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + + val topUpInput = tokenReserveInput( + fakeTxIds(0), minValue * 2, reserveAmount, step, emergency, + Array(new ContextVar(0, ErgoValue.of(10: Byte))) + ) + val tokenFunding = feeInput(fakeTxIds(1), tokens = Array(new ErgoToken(reserveTokenId, 1))) + val drainingTopUp = tokenSuccessor( + topUpInput, minValue, reserveAmount + 1, step.inputTree, emergency + ) + a[Throwable] should be thrownBy createTx( + Array(topUpInput, tokenFunding), Array.empty, Array(drainingTopUp) + ) + + val tracker = trackerState(key) + val claimMessage = message(key) + val redemptionInput = tokenReserveInput( + fakeTxIds(2), minValue * 2, reserveAmount, step, emergency, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), step, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + val drainingSuccessor = tokenSuccessor( + redemptionInput, minValue, reserveAmount - amount, step.outputTree, emergency + ) + a[Throwable] should be thrownBy createTx( + Array(redemptionInput), Array(trackerInput(tracker)), + Array(drainingSuccessor, tokenPayout(redemptionInput, amount)), + secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis token v2 accepts exact token payout and rejects token leakage to change") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val amount = 300L + val reserveAmount = 1000L + val emergency = ctx.getHeight + 1000L + val key = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId)) + val step = stateStep(key, None, (timestamp, totalDebt, amount)) + val tracker = trackerState(key) + val claimMessage = message(key) + + def input(txId: String) = tokenReserveInput( + txId, minValue * 2, reserveAmount, step, emergency, + redemptionVars( + signatureBytes(claimMessage, ownerSecret), step, + Some(signatureBytes(claimMessage, trackerSecret)), Some(tracker.lookupProof) + ) + ) + + val validInput = input(fakeTxIds(0)) + noException should be thrownBy createTx( + Array(validInput, feeInput(fakeTxIds(1))), Array(trackerInput(tracker)), + Array( + tokenSuccessor(validInput, validInput.getValue, reserveAmount - amount, step.outputTree, emergency), + tokenPayout(validInput, amount) + ), secrets = Array(receiverSecret.toString) + ) + + val leakingInput = input(fakeTxIds(2)) + a[Throwable] should be thrownBy createTx( + Array(leakingInput, feeInput(fakeTxIds(3))), Array(trackerInput(tracker)), + Array( + tokenSuccessor(leakingInput, leakingInput.getValue, reserveAmount - amount, step.outputTree, emergency), + tokenPayout(leakingInput, amount - 1) + ), secrets = Array(receiverSecret.toString) + ) + } + } + + property("Basis v2 top-up branches preserve state and accept only externally funded increases") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val emergency = ctx.getHeight + 1000L + val ergKey = claimKey(ergDomain, reserveNftA, None) + val ergStep = stateStep(ergKey, None, (timestamp, totalDebt, 1L)) + val ergInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue, ergStep, emergency, + Array(new ContextVar(0, ErgoValue.of(10: Byte))) + ) + noException should be thrownBy createTx( + Array(ergInput, feeInput(fakeTxIds(1))), Array.empty, + Array(ergSuccessor(ergInput, minValue + 100000000L, ergStep.inputTree, reserveNftA, emergency)) + ) + + val tokenKey = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId)) + val tokenStep = stateStep(tokenKey, None, (timestamp, totalDebt, 1L)) + val tokenInput = tokenReserveInput( + fakeTxIds(2), minValue, 1000L, tokenStep, emergency, + Array(new ContextVar(0, ErgoValue.of(10: Byte))) + ) + noException should be thrownBy createTx( + Array( + tokenInput, + feeInput(fakeTxIds(3), tokens = Array(new ErgoToken(reserveTokenId, 1L))) + ), + Array.empty, + Array(tokenSuccessor(tokenInput, minValue, 1001L, tokenStep.inputTree, emergency)) + ) + } + } + + property("Basis v2 refund initiation and completion enforce owner proof and overflow-safe delay") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val emergency = ctx.getHeight + 100000L + val key = claimKey(ergDomain, reserveNftA, None) + val step = stateStep(key, None, (timestamp, totalDebt, 1L)) + + val initiateInput = ergReserveInput( + fakeTxIds(0), reserveNftA, minValue, step, emergency, + Array(new ContextVar(0, ErgoValue.of(20: Byte))) + ) + noException should be thrownBy createTx( + Array(initiateInput), Array.empty, + Array( + ergSuccessor( + initiateInput, minValue, step.inputTree, reserveNftA, emergency, + refundHeight = ctx.getHeight.toLong + ) + ), secrets = Array(ownerSecret.toString) + ) + + val unauthorizedInitiateInput = ergReserveInput( + fakeTxIds(4), reserveNftA, minValue, step, emergency, + Array(new ContextVar(0, ErgoValue.of(20: Byte))) + ) + a[Throwable] should be thrownBy createTx( + Array(unauthorizedInitiateInput), Array.empty, + Array( + ergSuccessor( + unauthorizedInitiateInput, minValue, step.inputTree, reserveNftA, + emergency, refundHeight = ctx.getHeight.toLong + ) + ) + ) + + val maturedHeight = ctx.getHeight.toLong - 43200L + val completeInput = ergReserveInput( + fakeTxIds(1), reserveNftA, minValue, step, emergency, + Array(new ContextVar(0, ErgoValue.of(30: Byte))), refundHeight = maturedHeight + ) + val ownerOutput = createOut( + p2pkTree(ownerPk), minValue, Array.empty[ErgoValue[_]], + Array(new ErgoToken(reserveNftA, 1L)) + ) + noException should be thrownBy createTx( + Array(completeInput), Array.empty, Array(ownerOutput), + secrets = Array(ownerSecret.toString) + ) + a[Throwable] should be thrownBy createTx( + Array(completeInput), Array.empty, Array(ownerOutput) + ) + + val earlyInput = ergReserveInput( + fakeTxIds(2), reserveNftA, minValue, step, emergency, + Array(new ContextVar(0, ErgoValue.of(30: Byte))), + refundHeight = ctx.getHeight.toLong - 43199L + ) + a[Throwable] should be thrownBy createTx( + Array(earlyInput), Array.empty, Array(ownerOutput), + secrets = Array(ownerSecret.toString) + ) + + val overflowSeed = ergReserveInput( + fakeTxIds(3), reserveNftA, minValue, step, emergency, + Array(new ContextVar(0, ErgoValue.of(30: Byte))), refundHeight = Long.MaxValue + ) + a[Throwable] should be thrownBy createTx( + Array(overflowSeed), Array.empty, Array(ownerOutput), + secrets = Array(ownerSecret.toString) + ) + } + } + + property("Basis token v2 refund branches preserve both token roles until owner-authorized completion") { + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + val emergency = ctx.getHeight + 100000L + val key = claimKey(tokenDomain, tokenReserveNft, Some(reserveTokenId)) + val step = stateStep(key, None, (timestamp, totalDebt, 1L)) + val reserveAmount = 1000L + + val initiateInput = tokenReserveInput( + fakeTxIds(0), minValue, reserveAmount, step, emergency, + Array(new ContextVar(0, ErgoValue.of(20: Byte))) + ) + noException should be thrownBy createTx( + Array(initiateInput), Array.empty, + Array( + tokenSuccessor( + initiateInput, minValue, reserveAmount, step.inputTree, emergency, + refundHeight = ctx.getHeight.toLong + ) + ), secrets = Array(ownerSecret.toString) + ) + + val unauthorizedInitiateInput = tokenReserveInput( + fakeTxIds(2), minValue, reserveAmount, step, emergency, + Array(new ContextVar(0, ErgoValue.of(20: Byte))) + ) + a[Throwable] should be thrownBy createTx( + Array(unauthorizedInitiateInput), Array.empty, + Array( + tokenSuccessor( + unauthorizedInitiateInput, minValue, reserveAmount, + step.inputTree, emergency, refundHeight = ctx.getHeight.toLong + ) + ) + ) + + val completeInput = tokenReserveInput( + fakeTxIds(1), minValue, reserveAmount, step, emergency, + Array(new ContextVar(0, ErgoValue.of(30: Byte))), + refundHeight = ctx.getHeight.toLong - 43200L + ) + val ownerOutput = createOut( + p2pkTree(ownerPk), minValue, Array.empty[ErgoValue[_]], + Array( + new ErgoToken(tokenReserveNft, 1L), + new ErgoToken(reserveTokenId, reserveAmount) + ) + ) + noException should be thrownBy createTx( + Array(completeInput), Array.empty, Array(ownerOutput), + secrets = Array(ownerSecret.toString) + ) + a[Throwable] should be thrownBy createTx( + Array(completeInput), Array.empty, Array(ownerOutput) + ) + } + } + + property("Basis v2 source constants compile from the exact repository files") { + val root = Paths.get(sys.props("user.dir")) + val ergSource = new String(Files.readAllBytes(root.resolve("contracts/offchain/basis-v2.es")), StandardCharsets.UTF_8) + .replace("\r\n", "\n").stripSuffix("\n") + val tokenSource = new String(Files.readAllBytes(root.resolve("contracts/offchain/basis-token-v2.es")), StandardCharsets.UTF_8) + .replace("\r\n", "\n").stripSuffix("\n") + Constants.basisV2Contract shouldEqual ergSource + Constants.basisTokenV2Contract shouldEqual tokenSource + + createMockedErgoClient(MockData(Nil, Nil)).execute { implicit ctx => + Constants.basisV2ErgoTree.bytes shouldEqual + ctx.compileContract(ConstantsBuilder.empty(), ergSource).getErgoTree.bytes + Constants.basisTokenV2ErgoTree.bytes shouldEqual + ctx.compileContract(ConstantsBuilder.empty(), tokenSource).getErgoTree.bytes + } + } + + property("Basis v2 full ErgoTree bytes match the committed golden files") { + val root = Paths.get(sys.props("user.dir")) + def expected(path: String): Array[Byte] = + hex(new String(Files.readAllBytes(root.resolve(path)), StandardCharsets.US_ASCII).trim) + Constants.basisV2ErgoTree.bytes shouldEqual expected("contracts/offchain/basis-v2.p2s") + Constants.basisTokenV2ErgoTree.bytes shouldEqual expected("contracts/offchain/basis-token-v2.p2s") + + // Keep the digest calculation in the test so reviewers can independently + // compare the full bytes before relying on the shorter receipt hashes. + MessageDigest.getInstance("SHA-256").digest(Constants.basisV2ErgoTree.bytes).length shouldEqual 32 + MessageDigest.getInstance("SHA-256").digest(Constants.basisTokenV2ErgoTree.bytes).length shouldEqual 32 + } +} diff --git a/src/test/scala/chaincash/ChainCashSpec.scala b/src/test/scala/chaincash/ChainCashSpec.scala index dc1a9c7..443af21 100644 --- a/src/test/scala/chaincash/ChainCashSpec.scala +++ b/src/test/scala/chaincash/ChainCashSpec.scala @@ -172,7 +172,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(noteTokenId, noteValue)) .registers(Constants.emptyTreeErgoValue, ErgoValue.of(holderPk), ErgoValue.of(position)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -189,12 +189,12 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1)) .registers(ErgoValue.of(holderPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId2, fakeIndex) val noteOutput = createOut( - Constants.noteContract, + HistoricalContractFixtures.noteContract, minValue, Array(outTree, ErgoValue.of(holderPk), ErgoValue.of(position + 1)), Array(new ErgoToken(noteTokenId, 1)) @@ -243,7 +243,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue + feeValue) .tokens(new ErgoToken(noteTokenId, noteValue)) .registers(historyTree, ErgoValue.of(holderPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -257,7 +257,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1)) .registers(ErgoValue.of(holderPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId2, fakeIndex) .withContextVars( @@ -291,14 +291,14 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .convertToInputWith(fakeTxId3, fakeIndex) val reserveOutput = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue - oracleRate * 98 / 100, registers = Array(ErgoValue.of(holderPk)), tokens = Array(new ErgoToken(reserveNFT, 1)) ) val receiptOutput = createOut( - Constants.receiptContract, + HistoricalContractFixtures.receiptContract, minValue, registers = Array(historyTree, ErgoValue.of(0L), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(holderPk)), tokens = Array(new ErgoToken(noteTokenId, noteValue)) @@ -407,7 +407,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue + feeValue) .tokens(new ErgoToken(noteTokenId, finalNoteValue)) .registers(historyTree, ErgoValue.of(holderPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -424,7 +424,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveBNFT, 1)) .registers(ErgoValue.of(reserveBPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId2, fakeIndex) .withContextVars( @@ -446,14 +446,14 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .convertToInputWith(fakeTxId5, fakeIndex) val reserveOutput = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue - oracleRate * 98 / 100, registers = Array(ErgoValue.of(reserveBPk)), tokens = Array(new ErgoToken(reserveBNFT, 1)) ) val receiptOutput = createOut( - Constants.receiptContract, + HistoricalContractFixtures.receiptContract, minValue, registers = Array(historyTree, ErgoValue.of(bPosition), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(reserveBPk)), tokens = Array(new ErgoToken(noteTokenId, finalNoteValue)) @@ -493,7 +493,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveANFT, 1)) .registers(ErgoValue.of(reserveAPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId5, fakeIndex) .withContextVars( @@ -505,14 +505,14 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty ) val reserveAOutput = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue - oracleRate * 98 / 100, registers = Array(ErgoValue.of(reserveAPk)), tokens = Array(new ErgoToken(reserveANFT, 1)) ) val receiptAOutput = createOut( - Constants.receiptContract, + HistoricalContractFixtures.receiptContract, minValue, registers = Array(historyTree, ErgoValue.of(aPosition), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(reserveAPk)), tokens = Array(new ErgoToken(noteTokenId, finalNoteValue)) @@ -612,7 +612,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .registers(historyTree, ErgoValue.of(bPosition), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(reserveBPk)) .tokens(new ErgoToken(noteTokenId, bValue)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.receiptContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.receiptContract)) .build() .convertToInputWith(fakeTxId4, fakeIndex) @@ -626,7 +626,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveCNFT, 1)) .registers(ErgoValue.of(reserveCPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId5, fakeIndex) .withContextVars( @@ -648,14 +648,14 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .convertToInputWith(fakeTxId2, fakeIndex) val reserveCOutput = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue - oracleRate * 98 / 100, registers = Array(ErgoValue.of(reserveCPk)), tokens = Array(new ErgoToken(reserveCNFT, 1)) ) val receiptCOutput = createOut( - Constants.receiptContract, + HistoricalContractFixtures.receiptContract, minValue, registers = Array(historyTree, ErgoValue.of(cPosition), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(reserveCPk)), tokens = Array(new ErgoToken(noteTokenId, finalNoteValue)) @@ -743,7 +743,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue + feeValue) .tokens(new ErgoToken(note1TokenId, noteValue)) .registers(historyTree1, ErgoValue.of(holder1Pk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -757,7 +757,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue + feeValue) .tokens(new ErgoToken(note2TokenId, noteValue)) .registers(historyTree2, ErgoValue.of(holder2Pk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId2, fakeIndex) .withContextVars( @@ -771,7 +771,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserve1NFTBytes, 1)) .registers(ErgoValue.of(holder1Pk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId3, fakeIndex) .withContextVars( @@ -790,7 +790,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserve2NFTBytes, 1)) .registers(ErgoValue.of(holder2Pk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId4, fakeIndex) .withContextVars( @@ -815,28 +815,28 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .convertToInputWith(fakeTxId3, fakeIndex) val reserve1Output = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue, registers = Array(ErgoValue.of(holder1Pk)), tokens = Array(new ErgoToken(reserve1NFT, 1)) ) val reserve2Output = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue, registers = Array(ErgoValue.of(holder2Pk)), tokens = Array(new ErgoToken(reserve2NFT, 1)) ) val receipt1Output = createOut( - Constants.receiptContract, + HistoricalContractFixtures.receiptContract, minValue, registers = Array(historyTree1, ErgoValue.of(0L), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(holder1Pk)), tokens = Array(new ErgoToken(note1TokenId, noteValue)) ) val receipt2Output = createOut( - Constants.receiptContract, + HistoricalContractFixtures.receiptContract, minValue, registers = Array(historyTree2, ErgoValue.of(0L), ErgoValue.of(ctx.getHeight - 5), ErgoValue.of(holder2Pk)), tokens = Array(new ErgoToken(note2TokenId, noteValue)) @@ -897,7 +897,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(firstNoteTokenId, firstNoteValue)) .registers(Constants.emptyTreeErgoValue, ErgoValue.of(holderPk), ErgoValue.of(firstPosition)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -915,7 +915,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(secondNoteTokenId, secondNoteValue)) .registers(Constants.emptyTreeErgoValue, ErgoValue.of(holderPk), ErgoValue.of(secondPosition)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.noteContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.noteContract)) .build() .convertToInputWith(fakeTxId1, fakeIndex) .withContextVars( @@ -942,33 +942,33 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1)) .registers(ErgoValue.of(holderPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId2, fakeIndex) val note1Output = createOut( - Constants.noteContract, + HistoricalContractFixtures.noteContract, minValue, registers = Array(outTree1, ErgoValue.of(holderPk), ErgoValue.of(firstPosition + 1)), tokens = Array(new ErgoToken(firstNoteTokenId, 50)) ) val note1Change = createOut( - Constants.noteContract, + HistoricalContractFixtures.noteContract, minValue, registers = Array(outTree1, ErgoValue.of(holderPk), ErgoValue.of(firstPosition + 1)), tokens = Array(new ErgoToken(firstNoteTokenId, 5)) ) val note2Output = createOut( - Constants.noteContract, + HistoricalContractFixtures.noteContract, minValue, registers = Array(outTree2, ErgoValue.of(holderPk), ErgoValue.of(secondPosition + 1)), tokens = Array(new ErgoToken(secondNoteTokenId, 50)) ) val note2Change = createOut( - Constants.noteContract, + HistoricalContractFixtures.noteContract, minValue, registers = Array(outTree2, ErgoValue.of(holderPk), ErgoValue.of(secondPosition + 1)), tokens = Array(new ErgoToken(secondNoteTokenId, 10)) @@ -1001,7 +1001,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .value(minValue) .tokens(new ErgoToken(reserveNFTBytes, 1)) .registers(ErgoValue.of(holderPk)) - .contract(ctx.compileContract(ConstantsBuilder.empty(), Constants.reserveContract)) + .contract(ctx.compileContract(ConstantsBuilder.empty(), HistoricalContractFixtures.reserveContract)) .build() .convertToInputWith(fakeTxId2, fakeIndex) .withContextVars( @@ -1018,7 +1018,7 @@ class ChainCashSpec extends PropSpec with Matchers with ScalaCheckDrivenProperty .convertToInputWith(fakeTxId1, fakeIndex) val reserveOutput = createOut( - Constants.reserveContract, + HistoricalContractFixtures.reserveContract, minValue, registers = Array(ErgoValue.of(holderPk), ErgoValue.of(ctx.getHeight - 2)), tokens = Array(new ErgoToken(reserveNFT, 1)) diff --git a/src/test/scala/chaincash/HistoricalContractFixtures.scala b/src/test/scala/chaincash/HistoricalContractFixtures.scala new file mode 100644 index 0000000..8efcc6e --- /dev/null +++ b/src/test/scala/chaincash/HistoricalContractFixtures.scala @@ -0,0 +1,99 @@ +package chaincash + +import chaincash.contracts.Constants +import scorex.crypto.hash.Blake2b256 +import scorex.util.encode.Base58 + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +/** + * Exact source fixtures for the retired ChainCash prototypes and Basis v1. + * + * These sources are loaded only from the test classpath. Production code must + * not expose their contract text, ErgoTrees, addresses, or transaction builders. + */ +private[chaincash] object HistoricalContractFixtures { + + val expectedCanonicalLfSha256: Map[String, String] = Map( + "onchain/reserve.es" -> "3f39c13879748230cd08ee10d9970370506509e966229b735af77486843fa335", + "onchain/receipt.es" -> "69c24be8426e7c7c8fdf77ab248e4549b8cc9832122aed010a54e304357869ea", + "onchain/note.es" -> "5a145dfa334efa60a26fe25ed136611992f207807ecb628c4c8ec196d940df16", + "layer2-old/reserve.es" -> "026c29b169f39f75a262590388364aea926a270c6fa78511199c1ae1512336e4", + "layer2-old/redemption.es" -> "b87614ec6aacbed34a0bf7950629d505cc6f204d1b4e7db52c5b43a133e122c6", + "layer2-old/redproducer.es" -> "9e9842716a49d631bc67ecf201da42c020d441e3329b9fd7778ea14c9dcc8351", + "layer2-old/note.es" -> "797b69c823ff392d5a941cbd02b9297341c5926c2ce721ab6735f71f3c181e1b", + "offchain/basis.es" -> "56f6d229207469a45764c2a9657c5b37d6c5faf9025a9d3fc0636f1dd6120823", + "offchain/basis-token.es" -> "f3d8d4b5e8677409b82c49a396b5c68487d1a043963cee25558b368aa225961d" + ) + + private[chaincash] def canonicalLfBytes(bytes: Array[Byte]): Array[Byte] = + new String(bytes, StandardCharsets.UTF_8) + .replace("\r\n", "\n") + .getBytes(StandardCharsets.UTF_8) + + private[chaincash] def canonicalLfSha256(bytes: Array[Byte]): String = + MessageDigest.getInstance("SHA-256") + .digest(canonicalLfBytes(bytes)) + .map(byte => f"${byte & 0xff}%02x") + .mkString + + def archivedSource(relativePath: String): String = { + val resourcePath = s"/contracts/historical/$relativePath" + val stream = Option(getClass.getResourceAsStream(resourcePath)) + .getOrElse(throw new IllegalStateException(s"Missing historical fixture: $resourcePath")) + val bytes = try { + val output = new java.io.ByteArrayOutputStream() + val buffer = new Array[Byte](8192) + var count = stream.read(buffer) + while (count != -1) { + output.write(buffer, 0, count) + count = stream.read(buffer) + } + output.toByteArray + } finally { + stream.close() + } + + val expected = expectedCanonicalLfSha256.getOrElse( + relativePath, + throw new IllegalArgumentException(s"Unregistered historical fixture: $relativePath") + ) + val canonicalBytes = canonicalLfBytes(bytes) + val actual = canonicalLfSha256(bytes) + require(actual == expected, s"Historical fixture digest mismatch for $relativePath") + + new String(canonicalBytes, StandardCharsets.UTF_8) + .stripSuffix("\n") + } + + private def substitute(contract: String, values: Map[String, String]): String = + values.foldLeft(contract) { case (source, (name, value)) => + source.replace("$" + name, value) + } + + lazy val reserveContract: String = archivedSource("onchain/reserve.es") + private lazy val reserveErgoTree = Constants.compile(reserveContract) + private lazy val reserveContractHash = Base58.encode(Blake2b256(reserveErgoTree.bytes.tail)) + + lazy val receiptContract: String = substitute( + archivedSource("onchain/receipt.es"), + Map("reserveContractHash" -> reserveContractHash) + ) + private lazy val receiptErgoTree = Constants.compile(receiptContract) + private lazy val receiptContractHash = Base58.encode(Blake2b256(receiptErgoTree.bytes.tail)) + + lazy val noteContract: String = substitute( + archivedSource("onchain/note.es"), + Map( + "reserveContractHash" -> reserveContractHash, + "receiptContractHash" -> receiptContractHash + ) + ) + + lazy val basisContract: String = archivedSource("offchain/basis.es") + lazy val basisErgoTree = Constants.compile(basisContract) + + lazy val basisTokenContract: String = archivedSource("offchain/basis-token.es") + lazy val basisTokenErgoTree = Constants.compile(basisTokenContract) +} diff --git a/src/test/scala/chaincash/LegacyContractRetirementSpec.scala b/src/test/scala/chaincash/LegacyContractRetirementSpec.scala new file mode 100644 index 0000000..0343ab4 --- /dev/null +++ b/src/test/scala/chaincash/LegacyContractRetirementSpec.scala @@ -0,0 +1,120 @@ +package chaincash + +import chaincash.contracts.{Constants, Printer} +import org.scalatest.{Matchers, PropSpec} + +import java.io.{ByteArrayOutputStream, PrintStream} +import java.nio.charset.StandardCharsets +import scala.util.Try + +class LegacyContractRetirementSpec extends PropSpec with Matchers { + + property("production Constants exports only the reviewed Basis v2 contract family") { + Constants.publishedContracts shouldEqual Vector( + "Basis v2" -> Constants.basisV2Address, + "Basis-token v2" -> Constants.basisTokenV2Address + ) + + val exportedMethods = Constants.getClass.getMethods.map(_.getName).toSet + val retiredGetters = Set( + "readContract", + "basisContract", + "basisErgoTree", + "basisAddress", + "basisTokenContract", + "basisTokenErgoTree", + "basisTokenAddress", + "basisPlasmaParameters", + "reserveContract", + "reserveContractHash", + "reserveContractHashString", + "reserveErgoTree", + "reserveAddress", + "receiptContract", + "receiptContractHash", + "receiptContractHashString", + "receiptErgoTree", + "receiptAddress", + "noteContract", + "noteErgoTree", + "noteAddress", + "redemptionContract", + "redemptionErgoTree", + "redemptionAddress", + "redemptionProducerContract", + "redemptionProducerErgoTree", + "redemptionProducerAddress" + ) + + exportedMethods.intersect(retiredGetters) shouldBe empty + } + + property("production classpath has no legacy address printer or transaction builders") { + val retiredClasses = Seq( + "chaincash.contracts.ContractsPrinter$", + "chaincash.contracts.BasisDeployer$", + "chaincash.contracts.BasisConstants$", + "chaincash.contracts.BasisNoteCreator$", + "chaincash.offchain.NoteUtils", + "chaincash.offchain.ReserveUtils", + "chaincash.offchain.Tester$" + ) + + retiredClasses.foreach { className => + withClue(className) { + Try(Class.forName(className)).isFailure shouldBe true + } + } + } + + property("production printer emits exactly the two published Basis v2 addresses") { + val bytes = new ByteArrayOutputStream() + val stream = new PrintStream(bytes, true, StandardCharsets.UTF_8.name()) + try { + Console.withOut(stream) { + Printer.main(Array.empty) + } + } finally { + stream.close() + } + + val outputLines = new String(bytes.toByteArray, StandardCharsets.UTF_8) + .split("\\R") + .map(_.trim) + .filter(_.nonEmpty) + .toVector + + outputLines shouldEqual Vector( + s"Basis v2 p2s address: ${Constants.basisV2Address}", + s"Basis-token v2 p2s address: ${Constants.basisTokenV2Address}" + ) + } + + property("retired Basis v1 sources exist only on the test classpath") { + val projectRoot = java.nio.file.Paths.get(sys.props("user.dir")) + java.nio.file.Files.exists(projectRoot.resolve("contracts/offchain/basis.es")) shouldBe false + java.nio.file.Files.exists(projectRoot.resolve("contracts/offchain/basis-token.es")) shouldBe false + + HistoricalContractFixtures.basisContract should not be empty + HistoricalContractFixtures.basisTokenContract should not be empty + } + + property("historical contract sources retain their pinned canonical-LF bytes") { + HistoricalContractFixtures.expectedCanonicalLfSha256.keys.foreach { path => + withClue(path) { + HistoricalContractFixtures.archivedSource(path) should not be empty + } + } + } + + property("historical source identity is equal for LF and CRLF checkouts") { + val lf = "first line\nsecond line\n".getBytes(StandardCharsets.UTF_8) + val crlf = "first line\r\nsecond line\r\n".getBytes(StandardCharsets.UTF_8) + val changed = "first line\nchanged line\n".getBytes(StandardCharsets.UTF_8) + + HistoricalContractFixtures.canonicalLfSha256(crlf) shouldEqual + HistoricalContractFixtures.canonicalLfSha256(lf) + HistoricalContractFixtures.canonicalLfSha256(changed) should not equal + HistoricalContractFixtures.canonicalLfSha256(lf) + } +} diff --git a/src/test/scala/chaincash/contracts/BasisDeployerSpec.scala b/src/test/scala/chaincash/contracts/BasisDeployerSpec.scala index dc1156e..5e7ec7e 100644 --- a/src/test/scala/chaincash/contracts/BasisDeployerSpec.scala +++ b/src/test/scala/chaincash/contracts/BasisDeployerSpec.scala @@ -1,45 +1,44 @@ package chaincash.contracts +import chaincash.HistoricalContractFixtures import org.scalatest.{Matchers, PropSpec} -import sigma.ast.GroupElementConstant -import sigma.crypto.CryptoConstants +import java.nio.file.{Files, Paths} +import scala.util.Try + +/** Regression boundary for the retired Basis v1 deployment helper. */ class BasisDeployerSpec extends PropSpec with Matchers { - property("BasisDeployer should compile Basis contract successfully") { - // This test verifies that the Basis contract can be compiled - val basisContract = Constants.readContract("offchain/basis.es", Map.empty) - basisContract should not be empty + property("historical Basis v1 source remains compilable only through the test fixture") { + val historicalSource = HistoricalContractFixtures.basisContract + historicalSource should not be empty - val basisErgoTree = Constants.compile(basisContract) - basisErgoTree should not be null + val historicalTree = Constants.compile(historicalSource) + historicalTree.bytes shouldEqual HistoricalContractFixtures.basisErgoTree.bytes - val basisAddress = Constants.getAddressFromErgoTree(basisErgoTree) - // Address should be a valid mainnet P2S address, round-trippable via the encoder - basisAddress shouldBe a [org.ergoplatform.Pay2SAddress] - Constants.ergoAddressEncoder.fromString(basisAddress.toString).get shouldEqual basisAddress + val historicalAddress = Constants.getAddressFromErgoTree(historicalTree) + historicalAddress shouldBe a [org.ergoplatform.Pay2SAddress] + Constants.ergoAddressEncoder.fromString(historicalAddress.toString).get shouldEqual historicalAddress } - property("BasisDeployer should create valid deployment request") (pending) - - property("BasisDeployer should create valid scan request") { - val exampleReserveTokenId = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" - - val scanRequest = BasisDeployer.createBasisScanRequest(exampleReserveTokenId) - - scanRequest should include("scanName") - scanRequest should include("Basis Reserve") - scanRequest should include(exampleReserveTokenId) - scanRequest should include("containsAsset") + property("production Basis v1 deployment and note builders are absent") { + Seq( + "chaincash.contracts.BasisDeployer$", + "chaincash.contracts.BasisConstants$", + "chaincash.contracts.BasisNoteCreator$" + ).foreach { className => + withClue(className) { + Try(Class.forName(className)).isFailure shouldBe true + } + } } - property("BasisDeployer should have correct constants") { - BasisConstants.REDEEM_ACTION shouldBe 0 - BasisConstants.TOP_UP_ACTION shouldBe 1 - BasisConstants.INITIATE_REFUND_ACTION shouldBe 2 - BasisConstants.COMPLETE_REFUND_ACTION shouldBe 3 - BasisConstants.MIN_TOP_UP_AMOUNT shouldBe 100000000L // 0.1 ERG - BasisConstants.EMERGENCY_REDEMPTION_TIME_IN_BLOCKS shouldBe 2160 // 3 days in blocks (3 * 720) - BasisConstants.REFUND_PERIOD_BLOCKS shouldBe 43200 // 2 months in blocks (60 * 720) + property("Basis v1 ErgoScript has no production source path") { + val projectRoot = Paths.get(sys.props("user.dir")) + Files.exists(projectRoot.resolve("contracts/offchain/basis.es")) shouldBe false + Files.exists(projectRoot.resolve("contracts/offchain/basis-token.es")) shouldBe false + + HistoricalContractFixtures.basisContract should not be empty + HistoricalContractFixtures.basisTokenContract should not be empty } -} \ No newline at end of file +} diff --git a/src/test/scala/chaincash/contracts/ParticipantKeys.scala b/src/test/scala/chaincash/contracts/ParticipantKeys.scala new file mode 100644 index 0000000..82d0cc9 --- /dev/null +++ b/src/test/scala/chaincash/contracts/ParticipantKeys.scala @@ -0,0 +1,58 @@ +package chaincash.contracts + +import scorex.crypto.encode.Base16 +import sigma.GroupElement + +/** Test-only keys retained for historical fixtures that require local secrets. */ +private[chaincash] object ParticipantKeys { + private val secrets = ParticipantSecretsReader.readSecrets() + + private def getSecret(name: String): BigInt = + secrets.get(name) match { + case Some(participant) => BigInt(participant.secretHex, 16) + case None => + throw new IllegalArgumentException( + s"Secret for '$name' not found in secrets file. " + + s"Available participants: ${secrets.keys.mkString(", ")}" + ) + } + + private def getAddress(name: String): String = + secrets.get(name) match { + case Some(participant) => participant.address + case None => + throw new IllegalArgumentException( + s"Address for '$name' not found in secrets file. " + + s"Available participants: ${secrets.keys.mkString(", ")}" + ) + } + + val trackerAddress: String = getAddress("tracker") + val trackerSecret: BigInt = getSecret("tracker") + require( + AddressUtils.verifySecretMatchesAddress(trackerAddress, trackerSecret), + s"Tracker's secret does not correspond to address $trackerAddress" + ) + + val aliceAddress: String = getAddress("alice") + val aliceSecret: BigInt = getSecret("alice") + require( + AddressUtils.verifySecretMatchesAddress(aliceAddress, aliceSecret), + s"Alice's secret does not correspond to address $aliceAddress" + ) + + val bobAddress: String = getAddress("bob") + val bobSecret: BigInt = getSecret("bob") + + lazy val trackerPublicKey: GroupElement = + AddressUtils.derivePublicKeyFromAddress(trackerAddress) + lazy val alicePublicKey: GroupElement = + AddressUtils.derivePublicKeyFromAddress(aliceAddress) + lazy val bobPublicKey: GroupElement = + AddressUtils.derivePublicKeyFromAddress(bobAddress) + lazy val bobErgoTree: String = AddressUtils.deriveErgoTreeFromAddress(bobAddress) + + lazy val trackerPublicKeyHex: String = Base16.encode(trackerPublicKey.getEncoded.toArray) + lazy val alicePublicKeyHex: String = Base16.encode(alicePublicKey.getEncoded.toArray) + lazy val bobPublicKeyHex: String = Base16.encode(bobPublicKey.getEncoded.toArray) +} diff --git a/src/main/scala/chaincash/contracts/ParticipantSecretsReader.scala b/src/test/scala/chaincash/contracts/ParticipantSecretsReader.scala similarity index 100% rename from src/main/scala/chaincash/contracts/ParticipantSecretsReader.scala rename to src/test/scala/chaincash/contracts/ParticipantSecretsReader.scala