├── project ├── build.properties └── plugins.sbt ├── docs ├── conf │ ├── conf.pdf │ ├── compile.sh │ ├── .gitignore │ └── sources.bib ├── whitepaper │ ├── chaincash.pdf │ ├── compile.sh │ ├── sources.bib │ ├── .gitignore │ └── chaincash.tex ├── miw.md ├── silvercents.md ├── server.md ├── abstract.md ├── lets.md └── l2.md ├── contracts ├── offchain │ ├── README.md │ ├── apps │ │ └── gitcircles.md │ ├── basis.es │ └── basis.md ├── onchain │ ├── receipt.es │ ├── reserve.es │ └── note.es └── layer2-old │ ├── redproducer.es │ ├── note.es │ ├── reserve.es │ └── redemption.es ├── src ├── main │ └── scala │ │ ├── gp │ │ ├── TransferTransaction.scala │ │ ├── RedeemTransaction.scala │ │ └── MintTransaction.scala │ │ └── chaincash │ │ ├── offchain │ │ ├── Tester.scala │ │ ├── HttpUtils.scala │ │ ├── SigUtils.scala │ │ ├── TrackingTypes.scala │ │ ├── NotePredicate.scala │ │ ├── WalletUtils.scala │ │ ├── ReserveUtils.scala │ │ ├── TrackingUtils.scala │ │ ├── server │ │ │ └── model.scala │ │ ├── DbEntities.scala │ │ └── NoteUtils.scala │ │ └── contracts │ │ ├── ContractsPrinter.scala │ │ ├── README.md │ │ ├── Constants.scala │ │ └── BasisDeployer.scala └── test │ ├── resources │ ├── mockwebserver │ │ ├── node_responses │ │ │ ├── response_Box1.json │ │ │ ├── response_Box2.json │ │ │ ├── response_Box3.json │ │ │ ├── E1 │ │ │ │ └── response_Box1.json │ │ │ ├── response_NodeInfo.json │ │ │ └── response_LastHeaders.json │ │ └── explorer_responses │ │ │ ├── E1 │ │ │ └── response_boxesByAddressUnspent.json │ │ │ └── response_boxesByAddressUnspent.json │ ├── asset-issue.json │ ├── timestamp.json │ ├── token-filter.json │ └── dummy-program.json │ ├── java │ └── dexy │ │ ├── MockedErgoClient.java │ │ └── FileMockedErgoClient.java │ └── scala │ └── chaincash │ ├── HttpClientTesting.scala │ └── contracts │ └── BasisDeployerSpec.scala ├── .gitignore ├── README.md ├── LICENSE └── AGENTS.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.5.2 2 | -------------------------------------------------------------------------------- /docs/conf/conf.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterMoneyLabs/chaincash/HEAD/docs/conf/conf.pdf -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9") 4 | -------------------------------------------------------------------------------- /docs/whitepaper/chaincash.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterMoneyLabs/chaincash/HEAD/docs/whitepaper/chaincash.pdf -------------------------------------------------------------------------------- /contracts/offchain/README.md: -------------------------------------------------------------------------------- 1 | Different ChainCash variants for offchain applications. 2 | 3 | 4 | In most cases, reserves are on-chain, notes are created and making progress offchain. 5 | -------------------------------------------------------------------------------- /src/main/scala/gp/TransferTransaction.scala: -------------------------------------------------------------------------------- 1 | package gp 2 | 3 | // Transfer is like on-chain UTXO transaction, multiple possible inputs and outputs, and only inputs signed 4 | case class TransferTransaction(inputs: Seq[Note], outputs: Seq[Note]) 5 | -------------------------------------------------------------------------------- /src/main/scala/gp/RedeemTransaction.scala: -------------------------------------------------------------------------------- 1 | package gp 2 | 3 | // redeem is special kind of transaction, with a single input and single output, 4 | // and output being signed along with the input 5 | case class RedeemTransaction(input: Note, output: Note) 6 | -------------------------------------------------------------------------------- /docs/conf/compile.sh: -------------------------------------------------------------------------------- 1 | rm conf.aux 2 | rm conf.out 3 | rm conf.log 4 | rm conf.bbl 5 | rm conf.blg 6 | pdflatex conf 7 | bibtex conf 8 | pdflatex conf 9 | pdflatex conf 10 | rm conf.aux 11 | rm conf.out 12 | rm conf.log 13 | rm conf.bbl 14 | rm conf.blg 15 | -------------------------------------------------------------------------------- /docs/whitepaper/compile.sh: -------------------------------------------------------------------------------- 1 | rm chaincash.aux 2 | rm chaincash.out 3 | rm chaincash.log 4 | rm chaincash.bbl 5 | rm chaincash.blg 6 | pdflatex chaincash 7 | bibtex chaincash 8 | pdflatex chaincash 9 | pdflatex chaincash 10 | rm chaincash.aux 11 | rm chaincash.out 12 | rm chaincash.log 13 | rm chaincash.bbl 14 | rm chaincash.blg 15 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/Tester.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | import sigmastate.eval.CGroupElement 3 | 4 | object Tester extends App with TrackingUtils with NoteUtils { 5 | override val serverUrl: String = "http://127.0.0.1:9053" 6 | 7 | println(fetchNodeHeight()) 8 | 9 | processBlocks() 10 | 11 | println("my balance: " + myBalance()) 12 | 13 | sendNote(DbEntities.unspentNotes.head.get._2, CGroupElement(myPoint)) 14 | 15 | } -------------------------------------------------------------------------------- /src/main/scala/gp/MintTransaction.scala: -------------------------------------------------------------------------------- 1 | package gp 2 | 3 | case class Note(noteId: Array[Byte], amount: Long, ownerPubKey: Array[Byte]) 4 | 5 | // only single output, signed by trusted Git oracle 6 | // in this output, noteId is commit id, amount is number of lines of code, ownerPubKey is pubkey of Github account 7 | case class MintTransaction(output: Note) 8 | 9 | object MintTransaction { 10 | val OraclePubKey: Array[Byte] = Array.fill(33)(0) 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/node_responses/response_Box1.json: -------------------------------------------------------------------------------- 1 | { 2 | "boxId": "d47f958b201dc7162f641f7eb055e9fa7a9cb65cc24d4447a10f86675fc58328", 3 | "value": 1000000, 4 | "ergoTree": "0008cd036ba5cfbc03ea2471fdf02737f64dbcd58c34461a7ec1e586dcd713dacbf89a12", 5 | "creationHeight": 123142, 6 | "assets": [], 7 | "additionalRegisters": {}, 8 | "transactionId": "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b809", 9 | "index": 0 10 | } -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/node_responses/response_Box2.json: -------------------------------------------------------------------------------- 1 | { 2 | "boxId": "e050a3af38241ce444c34eb25c0ab880674fc23a0e63632633ae14f547141c37", 3 | "value": 1000000, 4 | "ergoTree": "0008cd036ba5cfbc03ea2471fdf02737f64dbcd58c34461a7ec1e586dcd713dacbf89a12", 5 | "creationHeight": 123135, 6 | "assets": [], 7 | "additionalRegisters": {}, 8 | "transactionId": "7fa034a55739a054c62fbf0ea6ae31a33b86d66073f018ff1607ff7fa7c73fef", 9 | "index": 0 10 | } -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/node_responses/response_Box3.json: -------------------------------------------------------------------------------- 1 | { 2 | "boxId": "26d6e08027e005270b38e5c5f4a73ffdb6d65a3289efb51ac37f98ad395d887c", 3 | "value": 10000000000, 4 | "ergoTree": "0008cd036ba5cfbc03ea2471fdf02737f64dbcd58c34461a7ec1e586dcd713dacbf89a12", 5 | "creationHeight": 113335, 6 | "assets": [], 7 | "additionalRegisters": {}, 8 | "transactionId": "5e1d74b6c9c459c499620a2059b484f64eb7465e7310faac863f5728c5958805", 9 | "index": 0 10 | } -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/node_responses/E1/response_Box1.json: -------------------------------------------------------------------------------- 1 | { 2 | "boxId": "976c21fbd859cca4113a0711706c33b3d1f1052958a0196d61480f4b950fdf91", 3 | "value": 4981000000, 4 | "ergoTree": "0008cd02472963123ce32c057907c7a7268bc09f45d9ca57819d3327b9e7497d7b1cc347", 5 | "creationHeight": 130509, 6 | "assets": [], 7 | "additionalRegisters": {}, 8 | "transactionId": "c5710af17f5124a232a5ef731fdf94a493025334c2a7d5a79e9923210972b962", 9 | "index": 2 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/explorer_responses/E1/response_boxesByAddressUnspent.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "976c21fbd859cca4113a0711706c33b3d1f1052958a0196d61480f4b950fdf91", 4 | "value": 4981000000, 5 | "creationHeight": 130509, 6 | "ergoTree": "0008cd02472963123ce32c057907c7a7268bc09f45d9ca57819d3327b9e7497d7b1cc347", 7 | "address": "9f4QF8AD1nQ3nJahQVkMj8hFSVVzVom77b52JU7EW71Zexg6N8v", 8 | "assets": [], 9 | "additionalRegisters": {}, 10 | "mainChain": true 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE/Editor files 2 | .idea 3 | .ensime 4 | .ensime_cache/ 5 | scorex.yaml 6 | .bsp 7 | 8 | # swaydb files 9 | db/ 10 | secrets/ 11 | 12 | # scala build folders 13 | target 14 | 15 | # standalone docker 16 | !Dockerfile 17 | 18 | # logs 19 | *.log 20 | *.log.gz 21 | *.tmp 22 | 23 | # binary files 24 | *.gz 25 | 26 | # Core latex/pdflatex auxiliary files: 27 | *.aux 28 | *.lof 29 | *.lot 30 | *.fls 31 | *.out 32 | *.toc 33 | *.fmt 34 | *.fot 35 | *.cb 36 | *.cb2 37 | *.bbl 38 | *.blg 39 | 40 | # osx files 41 | .DS_Store 42 | 43 | # metals, bloop, vscode 44 | .bloop/ 45 | .metals/ 46 | .vscode/ 47 | database/ 48 | project/metals.sbt 49 | 50 | # scala worksheets 51 | *.worksheet.sc 52 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/HttpUtils.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import scalaj.http.{Http, HttpOptions} 4 | 5 | trait HttpUtils { 6 | val ApiKey = "hello" //todo: externalize 7 | def getJsonAsString(url: String): String = { 8 | Http(s"$url") 9 | .header("Content-Type", "application/json") 10 | .header("Accept", "application/json") 11 | .header("Charset", "UTF-8") 12 | .header("api_key", ApiKey) 13 | .option(HttpOptions.readTimeout(10000)) 14 | .asString 15 | .body 16 | } 17 | 18 | def postString(url: String, data: String): String = { 19 | Http(s"$url") 20 | .header("Content-Type", "application/json") 21 | .header("Accept", "application/json") 22 | .header("Charset", "UTF-8") 23 | .header("api_key", ApiKey) 24 | .option(HttpOptions.readTimeout(10000)) 25 | .postData(data) 26 | .asString 27 | .body 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/test/java/dexy/MockedErgoClient.java: -------------------------------------------------------------------------------- 1 | package org.ergoplatform.appkit; 2 | 3 | import java.util.List; 4 | import org.ergoplatform.appkit.ErgoClient; 5 | 6 | /** 7 | * This interface is used to represent {@link ErgoClient} whose communication with 8 | * Ergo network node and explorer can be mocked with some pre-defined test data. 9 | * This interface can be implemented in different ways, depending on the source 10 | * of the test data. 11 | * This interface allows to abstract testing code from a concrete decision of how 12 | * to provide the test data. 13 | */ 14 | public interface MockedErgoClient extends ErgoClient { 15 | /** 16 | * Response content for mocked responses from Ergo node REST API. 17 | */ 18 | List getNodeResponses(); 19 | 20 | /** 21 | * Response content for mocked responses from Ergo Explorer REST API. 22 | */ 23 | List getExplorerResponses(); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/SigUtils.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import sigmastate.basics.CryptoConstants 4 | import special.sigma.GroupElement 5 | import sigmastate.eval._ 6 | import sigmastate.basics.SecP256K1Group 7 | import java.security.SecureRandom 8 | import scala.annotation.tailrec 9 | 10 | object SigUtils { 11 | 12 | def randBigInt: BigInt = { 13 | val random = new SecureRandom() 14 | val values = new Array[Byte](32) 15 | random.nextBytes(values) 16 | BigInt(values).mod(SecP256K1Group.q) 17 | } 18 | 19 | @tailrec 20 | def sign(msg: Array[Byte], secretKey: BigInt): (GroupElement, BigInt) = { 21 | val g: GroupElement = CryptoConstants.dlogGroup.generator 22 | 23 | val pk = g.exp(secretKey.bigInteger) 24 | 25 | val r = randBigInt 26 | val a: GroupElement = g.exp(r.bigInteger) 27 | val e = scorex.crypto.hash.Blake2b256(a.getEncoded.toArray ++ msg ++ pk.getEncoded.toArray) 28 | val z = (r + secretKey * BigInt(e)) % CryptoConstants.groupOrder 29 | 30 | if(z.bitLength <= 255) { 31 | (a, z) 32 | } else { 33 | sign(msg,secretKey) 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/test/resources/asset-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "constants": [ 3 | { 4 | "name": "myAddress", 5 | "type": "Address", 6 | "value": "9hz1B19M44TNpmVe8MS4xvXyycehh5uP5aCfj4a6iAowj88hkd2" 7 | }, 8 | { 9 | "name": "numTokensToIssue", 10 | "type": "Long", 11 | "value": "10" 12 | }, 13 | { 14 | "name": "minStorageRent", 15 | "type": "Long", 16 | "value": "1000000" 17 | } 18 | ], 19 | "inputs": [ 20 | { 21 | "id": { 22 | "name": "myBoxId" 23 | }, 24 | "address": { 25 | "value": "myAddress" 26 | }, 27 | "nanoErgs": { 28 | "name": "inputNanoErgs" 29 | } 30 | } 31 | ], 32 | "outputs": [ 33 | { 34 | "address": { 35 | "value": "myAddress" 36 | }, 37 | "tokens": [ 38 | { 39 | "index": 0, 40 | "id": { 41 | "value": "myBoxId" 42 | }, 43 | "amount": { 44 | "value": "numTokensToIssue" 45 | } 46 | } 47 | ], 48 | "nanoErgs": { 49 | "value": "minStorageRent" 50 | } 51 | } 52 | ], 53 | "fee": 1000000 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/explorer_responses/response_boxesByAddressUnspent.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "d47f958b201dc7162f641f7eb055e9fa7a9cb65cc24d4447a10f86675fc58328", 4 | "value": 1000000, 5 | "creationHeight": 123142, 6 | "ergoTree": "0008cd036ba5cfbc03ea2471fdf02737f64dbcd58c34461a7ec1e586dcd713dacbf89a12", 7 | "address": "9hHDQb26AjnJUXxcqriqY1mnhpLuUeC81C4pggtK7tupr92Ea1K", 8 | "assets": [], 9 | "additionalRegisters": {}, 10 | "mainChain": true 11 | }, 12 | { 13 | "id": "e050a3af38241ce444c34eb25c0ab880674fc23a0e63632633ae14f547141c37", 14 | "value": 1000000, 15 | "creationHeight": 123135, 16 | "ergoTree": "0008cd036ba5cfbc03ea2471fdf02737f64dbcd58c34461a7ec1e586dcd713dacbf89a12", 17 | "address": "9hHDQb26AjnJUXxcqriqY1mnhpLuUeC81C4pggtK7tupr92Ea1K", 18 | "assets": [], 19 | "additionalRegisters": {}, 20 | "mainChain": true 21 | }, 22 | { 23 | "id": "26d6e08027e005270b38e5c5f4a73ffdb6d65a3289efb51ac37f98ad395d887c", 24 | "value": 10000000000, 25 | "creationHeight": 113335, 26 | "ergoTree": "0008cd036ba5cfbc03ea2471fdf02737f64dbcd58c34461a7ec1e586dcd713dacbf89a12", 27 | "address": "9hHDQb26AjnJUXxcqriqY1mnhpLuUeC81C4pggtK7tupr92Ea1K", 28 | "assets": [], 29 | "additionalRegisters": {}, 30 | "mainChain": true 31 | } 32 | ] -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/node_responses/response_NodeInfo.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "mainnet-seed-node-frankfurt", 4 | "network": "mainnet", 5 | "appVersion": "3.1.4", 6 | "fullHeight": 123414, 7 | "headersHeight": 123414, 8 | "bestFullHeaderId": "8d87fd4a7372877462ff7ecb52a6063207ffda2689f4de3254cc9a2877953f4f", 9 | "previousFullHeaderId": "0895a4a3dddbfd8faee6d9f957c42b64ee328714221809ad20d8fa4d036e1bf9", 10 | "bestHeaderId": "8d87fd4a7372877462ff7ecb52a6063207ffda2689f4de3254cc9a2877953f4f", 11 | "stateRoot": "25ff1da6e5bf14304196c19461a4bd24f8ce6c493153baea3c3d6efe17d4bda814", 12 | "stateType": "utxo", 13 | "stateVersion": "8d87fd4a7372877462ff7ecb52a6063207ffda2689f4de3254cc9a2877953f4f", 14 | "isMining": false, 15 | "peersCount": 30, 16 | "unconfirmedCount": 2, 17 | "difficulty": 128274315345920, 18 | "currentTime": 1576789157261, 19 | "launchTime": 1576504334661, 20 | "headersScore": 17353496201374203904, 21 | "fullBlocksScore": 17353496201374203904, 22 | "genesisBlockId": "b0244dfc267baca974a4caee06120321562784303a8a688976ae56170e4d175b", 23 | "parameters": { 24 | "height": 122880, 25 | "storageFeeFactor": 1250000, 26 | "minValuePerByte": 360, 27 | "maxBlockSize": 524288, 28 | "maxBlockCost": 1000000, 29 | "blockVersion": 1, 30 | "tokenAccessCost": 100, 31 | "inputCost": 2000, 32 | "dataInputCost": 100, 33 | "outputCost": 100 34 | } 35 | } -------------------------------------------------------------------------------- /contracts/onchain/receipt.es: -------------------------------------------------------------------------------- 1 | { 2 | // receipt contract 3 | // it is possible to spend this box 3 years after, with tokens being necessarily burnt 4 | // it protects from storage rent taking tokens 5 | 6 | // registers: 7 | // R4 - AvlTree - history of ownership for corresponding redeemed note 8 | // R5 - Long - redeemed position 9 | // R6 - approx. height when this box was created 10 | // R7 - redeemer PK 11 | 12 | def noTokens(b: Box) = b.tokens.size == 0 13 | val noTokensInOutputs = OUTPUTS.forall(noTokens) 14 | 15 | val creationHeight = SELF.R6[Int].get 16 | val burnPeriod = 788400 // 3 years 17 | 18 | val burnDone = (HEIGHT > creationHeight + burnPeriod) && noTokensInOutputs 19 | 20 | // we check that the receipt is spent along with a reserve contract box. 21 | // for that, we fix reserve input position @ #1 22 | // we drop version byte during ergotrees comparison 23 | // signature of receipt holder is also required 24 | val reserveInputErgoTree = INPUTS(1).propositionBytes 25 | val treeHash = blake2b256(reserveInputErgoTree.slice(1, reserveInputErgoTree.size)) 26 | val reserveSpent = treeHash == fromBase58("$reserveContractHash") 27 | 28 | // we check receipt contract here, and other fields in reserve contract, see comments in reserve.es 29 | val receiptOutputErgoTree = OUTPUTS(1).propositionBytes 30 | val receiptCreated = receiptOutputErgoTree == SELF.propositionBytes 31 | val reRedemption = proveDlog(SELF.R7[GroupElement].get) && sigmaProp(reserveSpent && receiptCreated) 32 | 33 | burnDone || reRedemption 34 | } -------------------------------------------------------------------------------- /src/main/scala/chaincash/contracts/ContractsPrinter.scala: -------------------------------------------------------------------------------- 1 | package chaincash.contracts 2 | 3 | import Constants._ 4 | import scorex.util.encode.Base16 5 | import sigmastate.Values.ByteArrayConstant 6 | import sigmastate.serialization.ValueSerializer 7 | 8 | object ContractsPrinter extends App { 9 | 10 | println(s"Note contract address: $noteAddress") 11 | 12 | println(s"Receipt contract address: $receiptAddress") 13 | 14 | println(s"Reserve contract address: $reserveAddress") 15 | 16 | 17 | val noteScriptBa = ByteArrayConstant(noteErgoTree.bytes) 18 | val reserveScriptBa = ByteArrayConstant(reserveErgoTree.bytes) 19 | 20 | 21 | val noteContractTrackingRule = s""" 22 | |{ 23 | | "scanName": "Note tracker", 24 | | "walletInteraction": "off", 25 | | "removeOffchain": false, 26 | | "trackingRule": { 27 | | "predicate": "equals", 28 | | "value": "${Base16.encode(ValueSerializer.serialize(noteScriptBa))}" 29 | | } 30 | |} 31 | """.stripMargin 32 | 33 | val reserveContractTrackingRule = s""" 34 | | 35 | |{ 36 | | "scanName": "Reserve tracker", 37 | | "walletInteraction": "off", 38 | | "removeOffchain": false, 39 | | "trackingRule": { 40 | | "predicate": "equals", 41 | | "value": "${Base16.encode(ValueSerializer.serialize(reserveScriptBa))}" 42 | | } 43 | |} 44 | |""".stripMargin 45 | 46 | println("==========Note tracking rule================") 47 | println(noteContractTrackingRule) 48 | println("==========Reserve tracking rule==============") 49 | println(reserveContractTrackingRule) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/scala/chaincash/HttpClientTesting.scala: -------------------------------------------------------------------------------- 1 | package chaincash 2 | 3 | import org.ergoplatform.appkit.{FileMockedErgoClient, FileUtil} 4 | import org.ergoplatform.sdk.JavaHelpers._ 5 | 6 | import java.io.File 7 | import java.util.{List => JList} 8 | import java.lang.{String => JString} 9 | 10 | trait HttpClientTesting { 11 | val responsesDir = "src/test/resources/mockwebserver" 12 | val addr1 = "9f4QF8AD1nQ3nJahQVkMj8hFSVVzVom77b52JU7EW71Zexg6N8v" 13 | 14 | def loadNodeResponse(name: String) = { 15 | FileUtil.read(new File(s"$responsesDir/node_responses/$name")) 16 | } 17 | 18 | def loadExplorerResponse(name: String) = { 19 | FileUtil.read(new File(s"$responsesDir/explorer_responses/$name")) 20 | } 21 | 22 | case class MockData(nodeResponses: Seq[String] = Nil, explorerResponses: Seq[String] = Nil) { 23 | def appendNodeResponses(moreResponses: Seq[String]): MockData = { 24 | this.copy(nodeResponses = this.nodeResponses ++ moreResponses) 25 | } 26 | def appendExplorerResponses(moreResponses: Seq[String]): MockData = { 27 | this.copy(explorerResponses = this.explorerResponses ++ moreResponses) 28 | } 29 | } 30 | 31 | object MockData { 32 | def empty = MockData() 33 | } 34 | 35 | def createMockedErgoClient(data: MockData): FileMockedErgoClient = { 36 | val nodeResponses = IndexedSeq(loadNodeResponse("response_NodeInfo.json"), loadNodeResponse("response_LastHeaders.json")) ++ data.nodeResponses 37 | val explorerResponses: IndexedSeq[String] = data.explorerResponses.toIndexedSeq 38 | new FileMockedErgoClient(nodeResponses.convertTo[JList[JString]], explorerResponses.convertTo[JList[JString]]) 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/TrackingTypes.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import chaincash.contracts.Constants 4 | import org.ergoplatform.ErgoBox 5 | import org.ergoplatform.ErgoBox.R5 6 | import scorex.util.ModifierId 7 | import scorex.util.encode.Base16 8 | import sigmastate.Values.GroupElementConstant 9 | import sigmastate.eval.CGroupElement 10 | import sigmastate.basics.CryptoConstants.EcPointType 11 | import sigmastate.serialization.GroupElementSerializer 12 | import work.lithos.plasma.collections.PlasmaMap 13 | 14 | object TrackingTypes { 15 | 16 | type ReserveNftId = ModifierId 17 | type NoteTokenId = ModifierId 18 | type NoteId = ModifierId 19 | 20 | case class SigData(reserveId: ReserveNftId, valueBacked: Long, a: EcPointType, z: BigInt) 21 | 22 | case class NoteData(currentUtxo: ErgoBox, history: IndexedSeq[SigData]) { 23 | 24 | def holder: EcPointType = { 25 | currentUtxo.get(R5).get.asInstanceOf[GroupElementConstant].value.asInstanceOf[CGroupElement].wrappedValue 26 | } 27 | 28 | def restoreProver: PlasmaMap[Array[Byte], Array[Byte]] = { 29 | val keyvals = history.map{sigData => 30 | val reserveId = Base16.decode(sigData.reserveId).get 31 | val value = GroupElementSerializer.toBytes(sigData.a) ++ sigData.z.toByteArray 32 | reserveId -> value 33 | } 34 | val map = Constants.emptyPlasmaMap 35 | map.insert(keyvals :_*) 36 | map 37 | } 38 | } 39 | 40 | case class ReserveData(reserveBox: ErgoBox, 41 | signedUnspentNotes: IndexedSeq[NoteId], 42 | liabilites: Long) { 43 | def reserveNftId: ReserveNftId = ModifierId @@ Base16.encode(reserveBox.additionalTokens.toArray.head._1.toArray) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /contracts/layer2-old/redproducer.es: -------------------------------------------------------------------------------- 1 | { 2 | // contract which is producing redemption contract boxes 3 | // it holds redemption contract tokens and releasing it 4 | // one token can be released when the contract is invoked 5 | // if an output with the token bound to the redemption contract 6 | // 7 | // TOKENS 8 | // #0 - contract NFT 9 | // #1 - redemption request token 10 | 11 | val redeemScriptHash = fromBase58("") 12 | 13 | val selfOutput = OUTPUTS(0) 14 | val redemptionRequestOutput = OUTPUTS(1) 15 | 16 | val redemptionRequestTokenId = SELF.tokens(1)._1 17 | val redemptionRequestTokensInSelf = SELF.tokens(1)._2 18 | 19 | val selfOutCorrect = selfOutput.value >= SELF.value && selfOutput.tokens(0) == SELF.tokens(0) && 20 | selfOutput.tokens(1)._1 == redemptionRequestTokenId && 21 | selfOutput.tokens(1)._2 == redemptionRequestTokensInSelf - 1 22 | 23 | val redemptionTokenCorrect = redemptionRequestOutput.tokens(0)._1 == redemptionRequestTokenId && 24 | redemptionRequestOutput.tokens(0)._2 == 1 25 | 26 | val properCollateral = redemptionRequestOutput.value == 2000000000 27 | 28 | val properDeadline = redemptionRequestOutput.R8[Int].get >= HEIGHT + 720 // one day for contestation 29 | 30 | val properRedeemPosition = redemptionRequestOutput.R6[(Long, Coll[Byte])].get._1 <= redemptionRequestOutput.R5[Long].get 31 | 32 | val redeemR7 = redemptionRequestOutput.R7[(Long, Boolean)].get 33 | 34 | val redeemR7Correct = redeemR7._1 == -1 && redeemR7._2 == false 35 | 36 | val redemptionScriptCorrect = blake2b256(redemptionRequestOutput.propositionBytes) == redeemScriptHash 37 | 38 | val redemptionRequestCorrect = redemptionTokenCorrect && properCollateral && properDeadline && 39 | properRedeemPosition && redeemR7Correct && redemptionScriptCorrect 40 | 41 | sigmaProp(selfOutCorrect && redemptionRequestCorrect) 42 | 43 | } -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/NotePredicate.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import TrackingTypes.{NoteData, NoteId, ReserveNftId} 4 | import sigmastate.basics.CryptoConstants.EcPointType 5 | 6 | trait NotePredicate { 7 | /** 8 | * Whether a note with identifier `noteId` can be accepted 9 | */ 10 | def acceptable(noteId: NoteId): Boolean 11 | 12 | def acceptable(noteIds: Seq[NoteId]): Boolean = { 13 | noteIds.map(acceptable).forall(_ == true) 14 | } 15 | } 16 | 17 | // todo: tests 18 | // Collateral or Whitelist (CoW) predicate #1 19 | // at least 100% collateralized or current holder whitelisted 20 | class CoW1Predicate(whitelist: Set[EcPointType]) extends NotePredicate { 21 | 22 | /** 23 | * Whether a note with identifier `noteId` can be accepted 24 | */ 25 | override def acceptable(noteId: NoteId): Boolean = { 26 | DbEntities.unspentNotes.get(noteId) match { 27 | case Some(nd) => 28 | val holder = nd.holder 29 | val goldPrice = DbEntities.state.get("goldPrice").get 30 | 31 | def estimateReserve(reserveNftId: ReserveNftId): (Long, Long) = { 32 | val rd = DbEntities.reserves.get(reserveNftId).get 33 | val assets = rd.reserveBox.value 34 | val liabilities = rd.liabilites * goldPrice.toLong 35 | assets -> liabilities 36 | } 37 | 38 | def reservesOk(nd: NoteData): Boolean = { 39 | val holderReserveOpt = DbEntities.reserveKeys.get(nd.holder) 40 | val holderReserve = holderReserveOpt.map(estimateReserve).getOrElse(0L -> 0L) 41 | val historyReserves = nd.history.map(_.reserveId).map(estimateReserve) 42 | val reserves: Seq[(Long, Long)] = historyReserves ++ Seq(holderReserve) 43 | val totalAssets = reserves.map(_._1).sum 44 | val totalLiabilities = reserves.map(_._2).sum 45 | totalAssets >= totalLiabilities 46 | } 47 | whitelist.contains(holder) || reservesOk(nd) 48 | case None => 49 | false 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /docs/miw.md: -------------------------------------------------------------------------------- 1 | Magic Internet Gold Wallet 2 | ========================== 3 | 4 | Trusted tokenized USD reserves, such as USDT or USDC, are getting extremely popular these days, however, censorship of 5 | payments and trusted issue of assets are quickly growing concerns as well. There is huge need for unfreezeable, 6 | stable-priced electronic cash solution with known security guarantees, but also simple, which can be freely created 7 | on demand in communities, but also provide backed or trusted (such as USDT) stablecoins when counterparty is not well 8 | known. We present such unified solution called Magic Internet Gold Wallet - ChainCash-based wallet with support for 9 | on-chain and offchain payments, and money creation freely in community circles, or backed by blockchain assets. 10 | 11 | The wallet will have further sections in the UI : 12 | 13 | * **Current status** 14 | 15 | Shows total balance of notes, value of backing which can be redeemed, liabilities. 16 | 17 | Also, recent incoming payments. 18 | 19 | * **My Wallets** 20 | 21 | Contains three wallets: fast, backed, non-backed. Public key is the same for all the wallets. 22 | 23 | Backed show reserves associated with it has increase reserve option. If reserve is not created yet, there is option to create it, with ERG or 24 | other options. 25 | 26 | There is possibility to see notes for every wallet. 27 | 28 | * **Circles management** 29 | 30 | This section contains two subsections, *manage communities*, *mutual credit clearing*. In manage communities section 31 | a user can add new communities via url or QR code, adding community is extending whitelist, a or do fine tuning of 32 | white and black lists. Mutual credit clearing will allow to analyze loops in mutual debt and form transactions to clear it. 33 | 34 | * **How-To** 35 | 36 | Contains some guides for popular use-cases, such as local trading, issuing 37 | tokenized commodities backed money. 38 | 39 | Onchain contracts 40 | ----------------- 41 | 42 | Offchain Infrastructure 43 | ----------------------- 44 | 45 | Fees 46 | ---- 47 | 48 | Privacy 49 | ------- 50 | 51 | Roadmap 52 | ------- 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/WalletUtils.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import chaincash.offchain.TrackingTypes.ReserveNftId 4 | import io.circe.parser.parse 5 | import org.ergoplatform.{ErgoAddressEncoder, ErgoBox, ErgoBoxCandidate, ErgoTreePredef, JsonCodecs, P2PKAddress} 6 | 7 | trait WalletUtils extends HttpUtils with JsonCodecs { 8 | val serverUrl: String 9 | 10 | val feeValue = 2000000 11 | 12 | lazy val myAddress = fetchChangeAddress() 13 | lazy val myPoint = myAddress.pubkey.value 14 | 15 | def createFeeOut(creationHeight: Int): ErgoBoxCandidate = { 16 | new ErgoBoxCandidate(feeValue, ErgoTreePredef.feeProposition(720), creationHeight) // 0.002 ERG 17 | } 18 | 19 | def fetchInputs(): Seq[ErgoBox] = { 20 | val boxesUnspentUrl = s"$serverUrl/wallet/boxes/unspent?minConfirmations=0&maxConfirmations=-1&minInclusionHeight=0&maxInclusionHeight=-1" 21 | val boxesUnspentJson = parse(getJsonAsString(boxesUnspentUrl)).toOption.get 22 | boxesUnspentJson.\\("box").map(_.as[ErgoBox].toOption.get) 23 | } 24 | 25 | def fetchNodeHeight(): Int = { 26 | val infoUrl = s"$serverUrl/info" 27 | val infoObject = parse(getJsonAsString(infoUrl)).toOption.get.asObject.get 28 | infoObject.apply("fullHeight").flatMap(_.asNumber).get.toInt.get 29 | } 30 | 31 | def fetchChangeAddress(): P2PKAddress = { 32 | val boxesUnspentUrl = s"$serverUrl/wallet/status" 33 | val walletStatus = parse(getJsonAsString(boxesUnspentUrl)).toOption.get.asObject.get 34 | val addrStrOpt = walletStatus.apply("changeAddress").flatMap(_.asString) 35 | val eae = new ErgoAddressEncoder(ErgoAddressEncoder.MainnetNetworkPrefix) 36 | val addrOpt = addrStrOpt.map(s => eae.fromString(s).get.asInstanceOf[P2PKAddress]) 37 | addrOpt.get 38 | } 39 | 40 | def myUnspentNotes(): Seq[ErgoBox] = { 41 | // todo: inefficient scan through all the unspent notes here 42 | DbEntities.unspentNotes.filter(_._2.holder == myPoint).map(_._2.currentUtxo).materialize 43 | } 44 | 45 | def myBalance(): Long = { 46 | myUnspentNotes().map(_.additionalTokens.toArray.head._2).sum 47 | } 48 | 49 | def myReserveIds(): Seq[ReserveNftId] = { 50 | DbEntities.myReserves.materialize 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/ReserveUtils.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import io.circe.syntax.EncoderOps 4 | import org.ergoplatform.ErgoBox.R4 5 | import org.ergoplatform.{ErgoBoxCandidate, JsonCodecs, P2PKAddress, UnsignedErgoLikeTransaction, UnsignedInput} 6 | import scorex.crypto.hash.Digest32 7 | import sigmastate.Values.GroupElementConstant 8 | import sigmastate.eval.Colls 9 | import special.sigma.GroupElement 10 | import sigmastate.eval._ 11 | import sigmastate.interpreter.ContextExtension 12 | 13 | trait ReserveUtils extends WalletUtils with JsonCodecs { 14 | import chaincash.contracts.Constants.reserveErgoTree 15 | 16 | // create reserve with `amount` nanoerg associated with `pubkey` 17 | def createReserve(pubKey: GroupElement, amount: Long, changeAddress: P2PKAddress): Unit = { 18 | val inputs = fetchInputs().take(60) 19 | val creationHeight = inputs.map(_.creationHeight).max 20 | val reserveInputNft = Digest32 @@ inputs.head.id.toArray 21 | 22 | val inputValue = inputs.map(_.value).sum 23 | require(inputValue >= amount + feeValue) 24 | 25 | val reserveOut = new ErgoBoxCandidate( 26 | amount, 27 | reserveErgoTree, 28 | creationHeight, 29 | Colls.fromItems((Digest32Coll @@ Colls.fromArray(reserveInputNft)) -> 1L), 30 | Map(R4 -> GroupElementConstant(pubKey)) 31 | ) 32 | val feeOut = createFeeOut(creationHeight) 33 | val changeOutOpt = if(inputValue > amount + feeValue) { 34 | val changeValue = inputValue - (amount + feeValue) 35 | Some(new ErgoBoxCandidate(changeValue, changeAddress.script, creationHeight)) 36 | } else { 37 | None 38 | } 39 | 40 | val unsignedInputs = inputs.map(box => new UnsignedInput(box.id, ContextExtension.empty)) 41 | val outs = Seq(reserveOut, feeOut) ++ changeOutOpt.toSeq 42 | val tx = new UnsignedErgoLikeTransaction(unsignedInputs.toIndexedSeq, IndexedSeq.empty, outs.toIndexedSeq) 43 | println(tx.asJson) 44 | } 45 | 46 | def createReserve(address: P2PKAddress, amount: Long): Unit = { 47 | createReserve(address.pubkey.value, amount, address) 48 | } 49 | 50 | def createReserve(amount: Long): Unit = { 51 | val changeAddress = fetchChangeAddress() 52 | createReserve(changeAddress, amount) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/scala/chaincash/contracts/BasisDeployerSpec.scala: -------------------------------------------------------------------------------- 1 | package chaincash.contracts 2 | 3 | import org.scalatest.{Matchers, PropSpec} 4 | import sigmastate.Values.GroupElementConstant 5 | import sigmastate.basics.CryptoConstants 6 | import sigmastate.eval.CGroupElement 7 | 8 | class BasisDeployerSpec extends PropSpec with Matchers { 9 | 10 | property("BasisDeployer should compile Basis contract successfully") { 11 | // This test verifies that the Basis contract can be compiled 12 | val basisContract = Constants.readContract("offchain/basis.es", Map.empty) 13 | basisContract should not be empty 14 | 15 | val basisErgoTree = Constants.compile(basisContract) 16 | basisErgoTree should not be null 17 | 18 | val basisAddress = Constants.getAddressFromErgoTree(basisErgoTree) 19 | basisAddress.toString should startWith("W") 20 | } 21 | 22 | property("BasisDeployer should create valid deployment request") (pending) 23 | 24 | property("BasisDeployer should create valid scan request") { 25 | val exampleReserveTokenId = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" 26 | 27 | val scanRequest = BasisDeployer.createBasisScanRequest(exampleReserveTokenId) 28 | 29 | scanRequest should include("scanName") 30 | scanRequest should include("Basis Reserve") 31 | scanRequest should include(exampleReserveTokenId) 32 | scanRequest should include("containsAsset") 33 | } 34 | 35 | property("BasisDeployer should calculate redemption fees correctly") { 36 | BasisConstants.calculateRedemptionFee(1000000000L) shouldBe 20000000L // 2% of 1 ERG 37 | BasisConstants.calculateRedemptionFee(500000000L) shouldBe 10000000L // 2% of 0.5 ERG 38 | 39 | BasisConstants.calculateNetRedemption(1000000000L) shouldBe 980000000L // 1 ERG - 2% 40 | BasisConstants.calculateNetRedemption(500000000L) shouldBe 490000000L // 0.5 ERG - 2% 41 | } 42 | 43 | property("BasisDeployer should have correct constants") { 44 | BasisConstants.REDEEM_ACTION shouldBe 0 45 | BasisConstants.TOP_UP_ACTION shouldBe 1 46 | BasisConstants.MIN_TOP_UP_AMOUNT shouldBe 1000000000L 47 | BasisConstants.EMERGENCY_REDEMPTION_TIME shouldBe 604800000L // 7 days in ms 48 | BasisConstants.REDEMPTION_FEE_PERCENTAGE shouldBe 2 49 | } 50 | } -------------------------------------------------------------------------------- /docs/silvercents.md: -------------------------------------------------------------------------------- 1 | "SilverCents – a hybrid crypto currency 2 | ======================================== 3 | 4 | SilverCents are the on-chain tokens in a cryptocurrency run on the Ergo Platform, using the Basis protocol (CCIP-1). 5 | SilverCents are exchangeable one for one with constitutional silver dimes and quarters that are suitable for circulation, 6 | and there are billions of these coins, distributed widely throughout the USA. The point of exchange is at the point of 7 | business of the vendors that participate in the SilverCents economy. 8 | 9 | The supply of SilverCents is determined by the participation of vendor entities and their customers. Vendors can mint 10 | new SilverCents by creating ChainCash notes that are 50% collateralized by DexySilver, and by a promise to redeem any 11 | of their SilverCents for constitutional silver dimes and quarters, within certain guidelines. 12 | 13 | Just like the actual junk silver, the SilverCents will circulate like an incompressible fluid in the local economies. 14 | In fact, it will be more liquid because customers carry their phones about all the time, but not silver quarters. 15 | Even if a Vendor redeems their issued SilverCents by returning junk silver to the customer, they can still spend their 16 | own SilverCents again, or sell them to customers to exchange for their product or service. The only way that SilverCents 17 | are destroyed is upon default, in which the requisite portion of the Dexy Silver collateral is distributed to the holders 18 | of those SilverCents, which are subsequently burned. In that event, the Vendor gets a default on their record, viewable 19 | by any other vendors in the system that hold their SilverCents from other notes. SilverCents will implement a reputation 20 | system for Vendors. 21 | 22 | SilverCents could be traded online, and form the basis of an undercollateralized stablecoin for what that is worth. But 23 | the purpose of SilverCents is facilitate local circular economies based upon familiar tokenization and availability of 24 | constitutional silver dimes and quarters. SilverCents are for farmers markets, the county fair, food trucks, flea 25 | markets, handyman services, and other small local business markets. SilverCents makes it easier for the customers to 26 | pay with their phone, and makes it easier for Vendors to settle accounts with one another in silver coinage. -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | # ChainCash Server 2 | 3 | This document is describing general ideas behind ChainCash payment server, a piece of software doing client-side 4 | validation of ChainCash notes. 5 | 6 | ## Client-Side Validation 7 | 8 | ChainCash is allowing to create money with different levels of collateralization, different types of collateral and 9 | trust and so on. It is up to a user then what to accept and what not. A user is identified with ChainCash server, which 10 | is deciding whether to accept notes offered as a mean of payment or not. In practice, there could be many users and 11 | services behind a single ChainCash server. Thus we talk about client-side validation further, where is a client is a 12 | ChainCash server with its individual settings. The client could be thought as a self-sovereign bank. 13 | 14 | ## Acceptance Predicate 15 | 16 | [ChainCash whitepaper](https://github.com/kushti/chaincash/blob/master/paper/chaincash.pdf) defines client-side 17 | acceptance predicate, and we are going to details here. 18 | 19 | Acceptance predicate should be defined via [TOML](https://toml.io/en/) based settings. 20 | It should be possible to define following data the predicate is built on: 21 | 22 | * whitelist (for current holder) 23 | * blacklist (for current holder) 24 | * collateralization level 25 | 26 | ## Prototype 27 | 28 | Some prototype code (on-chain data tracking, persistence, transaction builders) in Scala can be found in [offchain](https://github.com/kushti/chaincash/tree/master/src/main/scala/chaincash/offchain) 29 | folder. 30 | 31 | ## Implementation 32 | 33 | ChainCash server will be implemented in Rust and implement following functionality: 34 | 35 | * support for one or multiple keys (accounts) 36 | * ability to create reserves for key and withdraw from them 37 | * ability to create new notes against reserves and propose them for payments, and to redeem notes 38 | * API to accept (or reject) payments, provide data for the state of accounts and the state of the whole system 39 | 40 | ## ChainCash Improvement Proposals 41 | 42 | Evolution of contracts is done via ChainCash improvement proposals (CCIPs). CCIP life cycle is about: 43 | 44 | * proposal stage when an author is proposing a new CCIP with new contracts (offchain protocol, sidechain) 45 | for public review 46 | * discussions stage which is leading to acceptance or rejection of the CCIP 47 | * implementation in case of acceptance 48 | 49 | Old ChainCash servers after new CCIP implementation continue to work, just can not recognize new types of notes. A 50 | server should indicate which CCIPs it is supporting. 51 | -------------------------------------------------------------------------------- /docs/abstract.md: -------------------------------------------------------------------------------- 1 | Proof-of-work cryptocurrencies, such as Bitcoin, as well as tokenized on a blockchain real-world assets provide unique 2 | possibility to have global and transparent kind of collateral to issue new money on top of (e.g. in form of stablecoins). 3 | However, in practice trustless and reliable derivatives are overcollateralized, but that means inelasticity of supply. Most of community currencies have elasticity of supply, but rely on trust so they are local, often have transparency issues, and blockchain as a solution for transparency and trustless management is usually too expensive for them. 4 | 5 | To combine the best of both worlds, in this work we propose a new global monetary system with decentralized issuance in peer-to-peer digital environment. Notes in this system are issued and transferred over a blockchain and collectively backed by collateral or trust (or both). 6 | Collateral for a note comes from reserves peers may have, and, on spending a note, a peer is attaching its reserve to collective backing. At the same time, a newly issued note could be accepted by a peer without any backing provided, for example, if such a note is issued by a friend or a trusted charity. Every peer in the system is having own individual rules for accepting notes~(widely accepted but optional standards may exist at the same), which provides basis for elasticity of supply. A note can be redemeed at any time against any of reserves of agents previously signed the note. However, any agent after the first one in the signatures chain is getting redemption receipt which is indicating debt of previous signers before him, and then he may redeem the receipt against a reserve of any previous signer, with a new redeemable receipt being generated. There is redemption fee to promote circulation of notes within the system. Different kinds of collateral can be used (and attached to the same note), such as native blockchain tokens (such as Bitcoin), derivative tokens (such as stablecoins), tokenized real-world assets etc. All the notes share the same unit of account. 7 | 8 | We provide implementation of smart contracts for reserve and note on top of Ergo blockchain. Currently, all the contracts are making progress on the blockchain only, but other options can be considered, for example, note trasfers can be made off-chain (using one of Layer 2 solutions) with only reserves being on-chain. 9 | 10 | We also show how to implement a local exchange trading system and mutual credit clearing on ChainCash. Thus we can have purely trust-based monetary subsystems as well as hybrid and collateral-based ones in the same global transparent environment. -------------------------------------------------------------------------------- /docs/whitepaper/sources.bib: -------------------------------------------------------------------------------- 1 | @inproceedings{saito2003peer, 2 | title={Peer-to-peer money: Free currency over the Internet}, 3 | author={Saito, Kenji}, 4 | booktitle={Web and Communication Technologies and Internet-Related Social Issues—HSI 2003: Second International Conference on Human. Society@ Internet Seoul, Korea, June 18--20, 2003 Proceedings 2}, 5 | pages={404--414}, 6 | year={2003}, 7 | organization={Springer} 8 | } 9 | 10 | @misc{kya, 11 | title = {Know Your Assumptions}, 12 | howpublished = {\url{https://www.ergoforum.org/t/know-your-assumptions/4198}}, 13 | note = {Accessed: 2023-01-30} 14 | } 15 | 16 | @article{machlup1970euro, 17 | title={Euro-dollar creation: A mystery story}, 18 | author={Machlup, Fritz}, 19 | journal={PSL Quarterly Review}, 20 | volume={23}, 21 | number={94}, 22 | year={1970} 23 | } 24 | 25 | @article{nakamoto2008peer, 26 | title={A peer-to-peer electronic cash system}, 27 | author={Nakamoto, Satoshi and Bitcoin, A}, 28 | journal={Bitcoin.--URL: https://bitcoin.org/bitcoin.pdf}, 29 | volume={4}, 30 | number={2}, 31 | year={2008} 32 | } 33 | 34 | @article{williams1996new, 35 | title={The new barter economy: an appraisal of Local Exchange and Trading Systems (LETS)}, 36 | author={Williams, Colin C}, 37 | journal={Journal of Public Policy}, 38 | volume={16}, 39 | number={1}, 40 | pages={85--101}, 41 | year={1996}, 42 | publisher={Cambridge University Press} 43 | } 44 | 45 | @article{unterguggenbercer1934end, 46 | title={THE END RESULTS OF THE WOERGL EXPERIMENT.}, 47 | author={Unterguggenbercer, Michael}, 48 | journal={Annals of Public and Cooperative Economics}, 49 | volume={10}, 50 | number={1}, 51 | pages={60--63}, 52 | year={1934}, 53 | publisher={Wiley Online Library} 54 | } 55 | 56 | @misc{impl, 57 | title = {ChainCash implementation}, 58 | howpublished = {\url{https://github.com/kushti/chaincash/tree/master/contracts}}, 59 | note = {Accessed: 2023-02-22} 60 | } 61 | 62 | @misc{contracts, 63 | title = {ChainCash contracts}, 64 | howpublished = {\url{https://github.com/ChainCashLabs/chaincash/tree/master/contracts/onchain}}, 65 | note = {Accessed: 2023-11-11} 66 | } 67 | 68 | @techreport{chepurnoy2019contractual, 69 | title={On Contractual Money}, 70 | author={Chepurnoy, Alexander and Saxena, Amitabh}, 71 | year={2019}, 72 | institution={EasyChair} 73 | } 74 | 75 | @misc{mtcs, 76 | title = {About CoFi}, 77 | howpublished = {\url{https://cofi.informal.systems/about}}, 78 | note = {Accessed: 2023-05-10} 79 | } 80 | 81 | @misc{server, 82 | title = {ChainCash server}, 83 | howpublished = {\url{https://github.com/ChainCashLabs/chaincash-rs}}, 84 | note = {Accessed: 2023-11-11} 85 | } 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/test/java/dexy/FileMockedErgoClient.java: -------------------------------------------------------------------------------- 1 | package org.ergoplatform.appkit; 2 | 3 | import okhttp3.HttpUrl; 4 | import okhttp3.mockwebserver.MockResponse; 5 | import okhttp3.mockwebserver.MockWebServer; 6 | import org.ergoplatform.appkit.impl.BlockchainContextBuilderImpl; 7 | import org.ergoplatform.appkit.impl.NodeAndExplorerDataSourceImpl; 8 | import org.ergoplatform.explorer.client.ExplorerApiClient; 9 | import org.ergoplatform.restapi.client.ApiClient; 10 | 11 | import java.io.IOException; 12 | import java.util.List; 13 | import java.util.function.Function; 14 | 15 | /** 16 | * MockedRunner using given files to provide BlockchainContext information. 17 | */ 18 | public class FileMockedErgoClient implements MockedErgoClient { 19 | 20 | private final List _nodeResponses; 21 | private final List _explorerResponses; 22 | 23 | private ApiClient client; 24 | private ExplorerApiClient explorerClient; 25 | private NodeAndExplorerDataSourceImpl dataSource; 26 | 27 | public FileMockedErgoClient(List nodeResponses, List explorerResponses) { 28 | _nodeResponses = nodeResponses; 29 | _explorerResponses = explorerResponses; 30 | 31 | MockWebServer node = new MockWebServer(); 32 | enqueueResponses(node, _nodeResponses); 33 | 34 | MockWebServer explorer = new MockWebServer(); 35 | enqueueResponses(explorer, _explorerResponses); 36 | 37 | try { 38 | node.start(); 39 | explorer.start(); 40 | } catch (IOException e) { 41 | throw new ErgoClientException("Cannot start server " + node.toString(), e); 42 | } 43 | 44 | HttpUrl baseUrl = node.url("/"); 45 | client = new ApiClient(baseUrl.toString()); 46 | HttpUrl explorerBaseUrl = explorer.url("/"); 47 | explorerClient = new ExplorerApiClient(explorerBaseUrl.toString()); 48 | dataSource = new NodeAndExplorerDataSourceImpl(client, explorerClient); 49 | 50 | } 51 | 52 | @Override 53 | public List getNodeResponses() { 54 | return _nodeResponses; 55 | } 56 | 57 | @Override 58 | public List getExplorerResponses() { 59 | return _explorerResponses; 60 | } 61 | 62 | void enqueueResponses(MockWebServer server, List rs) { 63 | for (String r : rs) { 64 | server.enqueue(new MockResponse() 65 | .addHeader("Content-Type", "application/json; charset=utf-8") 66 | .setBody(r)); 67 | } 68 | } 69 | 70 | @Override 71 | public BlockchainDataSource getDataSource() { 72 | return dataSource; 73 | } 74 | 75 | @Override 76 | public T execute(Function action) { 77 | 78 | BlockchainContext ctx = 79 | new BlockchainContextBuilderImpl(dataSource, NetworkType.MAINNET).build(); 80 | 81 | T res = action.apply(ctx); 82 | 83 | 84 | return res; 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/contracts/README.md: -------------------------------------------------------------------------------- 1 | # ChainCash Contracts 2 | 3 | This directory contains utilities for deploying and managing ChainCash contracts on the Ergo blockchain. 4 | 5 | ## Contracts 6 | 7 | ### Basis Reserve Contract 8 | 9 | The Basis reserve contract is an on-chain reserve that backs off-chain payments, allowing for: 10 | - Off-chain payments with no need to create anything on-chain first 11 | - Credit creation capabilities 12 | - Redemption with 2% fee 13 | - Emergency redemption after 7 days 14 | 15 | #### Deployment 16 | 17 | Use `BasisDeployer` utility to deploy Basis reserve contracts: 18 | 19 | ```scala 20 | import chaincash.contracts.BasisDeployer 21 | import sigmastate.Values.GroupElementConstant 22 | import sigmastate.eval.CGroupElement 23 | import sigmastate.basics.CryptoConstants 24 | 25 | // Example deployment 26 | val ownerKey = GroupElementConstant(CGroupElement(CryptoConstants.dlogGroup.generator)) 27 | val trackerNftId = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 28 | val reserveTokenId = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" 29 | 30 | val deploymentRequest = BasisDeployer.createBasisDeploymentRequest( 31 | ownerKey, 32 | trackerNftId, 33 | reserveTokenId, 34 | initialCollateral = 1000000000L // 1 ERG 35 | ) 36 | 37 | println(deploymentRequest) 38 | ``` 39 | 40 | #### Contract Features 41 | 42 | - **Redemption Action (0)**: Redeem off-chain notes with tracker signature 43 | - **Top-up Action (1)**: Add more collateral to the reserve 44 | - **Emergency Redemption**: Redeem without tracker signature after 7 days 45 | - **Double Spending Prevention**: AVL tree tracks redeemed timestamps 46 | 47 | #### Required Parameters 48 | 49 | 1. **Owner Public Key**: GroupElement representing reserve owner 50 | 2. **Tracker NFT ID**: NFT identifying the tracker service 51 | 3. **Reserve Token ID**: Singleton NFT identifying the reserve 52 | 4. **Initial Collateral**: Minimum 1 ERG (1000000000 nanoERG) 53 | 54 | ### Core ChainCash Contracts 55 | 56 | - **Reserve Contract**: On-chain collateral management 57 | - **Note Contract**: Digital currency issuance and transfer 58 | - **Receipt Contract**: Redemption receipt management 59 | 60 | ## Testing 61 | 62 | Run tests to verify contract compilation and deployment: 63 | 64 | ```bash 65 | sbt test 66 | ``` 67 | 68 | ## Deployment Process 69 | 70 | 1. **Compile Contracts**: Use `Constants` object to compile contracts 71 | 2. **Generate Addresses**: Get pay-to-script addresses for each contract 72 | 3. **Create Deployment Requests**: Use deployment utilities 73 | 4. **Submit Transactions**: Send deployment transactions to Ergo blockchain 74 | 5. **Monitor**: Use scan requests to monitor contract states 75 | 76 | ## Network Configuration 77 | 78 | - **Mainnet**: NetworkType.MAINNET 79 | - **Testnet**: NetworkType.TESTNET (modify Constants.scala) 80 | 81 | ## Security Notes 82 | 83 | - Always test on testnet before mainnet deployment 84 | - Verify contract addresses before deployment 85 | - Use proper key management for reserve owners 86 | - Monitor tracker services for availability -------------------------------------------------------------------------------- /docs/lets.md: -------------------------------------------------------------------------------- 1 | How to implement LETS on ChainCash 2 | ---------------------------------- 3 | 4 | This article describes how Local Exchange Trading System (LETS) on top of ChainCash by using ChainCash Server. 5 | 6 | LETS 7 | ---- 8 | 9 | A local exchange trading system (LETS) is a local mutual credit association in which members are allowed 10 | to create common credit money individually, written into a common ledger. 11 | 12 | As an example, assume that Alice, with zero balance, is willing to buy a litre of raw milk from Bob. 13 | 14 | First, they agree on a price; for example, assume that the price is about 2 Euro (as Alice and Bob are living 15 | in Ireland). After the deal is written into a ledger, Alice's balance becomes -2 (minus two) Euro, and Bob's balance 16 | becomes 2 Euro. Then Bob may spend his 2 Euro, for example, on homemade beer from Charlie. Such systems often impose 17 | limits on negative balances, and sometimes even on positive ones, to promote exchange in the community. 18 | 19 | LETS on ChainCash 20 | ----------------- 21 | 22 | * LETS members are decided off-chain 23 | * LETS members white-list each other in ChainCash Server 24 | * standard CCIP-1 contracts are used 25 | * there is no requirement for min reserve value, so it can be close to zero ERG (slightly above to cover 26 | storage rent requirements, eg 0.001 ERG) 27 | * then for Alice to pay to Bob in our example, she is issuing a note with needed amount and pays Bob with it. Then 28 | Bob may use it to pay Charlie. Bob may use multiple notes in one transaction 29 | * Alice's balance at any time can be calculated as total value of notes she holds at the moment minus total value of 30 | all the note she ever issued. It may be negative. 31 | 32 | Mutual Credit Clearing 33 | ------------------------ 34 | 35 | If Alice holds a note issued by Charlie, and Charlie holds a note issued by Alice, and both notes are of the same value, 36 | they can do clearing. For that, they create a single transaction which is redeeming both notes but without decreasing 37 | reserves, and sign it (signatures from both are needed, but for different inputs). There's "mutual credit clearing" 38 | test in `ChainCashSpec.scala` which can be used as an example of how to construct mutual credit clearing transaction. 39 | 40 | If notes are of different values, bigger one's can spend it to self to get two notes (payment and change), with one of 41 | them being equal to counteparty's note, and then clearing is possible. 42 | 43 | 44 | Extensions 45 | ---------- 46 | 47 | Using ChainCash contract is not the efficient option for implementing simple LETS just (in comparison with LETS-specific 48 | contracts in https://docs.ergoplatform.com/uses/lets/trustless-lets/#implementation). But with ChainCash a LETS can be 49 | a part of more complex financial system. And chain of ownership can be useful for integration. 50 | 51 | For example, if a note has a signature of special LETS member on it (eg a local municipality), it can be accepted by a 52 | party outside LETS, and if the party has a reserve, then the note may have value for other non-LETS agents. Also, we 53 | can imagine a note being accepted if enough known LETS members have signed it. 54 | -------------------------------------------------------------------------------- /docs/conf/.gitignore: -------------------------------------------------------------------------------- 1 | laws.pdf 2 | 3 | ## Core latex/pdflatex auxiliary files: 4 | *.aux 5 | *.lof 6 | *.log 7 | *.lot 8 | *.fls 9 | *.out 10 | *.toc 11 | *.fmt 12 | *.fot 13 | *.cb 14 | *.cb2 15 | 16 | ## Intermediate documents: 17 | *.dvi 18 | *-converted-to.* 19 | # these rules might exclude image files for figures etc. 20 | # *.ps 21 | # *.eps 22 | # *.pdf 23 | 24 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 25 | *.bbl 26 | *.bcf 27 | *.blg 28 | *-blx.aux 29 | *-blx.bib 30 | *.run.xml 31 | 32 | ## Build tool auxiliary files: 33 | *.fdb_latexmk 34 | *.synctex 35 | *.synctex(busy) 36 | *.synctex.gz 37 | *.synctex.gz(busy) 38 | *.pdfsync 39 | 40 | ## Auxiliary and intermediate files from other packages: 41 | # algorithms 42 | *.alg 43 | *.loa 44 | 45 | # achemso 46 | acs-*.bib 47 | 48 | # amsthm 49 | *.thm 50 | 51 | # beamer 52 | *.nav 53 | *.pre 54 | *.snm 55 | *.vrb 56 | 57 | # changes 58 | *.soc 59 | 60 | # cprotect 61 | *.cpt 62 | 63 | # elsarticle (documentclass of Elsevier journals) 64 | *.spl 65 | 66 | # endnotes 67 | *.ent 68 | 69 | # fixme 70 | *.lox 71 | 72 | # feynmf/feynmp 73 | *.mf 74 | *.mp 75 | *.t[1-9] 76 | *.t[1-9][0-9] 77 | *.tfm 78 | 79 | #(r)(e)ledmac/(r)(e)ledpar 80 | *.end 81 | *.?end 82 | *.[1-9] 83 | *.[1-9][0-9] 84 | *.[1-9][0-9][0-9] 85 | *.[1-9]R 86 | *.[1-9][0-9]R 87 | *.[1-9][0-9][0-9]R 88 | *.eledsec[1-9] 89 | *.eledsec[1-9]R 90 | *.eledsec[1-9][0-9] 91 | *.eledsec[1-9][0-9]R 92 | *.eledsec[1-9][0-9][0-9] 93 | *.eledsec[1-9][0-9][0-9]R 94 | 95 | # glossaries 96 | *.acn 97 | *.acr 98 | *.glg 99 | *.glo 100 | *.gls 101 | *.glsdefs 102 | 103 | # gnuplottex 104 | *-gnuplottex-* 105 | 106 | # gregoriotex 107 | *.gaux 108 | *.gtex 109 | 110 | # hyperref 111 | *.brf 112 | 113 | # knitr 114 | *-concordance.tex 115 | # TODO Comment the next line if you want to keep your tikz graphics files 116 | *.tikz 117 | *-tikzDictionary 118 | 119 | # listings 120 | *.lol 121 | 122 | # makeidx 123 | *.idx 124 | *.ilg 125 | *.ind 126 | *.ist 127 | 128 | # minitoc 129 | *.maf 130 | *.mlf 131 | *.mlt 132 | *.mtc[0-9]* 133 | *.slf[0-9]* 134 | *.slt[0-9]* 135 | *.stc[0-9]* 136 | 137 | # minted 138 | _minted* 139 | *.pyg 140 | 141 | # morewrites 142 | *.mw 143 | 144 | # nomencl 145 | *.nlo 146 | 147 | # pax 148 | *.pax 149 | 150 | # pdfpcnotes 151 | *.pdfpc 152 | 153 | # sagetex 154 | *.sagetex.sage 155 | *.sagetex.py 156 | *.sagetex.scmd 157 | 158 | # scrwfile 159 | *.wrt 160 | 161 | # sympy 162 | *.sout 163 | *.sympy 164 | sympy-plots-for-*.tex/ 165 | 166 | # pdfcomment 167 | *.upa 168 | *.upb 169 | 170 | # pythontex 171 | *.pytxcode 172 | pythontex-files-*/ 173 | 174 | # thmtools 175 | *.loe 176 | 177 | # TikZ & PGF 178 | *.dpth 179 | *.md5 180 | *.auxlock 181 | 182 | # todonotes 183 | *.tdo 184 | 185 | # easy-todo 186 | *.lod 187 | 188 | # xindy 189 | *.xdy 190 | 191 | # xypic precompiled matrices 192 | *.xyc 193 | 194 | # endfloat 195 | *.ttt 196 | *.fff 197 | 198 | # Latexian 199 | TSWLatexianTemp* 200 | 201 | ## Editors: 202 | # WinEdt 203 | *.bak 204 | *.sav 205 | 206 | # Texpad 207 | .texpadtmp 208 | 209 | # Kile 210 | *.backup 211 | 212 | # KBibTeX 213 | *~[0-9]* 214 | 215 | # auto folder when using emacs and auctex 216 | /auto/* 217 | 218 | # expex forward references with \gathertags 219 | *-tags.tex 220 | -------------------------------------------------------------------------------- /src/test/resources/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "constants": [ 3 | { 4 | "name": "myBoxId", 5 | "type": "CollByte", 6 | "value": "506dfb0a34d44f2baef77d99f9da03b1f122bdc4c7c31791a0c706e23f1207e7" 7 | }, 8 | { 9 | "name": "emissionAddress", 10 | "type": "Address", 11 | "value": "2z93aPPTpVrZJHkQN54V7PatEfg3Ac1zKesFxUz8TGGZwPT4Rr5q6tBwsjEjounQU4KNZVqbFAUsCNipEKZmMdx2WTqFEyUURcZCW2CrSqKJ8YNtSVDGm7eHcrbPki9VRsyGpnpEQvirpz6GKZgghcTRDwyp1XtuXoG7XWPC4bT1U53LhiM3exE2iUDgDkme2e5hx9dMyBUi9TSNLNY1oPy2MjJ5seYmGuXCTRPLqrsi" 12 | }, 13 | { 14 | "name": "timestampAddress", 15 | "type": "Address", 16 | "value": "4MQyMKvMbnCJG3aJ" 17 | }, 18 | { 19 | "name": "myTokenId", 20 | "type": "CollByte", 21 | "value": "dbea46d988e86b1e60181b69936a3b927c3a4871aa6ed5258d3e4df155750bea" 22 | }, 23 | { 24 | "name": "minTokenAmount", 25 | "type": "Long", 26 | "value": "2" 27 | }, 28 | { 29 | "name": "one", 30 | "type": "Long", 31 | "value": "1" 32 | }, 33 | { 34 | "name": "minStorageRent", 35 | "type": "Long", 36 | "value": "2000000" 37 | } 38 | ], 39 | "dataInputs": [ 40 | { 41 | "id": { 42 | "value": "myBoxId" 43 | } 44 | } 45 | ], 46 | "inputs": [ 47 | { 48 | "address": { 49 | "value": "emissionAddress" 50 | }, 51 | "tokens": [ 52 | { 53 | "index": 0, 54 | "id": { 55 | "value": "myTokenId" 56 | }, 57 | "amount": { 58 | "name": "inputTokenAmount", 59 | "value": "minTokenAmount", 60 | "filter": "Ge" 61 | } 62 | } 63 | ], 64 | "nanoErgs": { 65 | "name": "inputNanoErgs" 66 | }, 67 | "options": [ 68 | "Strict" 69 | ] 70 | } 71 | ], 72 | "outputs": [ 73 | { 74 | "address": { 75 | "value": "emissionAddress" 76 | }, 77 | "tokens": [ 78 | { 79 | "index": 0, 80 | "id": { 81 | "value": "myTokenId" 82 | }, 83 | "amount": { 84 | "value": "balanceTokenAmount" 85 | } 86 | } 87 | ], 88 | "nanoErgs": { 89 | "value": "inputNanoErgs" 90 | } 91 | }, 92 | { 93 | "address": { 94 | "value": "timestampAddress" 95 | }, 96 | "registers": [ 97 | { 98 | "value": "myBoxId", 99 | "num": "R4", 100 | "type": "CollByte" 101 | }, 102 | { 103 | "value": "HEIGHT", 104 | "num": "R5", 105 | "type": "Int" 106 | } 107 | ], 108 | "tokens": [ 109 | { 110 | "index": 0, 111 | "id": { 112 | "value": "myTokenId" 113 | }, 114 | "amount": { 115 | "value": "one" 116 | } 117 | } 118 | ], 119 | "nanoErgs": { 120 | "value": "minStorageRent" 121 | } 122 | } 123 | ], 124 | "binaryOps": [ 125 | { 126 | "name": "balanceTokenAmount", 127 | "first": "inputTokenAmount", 128 | "op": "Sub", 129 | "second": "one" 130 | } 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /docs/whitepaper/.gitignore: -------------------------------------------------------------------------------- 1 | laws.pdf 2 | 3 | ## Core latex/pdflatex auxiliary files: 4 | *.aux 5 | *.lof 6 | *.log 7 | *.lot 8 | *.fls 9 | *.out 10 | *.toc 11 | *.fmt 12 | *.fot 13 | *.cb 14 | *.cb2 15 | 16 | ## Intermediate documents: 17 | *.dvi 18 | *-converted-to.* 19 | # these rules might exclude image files for figures etc. 20 | # *.ps 21 | # *.eps 22 | # *.pdf 23 | 24 | ## Generated if empty string is given at "Please type another file name for output:" 25 | .pdf 26 | 27 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 28 | *.bbl 29 | *.bcf 30 | *.blg 31 | *-blx.aux 32 | *-blx.bib 33 | *.run.xml 34 | 35 | ## Build tool auxiliary files: 36 | *.fdb_latexmk 37 | *.synctex 38 | *.synctex(busy) 39 | *.synctex.gz 40 | *.synctex.gz(busy) 41 | *.pdfsync 42 | 43 | ## Auxiliary and intermediate files from other packages: 44 | # algorithms 45 | *.alg 46 | *.loa 47 | 48 | # achemso 49 | acs-*.bib 50 | 51 | # amsthm 52 | *.thm 53 | 54 | # beamer 55 | *.nav 56 | *.pre 57 | *.snm 58 | *.vrb 59 | 60 | # changes 61 | *.soc 62 | 63 | # cprotect 64 | *.cpt 65 | 66 | # elsarticle (documentclass of Elsevier journals) 67 | *.spl 68 | 69 | # endnotes 70 | *.ent 71 | 72 | # fixme 73 | *.lox 74 | 75 | # feynmf/feynmp 76 | *.mf 77 | *.mp 78 | *.t[1-9] 79 | *.t[1-9][0-9] 80 | *.tfm 81 | 82 | #(r)(e)ledmac/(r)(e)ledpar 83 | *.end 84 | *.?end 85 | *.[1-9] 86 | *.[1-9][0-9] 87 | *.[1-9][0-9][0-9] 88 | *.[1-9]R 89 | *.[1-9][0-9]R 90 | *.[1-9][0-9][0-9]R 91 | *.eledsec[1-9] 92 | *.eledsec[1-9]R 93 | *.eledsec[1-9][0-9] 94 | *.eledsec[1-9][0-9]R 95 | *.eledsec[1-9][0-9][0-9] 96 | *.eledsec[1-9][0-9][0-9]R 97 | 98 | # glossaries 99 | *.acn 100 | *.acr 101 | *.glg 102 | *.glo 103 | *.gls 104 | *.glsdefs 105 | 106 | # gnuplottex 107 | *-gnuplottex-* 108 | 109 | # gregoriotex 110 | *.gaux 111 | *.gtex 112 | 113 | # hyperref 114 | *.brf 115 | 116 | # knitr 117 | *-concordance.tex 118 | # TODO Comment the next line if you want to keep your tikz graphics files 119 | *.tikz 120 | *-tikzDictionary 121 | 122 | # listings 123 | *.lol 124 | 125 | # makeidx 126 | *.idx 127 | *.ilg 128 | *.ind 129 | *.ist 130 | 131 | # minitoc 132 | *.maf 133 | *.mlf 134 | *.mlt 135 | *.mtc[0-9]* 136 | *.slf[0-9]* 137 | *.slt[0-9]* 138 | *.stc[0-9]* 139 | 140 | # minted 141 | _minted* 142 | *.pyg 143 | 144 | # morewrites 145 | *.mw 146 | 147 | # nomencl 148 | *.nlo 149 | 150 | # pax 151 | *.pax 152 | 153 | # pdfpcnotes 154 | *.pdfpc 155 | 156 | # sagetex 157 | *.sagetex.sage 158 | *.sagetex.py 159 | *.sagetex.scmd 160 | 161 | # scrwfile 162 | *.wrt 163 | 164 | # sympy 165 | *.sout 166 | *.sympy 167 | sympy-plots-for-*.tex/ 168 | 169 | # pdfcomment 170 | *.upa 171 | *.upb 172 | 173 | # pythontex 174 | *.pytxcode 175 | pythontex-files-*/ 176 | 177 | # thmtools 178 | *.loe 179 | 180 | # TikZ & PGF 181 | *.dpth 182 | *.md5 183 | *.auxlock 184 | 185 | # todonotes 186 | *.tdo 187 | 188 | # easy-todo 189 | *.lod 190 | 191 | # xindy 192 | *.xdy 193 | 194 | # xypic precompiled matrices 195 | *.xyc 196 | 197 | # endfloat 198 | *.ttt 199 | *.fff 200 | 201 | # Latexian 202 | TSWLatexianTemp* 203 | 204 | ## Editors: 205 | # WinEdt 206 | *.bak 207 | *.sav 208 | 209 | # Texpad 210 | .texpadtmp 211 | 212 | # Kile 213 | *.backup 214 | 215 | # KBibTeX 216 | *~[0-9]* 217 | 218 | # auto folder when using emacs and auctex 219 | /auto/* 220 | 221 | # expex forward references with \gathertags 222 | *-tags.tex 223 | -------------------------------------------------------------------------------- /src/test/resources/token-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "constants": [ 3 | { 4 | "name": "506dfb0a34d44f2baef77d99f9da03b1f122bdc4c7c31791a0c706e23f1207e7", 5 | "type": "CollByte", 6 | "value": "506dfb0a34d44f2baef77d99f9da03b1f122bdc4c7c31791a0c706e23f1207e7" 7 | }, 8 | { 9 | "name": "ae57e4add0f181f5d1e8fd462969e4cc04f13b0da183676660d280ad0b64563f", 10 | "type": "CollByte", 11 | "value": "ae57e4add0f181f5d1e8fd462969e4cc04f13b0da183676660d280ad0b64563f" 12 | }, 13 | { 14 | "name": "dbea46d988e86b1e60181b69936a3b927c3a4871aa6ed5258d3e4df155750bea", 15 | "type": "CollByte", 16 | "value": "dbea46d988e86b1e60181b69936a3b927c3a4871aa6ed5258d3e4df155750bea" 17 | }, 18 | { 19 | "name": "9f5ZKbECVTm25JTRQHDHGM5ehC8tUw5g1fCBQ4aaE792rWBFrjK", 20 | "type": "Address", 21 | "value": "9f5ZKbECVTm25JTRQHDHGM5ehC8tUw5g1fCBQ4aaE792rWBFrjK" 22 | }, 23 | { 24 | "name": "1234", 25 | "type": "Long", 26 | "value": "1234" 27 | }, 28 | { 29 | "name": "1", 30 | "type": "Long", 31 | "value": "1" 32 | } 33 | ], 34 | "inputs": [ 35 | { 36 | "id": { 37 | "value": "dbea46d988e86b1e60181b69936a3b927c3a4871aa6ed5258d3e4df155750bea" 38 | }, 39 | "address": { 40 | "name": "myAddress" 41 | }, 42 | "tokens": [ 43 | { 44 | "index": 1, 45 | "id": { 46 | "name": "myTokenId" 47 | }, 48 | "amount": { 49 | "name": "myTokenAmount" 50 | } 51 | } 52 | ], 53 | "options": [ 54 | "Strict" 55 | ] 56 | }, 57 | { 58 | "address": { 59 | "value": "9f5ZKbECVTm25JTRQHDHGM5ehC8tUw5g1fCBQ4aaE792rWBFrjK" 60 | }, 61 | "tokens": [ 62 | { 63 | "index": 0, 64 | "id": { 65 | "name": "firstTokenId" 66 | }, 67 | "amount": { 68 | "value": "1234", 69 | "filter": "Gt" 70 | } 71 | }, 72 | { 73 | "index": 1, 74 | "id": { 75 | "name": "secondTokenId" 76 | }, 77 | "amount": { 78 | "value": "myTokenAmount" 79 | } 80 | }, 81 | { 82 | "index": 2, 83 | "id": { 84 | "name": "thirdTokenId" 85 | }, 86 | "amount": { 87 | "name": "thirdTokenAmount" 88 | } 89 | }, 90 | { 91 | "id": { 92 | "value": "506dfb0a34d44f2baef77d99f9da03b1f122bdc4c7c31791a0c706e23f1207e7" 93 | } 94 | }, 95 | { 96 | "id": { 97 | "value": "ae57e4add0f181f5d1e8fd462969e4cc04f13b0da183676660d280ad0b64563f" 98 | }, 99 | "amount": { 100 | "value": "1" 101 | } 102 | }, 103 | { 104 | "id": { 105 | "value": "myTokenId" 106 | }, 107 | "amount": { 108 | "value": "1234", 109 | "filter": "Lt" 110 | } 111 | }, 112 | { 113 | "id": { 114 | "value": "dbea46d988e86b1e60181b69936a3b927c3a4871aa6ed5258d3e4df155750bea" 115 | } 116 | } 117 | ], 118 | "nanoErgs": { 119 | "value": "myTokenAmount+1234", 120 | "filter": "Ge" 121 | }, 122 | "options": [ 123 | "Strict" 124 | ] 125 | } 126 | ], 127 | "outputs": [], 128 | "binaryOps": [ 129 | { 130 | "name": "myTokenAmount+1234", 131 | "first": "myTokenAmount", 132 | "op": "Add", 133 | "second": "1234" 134 | } 135 | ], 136 | "postConditions": [ 137 | { 138 | "first": "myTokenAmount", 139 | "second": "thirdTokenAmount", 140 | "op": "Gt" 141 | } 142 | ] 143 | } -------------------------------------------------------------------------------- /docs/l2.md: -------------------------------------------------------------------------------- 1 | ChainCash on Layer2 2 | =================== 3 | 4 | This article describes how ChainCash notes can be implemented on Layer2, which means a note can be created and 5 | transferred to any other ChainCash participant (possibly, multiple times) without blockchain transactions needed, and 6 | only redemption happens on Layer1, means blockchain transactions involved. 7 | 8 | Transfers 9 | --------- 10 | 11 | To issue a note with value *aliceNoteValue* and spend it to Bob immediately, Alice (which has layer1 reserve in a box 12 | associated with singleton token *aliceReserveId*) is signing record (zeroTreeHash, aliceReserveId, aliceNoteValue, bobReserveId), where 13 | *bobReserveId* is Bob's reserve singleton token id, and *zeroTreeHash* is root hash of empty AVL+ tree. She transfers the record 14 | along with the signature (offchain) 15 | 16 | Then Bob can redeem it immediately, or pay Carol with it, with possible change allowed. For that, to pay Carol 17 | *bobNote1Value* (and pay back self *bobNote2Value* = *aliceNoteValue* - *bobNote1Value*), Bob is creating and signing two records, 18 | (firstTreeHash, bobReserveId, bobNote1Value, carolReserveId) and (firstTreeHash, bobReserveId, bobNote2Value, bobReserveId), 19 | where *firstTreeHash* is hash of an AVL+ tree containing Alice's note signatures only (so it is hash of the initial tree with hash 20 | *zeroTreeHash* with Alice's signature for the first payment to Bob added). Bob is sending all the three records along with signatures to Carol. 21 | 22 | Similarly, all other receivers are checking that all previous transfers, i.e. records and signatures correctness. 23 | 24 | As AVL+ tree contains key -> value pairs , we use synthentic counter which is starting with 0 and increasing on every transfer, 25 | so in our example, Bob is getting tree which contains leaf with zero as key and (zeroTreeHash, aliceReserveId, aliceNoteValue, bobReserveId) 26 | (serialized to byte array) as value. 27 | 28 | Redemption 29 | ---------- 30 | 31 | As stated in the intro, redemption happens via on-chain smart-contract. In this section this contract is described in general. 32 | 33 | If note issuer (Alice in our example) has enough reserves, redemption is taking place against her reserve. However, if her reserve is not able to cover the note, then the 34 | next signer, so Bob in our example, will cover the redemption. If Bob's reserve is also not able to cover the note, then 35 | Carol does so etc. 36 | 37 | What is presented to the redemption contract, in case of honest agent doing it, that is hash of tree containing previous transfers, 38 | current holder reserve id (and then redemption can be done to a public key associated with the reserve), and reserve to redeem 39 | from (earliest reserve which is able to cover the note). 40 | 41 | However, the contract should work securely in the presence of an adversary trying. Adversary can do a lot in the setting 42 | when it can come with a signatures tree formed off-chain to on-chain contract: 43 | 44 | * it can come twice with the same tree to the contract, trying to do redemption twice 45 | * past holder can cut a tree to the point where it was holder of a note, and redeem it 46 | * holder can be disconnected from the tree at all (last leaf in the tree can refer to different holder) 47 | * tree can be corrupt (wrong signatures) 48 | * parts of the tree or the whole tree could be known to the adversary only 49 | 50 | Thus we organize redemption logic as follows: 51 | * a box with redemption contract must have collateral which is seized in case of providing wrong data 52 | * in case when data not available to other parties (than redemption box creator), there could be multi-round process 53 | of data recovery 54 | * a box with redemption contract which can interact with reserve contract could be created only by calling redemption 55 | box producer contract. 56 | 57 | 58 | Public Bulletin Board 59 | --------------------- 60 | 61 | 62 | 63 | References 64 | ---------- 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/TrackingUtils.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import io.circe.parser.parse 4 | import org.ergoplatform.ErgoBox 5 | import scorex.util.{ModifierId, ScorexLogging} 6 | import scorex.util.encode.Base16 7 | import TrackingTypes._ 8 | import chaincash.offchain.DbEntities.heightKey 9 | import org.ergoplatform.ErgoBox.R4 10 | import sigmastate.Values.GroupElementConstant 11 | 12 | trait TrackingUtils extends WalletUtils with HttpUtils with ScorexLogging { 13 | 14 | val noteScanId = 21 15 | val reserveScanId = 20 16 | 17 | 18 | def lastProcessedHeight(): Int = DbEntities.state.get(heightKey).map(_.toInt).getOrElse(0) 19 | 20 | private def fetchBoxes(scanId: Int, from: Int, to: Int) = { 21 | val reserveScanUrl = s"$serverUrl/scan/unspentBoxes/$scanId?minInclusionHeight=$from&maxInclusionHeight=$to" 22 | val boxesUnspentJson = parse(getJsonAsString(reserveScanUrl)).toOption.get 23 | boxesUnspentJson.\\("box").map(_.as[ErgoBox].toOption.get) 24 | } 25 | 26 | def processReservesFor(from: Int, to: Int) = { 27 | 28 | def processBox(box: ErgoBox): Unit = { 29 | val boxId = box.id 30 | val boxTokens = box.additionalTokens.toArray 31 | if (boxTokens.isEmpty) { 32 | log.warn(s"Reserve box with no NFT: ${Base16.encode(boxId)}") 33 | return 34 | } 35 | val reserveNftRecord = boxTokens.head 36 | if (reserveNftRecord._2 > 1) { 37 | log.warn(s"Reserve box with more than one id token: ${Base16.encode(boxId)}") 38 | return 39 | } 40 | val reserveNftId = reserveNftRecord._1 41 | val reserveNftIdEncoded = ModifierId @@ Base16.encode(reserveNftId.toArray) 42 | val rd: ReserveData = DbEntities.reserves.get(reserveNftIdEncoded) match { 43 | case Some(reserveData: ReserveData) => 44 | // if existing reserve box updated, e.g. top-up done on it 45 | ReserveData(box, reserveData.signedUnspentNotes, liabilites = 0L) // todo: fix liabilities 46 | case None => 47 | val rd = ReserveData(box, IndexedSeq.empty, liabilites = 0L) // todo: fix liabilities 48 | box.additionalRegisters.get(R4).foreach { v => 49 | v match { 50 | case owner: GroupElementConstant if owner == GroupElementConstant(myPoint) => 51 | DbEntities.myReserves.add(rd.reserveNftId) 52 | case _ => 53 | log.warn(s"Reserve R4 miss for $v") 54 | } 55 | } 56 | rd 57 | } 58 | DbEntities.reserves.put(key = rd.reserveNftId, value = rd) 59 | } 60 | 61 | fetchBoxes(reserveScanId, from, to).foreach { box => 62 | processBox(box) 63 | } 64 | } 65 | 66 | // todo: check how it works when not is spent multiple times in the same block 67 | def processNotes(from: Int, to: Int) = { 68 | def processBox(box: ErgoBox): Unit = { 69 | val boxId = ModifierId @@ Base16.encode(box.id) 70 | val boxTokens = box.additionalTokens.toArray 71 | if (boxTokens.isEmpty || boxTokens.length > 1) { 72 | log.warn(s"Reserve box with no NFT: $boxId") 73 | return 74 | } 75 | val noteTokenId = ModifierId @@ Base16.encode(boxTokens.head._1.toArray) 76 | val noteValue = boxTokens.head._2 77 | 78 | DbEntities.issuedNotes.get(noteTokenId) match { 79 | case Some(_) => 80 | 81 | case None => 82 | DbEntities.issuedNotes.put(noteTokenId, box) 83 | // todo: check that AVL+ tree is empty 84 | val noteData = NoteData(box, IndexedSeq.empty) 85 | DbEntities.unspentNotes.put(boxId, noteData) 86 | 87 | } 88 | 89 | } 90 | 91 | fetchBoxes(noteScanId, from, to).foreach { box => 92 | processBox(box) 93 | } 94 | } 95 | 96 | def processBlocks(): Unit = { 97 | val localHeight = lastProcessedHeight() 98 | val nodeHeight = fetchNodeHeight() 99 | if (nodeHeight > localHeight) { 100 | processReservesFor(localHeight, nodeHeight) 101 | processNotes(localHeight, nodeHeight) 102 | DbEntities.state.put(heightKey, nodeHeight.toString) 103 | } 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /contracts/offchain/apps/gitcircles.md: -------------------------------------------------------------------------------- 1 | Git Circles: framework to create currencies for online communities around Open Source projects 2 | ============================================================================================= 3 | 4 | In this document, we describe Git Circles, a solution to create community currencies for open source project communities. 5 | 6 | Basic actions could be seen in the following example: 7 | 8 | * Git oracle is posting commit id, its author (the simplest option is to have commits signed w. secp256k1), and number of lines of code in the commit, along wih oracle signature. Via this action new git points are minted, N lines of code provides N git points 9 | 10 | * There could be services which are providing services for git points (computing outsourcing services like Celaut, AI services etc) 11 | 12 | * One special kind of service is sponsorship, which is about exchanging git points for stablecoins (Gold or USD denominated) / ERG / rsBTC (bridged BTC) etc via https://www.ergoforum.org/t/basis-a-foundational-on-chain-reserve-approach-to-support-a-variety-of-offchain-protocols/5153 . A sponsor may spend git points obtained on services, so may be "sponsorship" would not be the most appropriate term (depending on services economy) 13 | 14 | Note, every project (repository or a set of coupled repositories) has its own token. For example, Linux git point is different from Java git point. Potentially Linux and Java ecosystems have different service providers and sponsors. Both systems use Ergo blockchain though. 15 | 16 | 17 | Some high-level tech details 18 | ============================ 19 | 20 | Components: 21 | 22 | * Ergo blockchain - to get from git points to blockchain assets (and back) 23 | * public bulletin board (aka tracker) with events - offchain service to maintain ordered log of offchain events. For starters, it could be a single trusted server. Then it can be replaced with federation, using pod network ( https://pod.network/ ) or a dedicated blockchain for agreement on transactions order. The same server (federation) can be used for Git oracle. Server's signature is used in redemption as well, for federation, as Schnorr signatures as used, MuSig or other aggregated signature schemes could be used for federation approved redemption. 24 | 25 | Events: 26 | 27 | * Pubkey registration 28 | 29 | In both offchain part and on-chain, secp256k1 based pubkeys are used. To bind Github account (e.g. "kushti") with secp256k1 pubkey, a key binding event should be posted, the binding event is about posting Github account along with pubkey and link to public gist published under Github account 30 | 31 | * Mint 32 | 33 | Git oracle's pubkey is known to everyone. The oracle is tracking master branch of project's repositories. When new commits appear there, done by authors with known pubkeys, oracle is posting minting events. The oracle is posting commit id, author's pubkey, note value, along with a signature. 34 | 35 | There are no rollbacks, so when commits are rolled back with forced push, minted notes are still around. Normally, forced push is not used in more or less established repositories. 36 | 37 | * Transfer 38 | 39 | After mints, unspent notes do exist in form of (commit id, author's pubkey, note value) are created, and we consider commit id here as unique id of the a note. Then transfer transaction is spending some existing notes, and creating new ones. New notes have the same format (note id, pubkey, note value), where note_id == hash(input note #0's id ++ input note #1's id ++ index), so hash of all the input note ids and also output index. Transfer transaction is signed by each of the input pubkeys, the whole transaction is used as a message. 40 | 41 | Structure of transfer transaction is similar to on-chain UTXO transactions. 42 | 43 | * Redemption 44 | 45 | Redemption transaction is similar to transfer transaction, but output should be signed by pubkey of receiver (sponsor) as well. Signed output allows for on-chain redemption via sastsfying Basis contract conditions ( https://www.ergoforum.org/t/basis-a-foundational-on-chain-reserve-approach-to-support-a-variety-of-offchain-protocols/5153 ) 46 | 47 | 48 | Fees and monetization: 49 | 50 | 51 | Tech stack: 52 | Ergo node and offchain app for sponsors and redemption 53 | For centralized server, Nostr relay and specialized clients -------------------------------------------------------------------------------- /src/main/scala/chaincash/contracts/Constants.scala: -------------------------------------------------------------------------------- 1 | package chaincash.contracts 2 | 3 | import org.ergoplatform.ErgoAddressEncoder 4 | import org.ergoplatform.appkit.{AppkitHelpers, ErgoValue, NetworkType} 5 | import scorex.crypto.hash.Blake2b256 6 | import scorex.util.encode.Base58 7 | import sigmastate.eval.CGroupElement 8 | import sigmastate.basics.CryptoConstants 9 | import sigmastate.AvlTreeFlags 10 | import sigmastate.Values.ErgoTree 11 | import sigmastate.lang.{CompilerSettings, SigmaCompiler, TransformingSigmaBuilder} 12 | import special.sigma.{AvlTree, GroupElement} 13 | import work.lithos.plasma.PlasmaParameters 14 | import work.lithos.plasma.collections.PlasmaMap 15 | 16 | import java.util 17 | 18 | object Constants { 19 | 20 | val networkType = NetworkType.MAINNET 21 | val networkPrefix = networkType.networkPrefix 22 | val ergoAddressEncoder = new ErgoAddressEncoder(networkPrefix) 23 | private val compiler = SigmaCompiler(CompilerSettings(networkPrefix, TransformingSigmaBuilder, lowerMethodCalls = true)) 24 | 25 | def getAddressFromErgoTree(ergoTree: ErgoTree) = ergoAddressEncoder.fromProposition(ergoTree).get 26 | 27 | def substitute(contract: String, substitutionMap: Map[String, String] = Map.empty): String = { 28 | substitutionMap.foldLeft(contract){case (c, (k,v)) => 29 | c.replace("$"+k, v) 30 | } 31 | } 32 | 33 | def readContract(path: String, substitutionMap: Map[String, String] = Map.empty) = { 34 | val contract = scala.io.Source.fromFile("contracts/" + path, "utf-8").getLines.mkString("\n") 35 | substitute(contract, substitutionMap) 36 | } 37 | 38 | def compile(ergoScript: String): ErgoTree = { 39 | AppkitHelpers.compile(new util.HashMap[String, Object](), ergoScript, networkPrefix) 40 | } 41 | 42 | 43 | val chainCashPlasmaParameters = PlasmaParameters(40, None) 44 | def emptyPlasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](AvlTreeFlags.InsertOnly, chainCashPlasmaParameters) 45 | val emptyTreeErgoValue: ErgoValue[AvlTree] = emptyPlasmaMap.ergoValue 46 | val emptyTree: AvlTree = emptyTreeErgoValue.getValue 47 | 48 | val g: GroupElement = CGroupElement(CryptoConstants.dlogGroup.generator) 49 | 50 | val reserveContract = readContract("onchain/reserve.es", Map.empty) 51 | val reserveErgoTree = compile(reserveContract) 52 | val reserveAddress = getAddressFromErgoTree(reserveErgoTree) 53 | val reserveContractHash = Blake2b256(reserveErgoTree.bytes.tail) 54 | val reserveContractHashString = Base58.encode(reserveContractHash) 55 | 56 | val receiptContract = readContract("onchain/receipt.es", Map("reserveContractHash" -> reserveContractHashString)) 57 | val receiptErgoTree = compile(receiptContract) 58 | val receiptAddress = getAddressFromErgoTree(receiptErgoTree) 59 | val receiptContractHash = Blake2b256(receiptErgoTree.bytes.tail) 60 | val receiptContractHashString = Base58.encode(receiptContractHash) 61 | 62 | val noteContract = readContract("onchain/note.es", 63 | Map("reserveContractHash" -> reserveContractHashString, "receiptContractHash" -> receiptContractHashString)) 64 | val noteErgoTree = compile(noteContract) 65 | val noteAddress = getAddressFromErgoTree(noteErgoTree) 66 | 67 | // Basis contracts 68 | 69 | val basisContract = readContract("offchain/basis.es", Map()) 70 | val basisErgoTree = compile(basisContract) 71 | val basisAddress = getAddressFromErgoTree(basisErgoTree) 72 | 73 | // contracts below are experimental and not finished ChainCash-on-Layer2 contracts 74 | 75 | val redemptionContract = scala.io.Source.fromFile("contracts/layer2-old/redemption.es", "utf-8").getLines.mkString("\n") 76 | val redemptionErgoTree = compile(redemptionContract) 77 | val redemptionAddress = getAddressFromErgoTree(redemptionErgoTree) 78 | 79 | val redemptionProducerContract = scala.io.Source.fromFile("contracts/layer2-old/redproducer.es", "utf-8").getLines.mkString("\n") 80 | val redemptionProducerErgoTree = compile(redemptionProducerContract) 81 | val redemptionProducerAddress = getAddressFromErgoTree(redemptionProducerErgoTree) 82 | } 83 | 84 | object Printer extends App { 85 | println("Basis p2s address: " + Constants.basisAddress) 86 | println("Redemption p2s address: " + Constants.redemptionAddress) 87 | println("Redemption producer p2s address: " + Constants.redemptionProducerAddress) 88 | 89 | // Example deployment info 90 | println("\nTo deploy Basis reserve:") 91 | println("1. Run BasisDeployer.main() for deployment requests") 92 | println("2. Use createBasisDeploymentRequest() with actual values") 93 | } 94 | -------------------------------------------------------------------------------- /contracts/layer2-old/note.es: -------------------------------------------------------------------------------- 1 | { 2 | // todo: rework note contracts to produce valid redemption, check that the new contract is compatible with redproducer 3 | 4 | // Note contract 5 | 6 | // It has two execution paths: 7 | 8 | // spend: full spending or with change 9 | 10 | // redeem: 11 | 12 | // to create a note, ... 13 | 14 | // box data: 15 | // R4 - history of ownership (under AVL+ tree), 16 | // tree contains reserveId as a key, signature as value, 17 | // and message is note value and token id 18 | // R5 - current holder of the note (public key given as a group element) 19 | // R6 - current length of the chain (as long int) 20 | // R7 - note value (as long int) 21 | 22 | val g: GroupElement = groupGenerator 23 | 24 | val history = SELF.R4[AvlTree].get 25 | 26 | val action = getVar[Byte](0).get 27 | 28 | val reserve = CONTEXT.dataInputs(0) 29 | val reserveId = reserve.tokens(0)._1 30 | 31 | if (action >= 0) { 32 | // spending path 33 | 34 | val holder = SELF.R5[GroupElement].get 35 | 36 | val selfOutput = OUTPUTS(action) 37 | 38 | // Message to be signed is position + note amount + note id 39 | val position = SELF.R6[Long].get 40 | val noteValueBytes = longToByteArray(SELF.R7[Long].get) 41 | val positionBytes = longToByteArray(position) 42 | val message = positionBytes ++ noteValueBytes ++ SELF.id 43 | 44 | // Computing challenge 45 | val e: Coll[Byte] = blake2b256(message) // weak Fiat-Shamir - todo: should be strong 46 | val eInt = byteArrayToBigInt(e) // challenge as big integer 47 | 48 | // a of signature in (a, z) 49 | val a = getVar[GroupElement](1).get 50 | val aBytes = a.getEncoded 51 | 52 | // z of signature in (a, z) 53 | val zBytes = getVar[Coll[Byte]](2).get 54 | val z = byteArrayToBigInt(zBytes) 55 | 56 | val properHolder = holder == reserve.R4[GroupElement].get 57 | 58 | // Signature is valid if g^z = a * x^e 59 | val properSignature = (g.exp(z) == a.multiply(holder.exp(eInt))) && properHolder 60 | 61 | val keyBytes = longToByteArray(position) ++ reserveId 62 | 63 | val valueBytes = aBytes ++ zBytes 64 | 65 | val keyVal = (keyBytes, valueBytes) 66 | val proof = getVar[Coll[Byte]](3).get 67 | 68 | val nextTree: Option[AvlTree] = history.insert(Coll(keyVal), proof) 69 | // This will fail if the operation failed or the proof is incorrect due to calling .get on the Option 70 | val outputDigest: Coll[Byte] = nextTree.get.digest 71 | 72 | def nextNoteCorrect(noteOut: Box): Boolean = { 73 | val insertionPerformed = noteOut.R4[AvlTree].get.digest == outputDigest 74 | val sameScript = noteOut.propositionBytes == SELF.propositionBytes 75 | val nextHolderDefined = noteOut.R5[GroupElement].isDefined 76 | val valuePreserved = noteOut.value >= SELF.value 77 | val positionIncreased = noteOut.R6[Long].get == (position + 1) 78 | 79 | positionIncreased && insertionPerformed && sameScript && nextHolderDefined && valuePreserved 80 | } 81 | 82 | val changeIdx = getVar[Byte](4) // optional index of change output 83 | 84 | val selfValue = SELF.R6[Long].get 85 | val selfOutValue = selfOutput.R6[Long].get 86 | 87 | val outputsValid = if(changeIdx.isDefined) { 88 | val changeOutput = OUTPUTS(changeIdx.get) 89 | 90 | val changeOutValue = changeOutput.R6[Long].get 91 | 92 | (selfOutValue + changeOutValue) <= selfValue && // burn allowed 93 | selfOutValue > 0 && 94 | changeOutValue > 0 && 95 | nextNoteCorrect(selfOutput) && 96 | nextNoteCorrect(changeOutput) 97 | } else { 98 | (selfOutValue == selfValue) && nextNoteCorrect(selfOutput) 99 | } 100 | 101 | sigmaProp(properSignature && outputsValid) 102 | } else { 103 | // redemption path 104 | 105 | // called by setting action variable to any negative value, -1 considered as standard by offchain apps 106 | 107 | val redeemOutput = OUTPUTS(0) 108 | val redemptionCorrect = redeemOutput.tokens(0)._1 == fromBase58("") //todo: redemption token id 109 | val historyCorrect = redeemOutput.R4[AvlTree].get == SELF.R4[AvlTree].get 110 | val redemptionDeadlineValid = redeemOutput.R5[Int].get >= HEIGHT + 1440 111 | 112 | sigmaProp(redemptionCorrect && historyCorrect && redemptionDeadlineValid) 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/server/model.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain.server 2 | 3 | import scala.util.Random 4 | 5 | /** 6 | * We consider that every ChainCash agent may be associated with a pubkey. In general, pubkey could be corresponding 7 | * to a complex cryptographic statement (e.g. provable via a sigma-protocol, like in Ergo). 8 | */ 9 | case class PubKey(value: String) 10 | 11 | /** 12 | * An issuer, also known as an agent, every participant in ChainCash monetary system can issue new money 13 | */ 14 | case class Issuer(pk: PubKey) 15 | 16 | /** 17 | * Signature which is proving knowledge of a secret corresponding to public key `pk` 18 | */ 19 | case class Signature(pk: PubKey) 20 | 21 | /** 22 | * Every agent may have reserve backing money spend or issued by the agent with collateral, or signalizing 23 | * trust which can be expressed towards the agent 24 | */ 25 | trait Reserve { 26 | val issuer: Issuer 27 | 28 | def usCentAmount: Long // in us cents 29 | } 30 | 31 | trait TrustBasedReserve extends Reserve { 32 | def usCentAmount: Long = 0L 33 | } 34 | 35 | trait CollateralBasedReserve extends Reserve 36 | 37 | case class ErgReserve(nanoergs: Long, issuer: Issuer) extends CollateralBasedReserve { 38 | override def usCentAmount = ??? 39 | } 40 | 41 | case class UsdReserve(usCentAmount: Long, issuer: Issuer) extends CollateralBasedReserve { 42 | val usd: Double = usCentAmount / 100.0 43 | } 44 | 45 | /** 46 | * A record added to a note when it is being spent 47 | */ 48 | case class BackingStatement(reserve: Reserve, signature: Signature) 49 | 50 | // amount is in usd cents for now 51 | case class Note(id: Long, amount: Long, backing: Seq[BackingStatement]) 52 | 53 | object Note { 54 | def apply(amount: Long, backing: Seq[BackingStatement]): Note = { 55 | Note(Random.nextLong(), amount, backing) 56 | } 57 | } 58 | 59 | trait Transaction 60 | 61 | case class ExchangeTransaction(input: Note, outputs: Seq[Note]) extends Transaction 62 | 63 | object ExchangeTransaction { 64 | def payment(from: Note, spenderReserve: Reserve, amount: Long): ExchangeTransaction = { 65 | require(from.amount >= amount, "Input can't cover the payment") 66 | val changeOutputOpt = if (from.amount == amount) { 67 | None 68 | } else { // > case 69 | Some(Note(from.amount - amount, from.backing)) 70 | } 71 | val newBackingStatement = BackingStatement(spenderReserve, Signature(spenderReserve.issuer.pk)) 72 | val paymentOutput = Note(amount, backing = from.backing ++ Seq(newBackingStatement)) 73 | ExchangeTransaction(from, paymentOutput +: changeOutputOpt.toSeq) 74 | } 75 | } 76 | 77 | case class MintingTransaction(output: Note) extends Transaction 78 | case class RedeemingTransaction(input: Note) extends Transaction 79 | 80 | 81 | trait AcceptanceFilter { 82 | def acceptable(note: Note): Boolean 83 | } 84 | 85 | case class NoteSet(set: Seq[Note]){ 86 | 87 | // inefficient and insecure 88 | def process(transaction: Transaction): NoteSet = transaction match { 89 | case ExchangeTransaction(input, outputs) => 90 | NoteSet(set.filter(n => n != input) ++ outputs) 91 | case MintingTransaction(output) => 92 | NoteSet(set :+ output) 93 | case RedeemingTransaction(input) => 94 | NoteSet(set.filter(n => n != input)) 95 | } 96 | } 97 | 98 | object NoteSet { 99 | def empty: NoteSet = NoteSet(Seq.empty) 100 | } 101 | 102 | object Tester extends App { 103 | def testingAcceptanceFilter(utxoSet: NoteSet): AcceptanceFilter = testingAcceptanceFilter(utxoSet.set) 104 | 105 | def testingAcceptanceFilter(circulatingNotes: Seq[Note]): AcceptanceFilter = { 106 | (note: Note) => { 107 | val noteReserves = note.backing.distinct 108 | val notesIntersecting = noteReserves.flatMap { r => circulatingNotes.filter(n => n.backing.contains(r)) } 109 | val notesIntersectingBacking = notesIntersecting.flatMap(_.backing.map(_.reserve).distinct).map(_.usCentAmount).sum 110 | notesIntersectingBacking > notesIntersecting.map(_.amount).sum 111 | } 112 | } 113 | 114 | val pk1 = PubKey("1") 115 | 116 | val issuer1 = Issuer(pk1) 117 | val issuer2 = Issuer(PubKey("2")) 118 | val issuer3 = Issuer(PubKey("3")) 119 | 120 | val reserve1 = UsdReserve(1000 * 100, issuer1) 121 | val reserve2 = UsdReserve(1500 * 100, issuer2) 122 | val reserve3 = UsdReserve(700 * 100, issuer3) 123 | 124 | var utxoSet = NoteSet.empty 125 | 126 | val tx1 = MintingTransaction(Note(1, 100 * 100, Seq(BackingStatement(reserve1, Signature(pk1))))) 127 | 128 | utxoSet = utxoSet.process(tx1) 129 | 130 | val filter1 = testingAcceptanceFilter(utxoSet) 131 | println(filter1.acceptable(utxoSet.set.head)) 132 | 133 | } -------------------------------------------------------------------------------- /docs/conf/sources.bib: -------------------------------------------------------------------------------- 1 | @inproceedings{saito2003peer, 2 | title={Peer-to-peer money: Free currency over the Internet}, 3 | author={Saito, Kenji}, 4 | booktitle={Web and Communication Technologies and Internet-Related Social Issues—HSI 2003: Second International Conference on Human. Society@ Internet Seoul, Korea, June 18--20, 2003 Proceedings 2}, 5 | pages={404--414}, 6 | year={2003}, 7 | organization={Springer} 8 | } 9 | 10 | @article{kiyotaki1989money, 11 | title={On money as a medium of exchange}, 12 | author={Kiyotaki, Nobuhiro and Wright, Randall}, 13 | journal={Journal of political Economy}, 14 | volume={97}, 15 | number={4}, 16 | pages={927--954}, 17 | year={1989}, 18 | publisher={The University of Chicago Press} 19 | } 20 | 21 | @inproceedings{chakravarty2020extended, 22 | title={The extended UTXO model}, 23 | author={Chakravarty, Manuel MT and Chapman, James and MacKenzie, Kenneth and Melkonian, Orestis and Peyton Jones, Michael and Wadler, Philip}, 24 | booktitle={Financial Cryptography and Data Security: FC 2020 International Workshops, AsiaUSEC, CoDeFi, VOTING, and WTSC, Kota Kinabalu, Malaysia, February 14, 2020, Revised Selected Papers 24}, 25 | pages={525--539}, 26 | year={2020}, 27 | organization={Springer} 28 | } 29 | 30 | @misc{kya, 31 | title = {Know Your Assumptions}, 32 | howpublished = {\url{https://www.ergoforum.org/t/know-your-assumptions/4198}}, 33 | note = {Accessed: 2023-01-30} 34 | } 35 | 36 | @article{machlup1970euro, 37 | title={Euro-dollar creation: A mystery story}, 38 | author={Machlup, Fritz}, 39 | journal={PSL Quarterly Review}, 40 | volume={23}, 41 | number={94}, 42 | year={1970} 43 | } 44 | 45 | @article{nakamoto2008peer, 46 | title={A peer-to-peer electronic cash system}, 47 | author={Nakamoto, Satoshi and Bitcoin, A}, 48 | journal={Bitcoin.--URL: https://bitcoin.org/bitcoin.pdf}, 49 | volume={4}, 50 | number={2}, 51 | year={2008} 52 | } 53 | 54 | @article{williams1996new, 55 | title={The new barter economy: an appraisal of Local Exchange and Trading Systems (LETS)}, 56 | author={Williams, Colin C}, 57 | journal={Journal of Public Policy}, 58 | volume={16}, 59 | number={1}, 60 | pages={85--101}, 61 | year={1996}, 62 | publisher={Cambridge University Press} 63 | } 64 | 65 | @article{unterguggenbercer1934end, 66 | title={THE END RESULTS OF THE WOERGL EXPERIMENT.}, 67 | author={Unterguggenbercer, Michael}, 68 | journal={Annals of Public and Cooperative Economics}, 69 | volume={10}, 70 | number={1}, 71 | pages={60--63}, 72 | year={1934}, 73 | publisher={Wiley Online Library} 74 | } 75 | 76 | @misc{impl, 77 | title = {ChainCash implementation}, 78 | howpublished = {\url{https://github.com/kushti/chaincash/tree/master/contracts}}, 79 | note = {Accessed: 2023-02-22} 80 | } 81 | 82 | @misc{contracts, 83 | title = {ChainCash contracts}, 84 | howpublished = {\url{https://github.com/ChainCashLabs/chaincash/tree/master/contracts/onchain}}, 85 | note = {Accessed: 2023-11-11} 86 | } 87 | 88 | @misc{server, 89 | title = {ChainCash server}, 90 | howpublished = {\url{https://github.com/ChainCashLabs/chaincash-rs}}, 91 | note = {Accessed: 2023-11-11} 92 | } 93 | 94 | @techreport{chepurnoy2019contractual, 95 | title={On Contractual Money}, 96 | author={Chepurnoy, Alexander and Saxena, Amitabh}, 97 | year={2019}, 98 | institution={EasyChair} 99 | } 100 | 101 | @article{bottazzi2024multilateral, 102 | title={Multilateral Trade Credit Set-off in MPC via Graph Anonymization and Network Simplex}, 103 | author={Bottazzi, Enrico and Ngo, Chan Nam and Tsutsumi, Masato}, 104 | journal={Cryptology ePrint Archive}, 105 | year={2024} 106 | } 107 | 108 | @article{mcquaid2004review, 109 | title={A Review of Local Exchange and Trading Schemes (LETS) and Time Banks in Scotland}, 110 | author={McQuaid, Ronald and Bond, Sue and Christy, Beverley}, 111 | year={2004}, 112 | publisher={Employment Research Institute} 113 | } 114 | 115 | 116 | @article{saito2005wot, 117 | title={WOT for WAT: Spinning the web of trust for peer-to-peer barter relationships}, 118 | author={Saito, Kenji}, 119 | journal={IEICE transactions on communications}, 120 | volume={88}, 121 | number={4}, 122 | pages={1503--1510}, 123 | year={2005}, 124 | publisher={The Institute of Electronics, Information and Communication Engineers} 125 | } 126 | 127 | @inproceedings{saito2005multiplication, 128 | title={Multiplication over time to facilitate peer-to-peer barter relationships}, 129 | booktitle={16th International Workshop on Database and Expert Systems Applications (DEXA'05)}, 130 | pages={785--789}, 131 | year={2005}, 132 | organization={IEEE} 133 | } 134 | 135 | @article{saito2006reduction, 136 | title={Reduction over time to facilitate peer-to-peer barter relationships}, 137 | author={Saito, Kenji and Morino, Eiichi and Murai, Jun}, 138 | journal={IEICE TRANSACTIONS on Information and Systems}, 139 | volume={89}, 140 | number={1}, 141 | pages={181--188}, 142 | year={2006}, 143 | publisher={The Institute of Electronics, Information and Communication Engineers} 144 | } 145 | 146 | @incollection{woltzenlogel2023stablecoin, 147 | title={Stablecoin}, 148 | author={Woltzenlogel Paleo, Bruno}, 149 | booktitle={Encyclopedia of Cryptography, Security and Privacy}, 150 | pages={1--5}, 151 | year={2023}, 152 | publisher={Springer} 153 | } 154 | 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChainCash - elastic peer-to-peer money creation via trust and blockchain assets 2 | 3 | This repository contains whitepaper and some prototyping code for 4 | ChainCash, a protocol to create money in self-sovereign way via trust or collateral, with collective backing and 5 | individual acceptance. 6 | 7 | ## Intro 8 | 9 | We consider money as a set of digital notes, and every note is collectively backed 10 | by all the previous spenders of the note. Every agent may create reserves to be used 11 | as collateral. When an agent spends note, whether received previously from another 12 | agent or just created by the agent itself, it is attaching its signature to it. 13 | A note could be redeemed at any time against any of reserves of agents previously 14 | signed the note. We allow an agent 15 | to issue and spend notes without a reserve. It is up to agent's counter-parties 16 | then whether to accept and so back an issued note with collateral or agent's 17 | trust or not. 18 | 19 | As an example, consider a small gold mining cooperative in Ghana issuing a 20 | note backed by (tokenized) gold. The note is then accepted by the national government as mean of tax payment. Then the government is using the note (which 21 | is now backed by gold and also trust in Ghana government, so, e.g. convertible 22 | to Ghanaian Cedi as well) to buy oil from a Saudi oil company. Then the oil 23 | company, having its own oil reserve also, is using the note to buy equipment 24 | from China. Now a Chinese company has a note which is backed by gold, oil, 25 | and Cedis. 26 | 27 | Economic agent's individual note quality estimation predicate Pi(n) is considering collaterals 28 | and trust of previous spenders. Different agents may have different collateralization 29 | estimation algorithm (by analyzing history of the single note n, or 30 | all the notes known), different whitelists, blacklists, or trust scores assigned to 31 | previous spenders of the note n etc. So in general case payment sender first 32 | need to consult with the receiver on whether the payment (consisting of one or 33 | multiple notes) can be accepted. However, in the real world likely there will be 34 | standard predicates, thus payment receiver (e.g. an online shop) may publish 35 | its predicate (or just predicate id) online, and then the payment can be done 36 | without prior interaction. 37 | 38 | ## Whitepaper And Other Materials 39 | 40 | High-level description of ChainCash protocol and its implementation can be found 41 | in the [whitepaper](https://github.com/ChainCashLabs/chaincash/blob/master/docs/conf/conf.pdf). 42 | 43 | More introductory materials: 44 | 45 | * [The World Needs For More Collateral](https://www.ergoforum.org/t/the-world-needs-for-more-collateral/4451) - forum thread 46 | * [Video presentation from Ergo Summit](https://www.youtube.com/watch?v=NxIlIpO6ZVI) 47 | * [Video: ChainCash, part two](https://www.youtube.com/watch?v=fk8ZFvNFDYc) 48 | 49 | ## ChainCash Server 50 | 51 | ChainCash server is software acting as a self-sovereign bank with client-side notes 52 | validation. It can be found at [https://github.com/ChainCashLabs/chaincash-rs](https://github.com/ChainCashLabs/chaincash-rs). 53 | Initial version of [design document](docs/server.md) is also available. The server has basic HTTP API but no any UI. 54 | 55 | ## Contents Of This Repository 56 | 57 | * Whitepaper - https://github.com/ChainCashLabs/chaincash/blob/master/docs/conf/conf.pdf 58 | High-level description of ChainCash protocol and its implementation 59 | 60 | * Contracts - https://github.com/kushti/chaincash/tree/master/contracts - note and reserve contracts in ErgoScript 61 | 62 | * Modelling - https://github.com/kushti/chaincash/tree/master/src/main/scala/chaincash/model 63 | Contract-less and blockchain-less models of ChainCash entities and one of notes collateralization 64 | estimation options. 65 | * Tests - https://github.com/kushti/chaincash/blob/master/src/test/scala/kiosk/ChainCashSpec.scala - Kiosk-based tests for transactions involving note 66 | contracts (note creation, spending, redemption) 67 | * Offchain part - https://github.com/kushti/chaincash/tree/master/src/main/scala/chaincash/offchain - on-chain data tracking, 68 | persistence, transaction builders. This is very rough prototype, at the moment better to look into ChainCash Server which 69 | is available at [https://github.com/ChainCashLabs/chaincash-rs](https://github.com/ChainCashLabs/chaincash-rs) . 70 | 71 | ## Communications 72 | 73 | Join discussion groups for developers and users: 74 | 75 | * Telegram: [https://t.me/chaincashtalks](https://t.me/chaincashtalks) 76 | 77 | ## Deployment Utilities 78 | 79 | ### Basis Reserve Contract Deployment 80 | 81 | The repository includes deployment utilities for the Basis reserve contract: 82 | 83 | ```scala 84 | // Run deployment utility 85 | sbt 'runMain chaincash.contracts.BasisDeployer' 86 | 87 | // Or use the contract printer 88 | sbt 'runMain chaincash.contracts.Constants$Printer' 89 | ``` 90 | 91 | This generates deployment requests for the Basis reserve contract, which supports: 92 | - Off-chain payments with credit creation 93 | - Redemption with 2% fee 94 | - Emergency redemption after 7 days 95 | - Tracker-based debt tracking 96 | 97 | See `src/main/scala/chaincash/contracts/README.md` for detailed usage. 98 | 99 | ## TODO 100 | 101 | * update ReserveData.liabilities and reserveKeys in offchain code 102 | * offchain code for note redemption 103 | * support few spendings of a note in the same block (offchain tracking of it) 104 | * support other tokens in reserves, e.g. SigUSD 105 | * efficient persistence for own notes (currently, all the notes in the system are iterated over) 106 | * check ERG preservation in note contracts 107 | -------------------------------------------------------------------------------- /contracts/onchain/reserve.es: -------------------------------------------------------------------------------- 1 | { 2 | // Contract for reserve (in ERG only) 3 | 4 | // Data: 5 | // - token #0 - identifying singleton token 6 | // - R4 - signing key (as a group element) 7 | // - R5 - tree of all the note tokens issued TODO: check preservation in actions 8 | // 9 | // Actions: 10 | // - redeem note (#0) 11 | // - top up (#1) 12 | // - mint note (#2) 13 | 14 | val v = getVar[Byte](0).get 15 | val action = v / 10 16 | val index = v % 10 17 | 18 | val ownerKey = SELF.R4[GroupElement].get // reserve owner's key, used in notes and unlock/lock/refund actions 19 | val selfOut = OUTPUTS(index) 20 | 21 | // common checks for all the paths (not incl. ERG value check) 22 | val selfPreserved = 23 | selfOut.propositionBytes == SELF.propositionBytes && 24 | selfOut.tokens == SELF.tokens && 25 | selfOut.R4[GroupElement].get == SELF.R4[GroupElement].get 26 | 27 | if (action == 0) { 28 | // redemption path 29 | 30 | // OUTPUTS: 31 | // #1 - receipt 32 | // #2 - buyback 33 | 34 | val g: GroupElement = groupGenerator 35 | 36 | // if set, re-redemption against receipt data is done, otherwise, a note is redeemed 37 | val receiptMode = getVar[Boolean](4).get 38 | 39 | // read note data if receiptMode == false, receipt data otherwise 40 | val noteInput = INPUTS(index) 41 | val noteTokenId = noteInput.tokens(0)._1 42 | val noteValue = noteInput.tokens(0)._2 // 1 token == 1 mg of gold 43 | val history = noteInput.R4[AvlTree].get 44 | val reserveId = SELF.tokens(0)._1 45 | 46 | // oracle provides gold price in nanoErg per kg in its R4 register 47 | val goldOracle = CONTEXT.dataInputs(0) 48 | // todo: externalize oracle NFT id 49 | // the ID below is from the mainnet 50 | val properOracle = goldOracle.tokens(0)._1 == fromBase16("3c45f29a5165b030fdb5eaf5d81f8108f9d8f507b31487dd51f4ae08fe07cf4a") 51 | val oracleRate = goldOracle.R4[Long].get / 1000000 // normalize to nanoerg per mg of gold 52 | 53 | // 2% redemption fee 54 | val maxToRedeem = noteValue * oracleRate * 98 / 100 55 | val redeemed = SELF.value - selfOut.value 56 | 57 | // 0.2% going to buyback contract to support oracles network 58 | val buyBackCorrect = if (redeemed > 0) { 59 | val toOracle = redeemed * 2 / 1000 60 | // todo: externalize buyback NFT id 61 | // the ID below is from the mainnet 62 | val buyBackNFTId = fromBase16("bf24ed4af7eb5a7839c43aa6b240697d81b196120c837e1a941832c266d3755c") 63 | val buyBackInput = INPUTS(2) 64 | val buyBackOutput = OUTPUTS(2) 65 | 66 | buyBackInput.tokens(0)._1 == buyBackNFTId && 67 | buyBackOutput.tokens(0)._1 == buyBackNFTId && 68 | (buyBackOutput.value - buyBackInput.value) >= toOracle 69 | } else { 70 | true 71 | } 72 | val redeemCorrect = (redeemed <= maxToRedeem) && buyBackCorrect 73 | 74 | val position = getVar[Long](3).get 75 | val positionBytes = longToByteArray(position) 76 | 77 | val proof = getVar[Coll[Byte]](1).get 78 | val key = positionBytes ++ reserveId 79 | val value = history.get(key, proof).get 80 | 81 | val aBytes = value.slice(0, 33) 82 | val zBytes = value.slice(33, value.size) 83 | val a = decodePoint(aBytes) 84 | val z = byteArrayToBigInt(zBytes) 85 | 86 | val maxValueBytes = getVar[Coll[Byte]](2).get 87 | 88 | val message = positionBytes ++ maxValueBytes ++ noteTokenId 89 | val maxValue = byteArrayToLong(maxValueBytes) 90 | 91 | // Computing challenge 92 | val e: Coll[Byte] = blake2b256(aBytes ++ message ++ ownerKey.getEncoded) // strong Fiat-Shamir 93 | val eInt = byteArrayToBigInt(e) // challenge as big integer 94 | 95 | // Signature is valid if g^z = a * x^e 96 | val properSignature = (g.exp(z) == a.multiply(ownerKey.exp(eInt))) && 97 | noteValue <= maxValue 98 | 99 | // we check that receipt is properly formed, but we do not check receipt's contract here, 100 | // to avoid circular dependency as receipt contract depends on (hash of) our contract, 101 | // thus we are checking receipt contract in note and receipt contracts 102 | val receiptOutIndex = if (redeemed == 0) { 103 | getVar[Int](5).get 104 | } else { 105 | 1 106 | } 107 | val receiptOut = OUTPUTS(receiptOutIndex) 108 | val properReceipt = 109 | receiptOut.tokens(0) == noteInput.tokens(0) && 110 | receiptOut.R4[AvlTree].get == history && 111 | receiptOut.R5[Long].get == position && 112 | receiptOut.R6[Int].get >= HEIGHT - 20 && // 20 blocks for inclusion 113 | receiptOut.R6[Int].get <= HEIGHT && 114 | receiptOut.R7[GroupElement].get == ownerKey 115 | 116 | // todo: could this be checked all the time ? if so then receiptMode can be eliminated 117 | val positionCorrect = if (receiptMode) { 118 | position < noteInput.R5[Long].get 119 | } else { 120 | true 121 | } 122 | 123 | sigmaProp(selfPreserved && properOracle && redeemCorrect && properSignature && properReceipt && positionCorrect) 124 | } else if (action == 1) { 125 | // top up 126 | // todo: check R5 preservation 127 | sigmaProp(selfPreserved && (selfOut.value - SELF.value >= 1000000000)) // at least 1 ERG added 128 | } else if (action == 2) { 129 | // issue a note 130 | // todo: check R5 preservation 131 | sigmaProp(selfPreserved) 132 | } else { 133 | sigmaProp(false) 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/DbEntities.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import com.google.common.primitives.{Longs, Shorts} 4 | import org.bouncycastle.util.BigIntegers 5 | import org.ergoplatform.ErgoBox 6 | import org.ergoplatform.wallet.boxes.ErgoBoxSerializer 7 | import scorex.util.ModifierId 8 | import scorex.util.encode.Base16 9 | import sigmastate.eval.CGroupElement 10 | import sigmastate.serialization.GroupElementSerializer 11 | import swaydb.{Glass, _} 12 | import swaydb.data.slice.Slice 13 | import swaydb.serializers.Default._ 14 | import swaydb.serializers.Serializer 15 | import TrackingTypes._ 16 | import sigmastate.basics.CryptoConstants.EcPointType 17 | 18 | object DbEntities { 19 | 20 | val heightKey = "height" 21 | 22 | val oracleRateKey = "goldprice" 23 | 24 | implicit object EcPointSerializer extends Serializer[EcPointType] { 25 | override def write(modifierId: EcPointType): Slice[Byte] = 26 | ByteArraySerializer.write(GroupElementSerializer.toBytes(modifierId)) 27 | 28 | override def read(slice: Slice[Byte]): EcPointType = { 29 | val bytes = ByteArraySerializer.read(slice) 30 | GroupElementSerializer.fromBytes(bytes) 31 | } 32 | } 33 | 34 | implicit object ModifierIdSerializer extends Serializer[ModifierId] { 35 | override def write(modifierId: ModifierId): Slice[Byte] = 36 | StringSerializer.write(modifierId) 37 | 38 | override def read(slice: Slice[Byte]): ModifierId = 39 | ModifierId @@ StringSerializer.read(slice) 40 | } 41 | 42 | implicit object BoxSerializer extends Serializer[ErgoBox] { 43 | override def write(box: ErgoBox): Slice[Byte] = 44 | ByteArraySerializer.write(ErgoBoxSerializer.toBytes(box)) 45 | 46 | override def read(slice: Slice[Byte]): ErgoBox = { 47 | val bytes = ByteArraySerializer.read(slice) 48 | ErgoBoxSerializer.parseBytes(bytes) 49 | } 50 | } 51 | 52 | implicit object NoteDataSerializer extends Serializer[NoteData] { 53 | override def write(noteData: NoteData): Slice[Byte] = { 54 | val boxBytes = ErgoBoxSerializer.toBytes(noteData.currentUtxo) 55 | val boxBytesCount = Shorts.toByteArray(boxBytes.length.toShort) 56 | val historyBytes = noteData.history.foldLeft(Array.emptyByteArray) { case (acc, sd) => 57 | acc ++ SigDataSerializer.toBytes(sd) 58 | } 59 | ByteArraySerializer.write(boxBytesCount ++ boxBytes ++ historyBytes) 60 | } 61 | 62 | override def read(slice: Slice[Byte]): NoteData = { 63 | val bytes = ByteArraySerializer.read(slice) 64 | val boxBytesCount = Shorts.fromByteArray(bytes.take(2)) 65 | val boxBytes = bytes.slice(2, 2 + boxBytesCount) 66 | val box = ErgoBoxSerializer.parseBytes(boxBytes) 67 | val historyBs = bytes.slice(2 + boxBytesCount, bytes.length) 68 | val history = historyBs.grouped(89).map(bs => SigDataSerializer.fromBytes(bs)).toIndexedSeq 69 | NoteData(box, history) 70 | } 71 | } 72 | 73 | object SigDataSerializer { 74 | def toBytes(sigData: SigData): Array[Byte] = { 75 | Base16.decode(sigData.reserveId).get ++ 76 | Longs.toByteArray(sigData.valueBacked) ++ 77 | GroupElementSerializer.toBytes(sigData.a) ++ 78 | BigIntegers.asUnsignedByteArray(32, sigData.z.bigInteger) 79 | } 80 | 81 | def fromBytes(bytes: Array[Byte]): SigData = { 82 | val ri = bytes.slice(0, 16) 83 | val vb = bytes.slice(16, 24) 84 | val a = bytes.slice(24, 57) 85 | val z = bytes.slice(57, 89) 86 | 87 | SigData( 88 | ModifierId @@ Base16.encode(ri), 89 | Longs.fromByteArray(vb), 90 | GroupElementSerializer.fromBytes(a), 91 | BigIntegers.fromUnsignedByteArray(z)) 92 | } 93 | } 94 | 95 | implicit object ReserveDataSerializer extends Serializer[ReserveData] { 96 | override def write(reserveData: ReserveData): Slice[Byte] = { 97 | val boxBytes = ErgoBoxSerializer.toBytes(reserveData.reserveBox) 98 | val boxBytesCount = Shorts.toByteArray(boxBytes.length.toShort) 99 | val lialibilitesBytes = Longs.toByteArray(reserveData.liabilites) 100 | val notesBytes = reserveData.signedUnspentNotes.foldLeft(Array.emptyByteArray) { case (acc, id) => 101 | acc ++ Base16.decode(id).get 102 | } 103 | ByteArraySerializer.write(boxBytesCount ++ boxBytes ++ lialibilitesBytes ++ notesBytes) 104 | } 105 | 106 | override def read(slice: Slice[Byte]): ReserveData = { 107 | val bytes = ByteArraySerializer.read(slice) 108 | val boxBytesCount = Shorts.fromByteArray(bytes.take(2)) 109 | val boxBytes = bytes.slice(2, 2 + boxBytesCount) 110 | val box = ErgoBoxSerializer.parseBytes(boxBytes) 111 | val lialibilitesBytes = bytes.slice(2 + boxBytesCount, 10 + boxBytesCount) 112 | val liabilities = Longs.fromByteArray(lialibilitesBytes) 113 | val historyBs = bytes.slice(2 + boxBytesCount, bytes.length) 114 | val notes = historyBs.grouped(32).map(bs => ModifierId @@ Base16.encode(bs)).toIndexedSeq 115 | ReserveData(box, notes, liabilities) 116 | } 117 | } 118 | 119 | 120 | val issuedNotes = persistent.Map[NoteTokenId, ErgoBox, Nothing, Glass](dir = "db/issued_notes") 121 | val unspentNotes = persistent.Map[NoteId, NoteData, Nothing, Glass](dir = "db/unspent_notes") 122 | val reserves = persistent.Map[ReserveNftId, ReserveData, Nothing, Glass](dir = "db/reserves") 123 | val state = persistent.Map[String, String, Nothing, Glass](dir = "db/state") 124 | 125 | // ecpoint -> reserve index 126 | val reserveKeys = persistent.Map[EcPointType, ReserveNftId, Nothing, Glass](dir = "db/reserveKeys") 127 | 128 | val myReserves = persistent.Set[ReserveNftId, Nothing, Glass](dir = "db/my-reserves") 129 | //todo: save myNotes as well ? 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/offchain/NoteUtils.scala: -------------------------------------------------------------------------------- 1 | package chaincash.offchain 2 | 3 | import chaincash.contracts.Constants 4 | import chaincash.contracts.Constants.{noteErgoTree, reserveErgoTree} 5 | import chaincash.offchain.TrackingTypes.NoteData 6 | import com.google.common.primitives.Longs 7 | import io.circe.syntax.EncoderOps 8 | import org.ergoplatform.ErgoBox.{R4, R5, R6} 9 | import org.ergoplatform.sdk.wallet.Constants.eip3DerivationPath 10 | import org.ergoplatform.sdk.wallet.secrets.ExtendedSecretKey 11 | import org.ergoplatform.sdk.wallet.settings.EncryptionSettings 12 | import org.ergoplatform.wallet.interface4j.SecretString 13 | import org.ergoplatform.wallet.secrets.JsonSecretStorage 14 | import org.ergoplatform.wallet.settings.SecretStorageSettings 15 | import org.ergoplatform.{DataInput, ErgoBoxCandidate, P2PKAddress, UnsignedErgoLikeTransaction, UnsignedInput} 16 | import scorex.crypto.hash.Digest32 17 | import scorex.util.encode.Base16 18 | import sigmastate.Values.{AvlTreeConstant, ByteArrayConstant, ByteConstant, GroupElementConstant, LongConstant} 19 | import sigmastate.eval.Colls 20 | import sigmastate.interpreter.ContextExtension 21 | import sigmastate.eval._ 22 | import sigmastate.serialization.GroupElementSerializer 23 | import special.sigma.GroupElement 24 | 25 | trait NoteUtils extends WalletUtils { 26 | // create note with nominal of `amountMg` mg of gold 27 | def createNote(amountMg: Long, ownerPubkey: GroupElement, changeAddress: P2PKAddress): Unit = { 28 | val inputs = fetchInputs().take(60) 29 | val creationHeight = inputs.map(_.creationHeight).max 30 | val noteTokenId = Digest32 @@ inputs.head.id.toArray 31 | 32 | val inputValue = inputs.map(_.value).sum 33 | require(inputValue >= feeValue * 21) 34 | 35 | val noteAmount = feeValue * 20 36 | 37 | val noteOut = new ErgoBoxCandidate( 38 | noteAmount, 39 | noteErgoTree, 40 | creationHeight, 41 | Colls.fromItems((Digest32Coll @@ Colls.fromArray(noteTokenId)) -> amountMg), 42 | Map( 43 | R4 -> AvlTreeConstant(Constants.emptyTree), 44 | R5 -> GroupElementConstant(ownerPubkey), 45 | R6 -> LongConstant(0) 46 | ) 47 | ) 48 | val feeOut = createFeeOut(creationHeight) 49 | val changeOutOpt = if(inputValue > 21 * feeValue) { 50 | val changeValue = inputValue - (21 * feeValue) 51 | Some(new ErgoBoxCandidate(changeValue, changeAddress.script, creationHeight)) 52 | } else { 53 | None 54 | } 55 | 56 | val unsignedInputs = inputs.map(box => new UnsignedInput(box.id, ContextExtension.empty)) 57 | val outs = Seq(noteOut, feeOut) ++ changeOutOpt.toSeq 58 | val tx = new UnsignedErgoLikeTransaction(unsignedInputs.toIndexedSeq, IndexedSeq.empty, outs.toIndexedSeq) 59 | println(tx.asJson) 60 | } 61 | 62 | def createNote(amountMg: Long): Unit = { 63 | val changeAddress = fetchChangeAddress() 64 | createNote(amountMg, changeAddress.pubkey.value, changeAddress) 65 | } 66 | 67 | private def readSecret(): ExtendedSecretKey ={ 68 | val sss = SecretStorageSettings("secrets", EncryptionSettings("HmacSHA256", 128000, 256)) 69 | val jss = JsonSecretStorage.readFile(sss).get 70 | jss.unlock(SecretString.create("wpass")) 71 | val masterKey = jss.secret.get 72 | masterKey.derive(eip3DerivationPath) 73 | } 74 | 75 | def sendNote(noteData: NoteData, to: GroupElement) = { 76 | val changeAddress = fetchChangeAddress() 77 | 78 | val noteInputBox = noteData.currentUtxo 79 | val p2pkInputs = fetchInputs().take(5) // to pay fees 80 | val inputs = Seq(noteInputBox) ++ p2pkInputs 81 | val creationHeight = inputs.map(_.creationHeight).max 82 | 83 | val inputValue = inputs.map(_.value).sum 84 | 85 | val noteRecord = noteInputBox.additionalTokens.toArray.head 86 | val noteTokenId = noteRecord._1 87 | val noteAmount = noteRecord._2 88 | 89 | val secret = readSecret() 90 | val msg: Array[Byte] = Longs.toByteArray(noteAmount) ++ noteTokenId.toArray 91 | val sig = SigUtils.sign(msg, secret.privateInput.w) 92 | secret.zeroSecret() 93 | val sigBytes = GroupElementSerializer.toBytes(sig._1) ++ sig._2.toByteArray 94 | 95 | // todo: likely should be passed from outside 96 | val reserveId = myReserveIds().head 97 | val reserveIdBytes = Base16.decode(reserveId).get 98 | val reserveBox = DbEntities.reserves.get(reserveId).get.reserveBox 99 | 100 | val prover = noteData.restoreProver 101 | val insertProof = prover.insert(reserveIdBytes -> sigBytes).proof 102 | val updTree = prover.ergoValue.getValue 103 | 104 | val noteOut = new ErgoBoxCandidate( 105 | noteInputBox.value, 106 | noteErgoTree, 107 | creationHeight, 108 | Colls.fromItems(noteTokenId -> noteAmount), 109 | Map(R4 -> AvlTreeConstant(updTree), R5 -> GroupElementConstant(to)) 110 | ) 111 | 112 | val noteInput = new UnsignedInput(noteInputBox.id, ContextExtension(Map( 113 | 0.toByte -> ByteConstant(0), 114 | 1.toByte -> GroupElementConstant(sig._1), 115 | 2.toByte -> ByteArrayConstant(sig._2.toByteArray), 116 | 3.toByte -> ByteArrayConstant(insertProof.bytes) 117 | ))) 118 | 119 | val feeOut = createFeeOut(creationHeight) 120 | val changeValue = inputValue - noteOut.value - feeOut.value 121 | val changeOut = new ErgoBoxCandidate(changeValue, changeAddress.script, creationHeight) 122 | val outs = IndexedSeq(noteOut, changeOut, feeOut) 123 | 124 | val unsignedInputs = Seq(noteInput) ++ p2pkInputs.map(box => new UnsignedInput(box.id, ContextExtension.empty)) 125 | 126 | val dataInputs = IndexedSeq(DataInput(reserveBox.id)) 127 | 128 | val tx = new UnsignedErgoLikeTransaction(unsignedInputs.toIndexedSeq, dataInputs, outs.toIndexedSeq) 129 | println(tx.asJson) 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/test/resources/dummy-program.json: -------------------------------------------------------------------------------- 1 | { 2 | "constants": [ 3 | { 4 | "name": "myLong1", 5 | "type": "Long", 6 | "value": "1234" 7 | }, 8 | { 9 | "name": "myCollByte", 10 | "type": "CollByte", 11 | "value": "506dfb0a34d44f2baef77d99f9da03b1f122bdc4c7c31791a0c706e23f1207e7" 12 | }, 13 | { 14 | "name": "myInt", 15 | "type": "Int", 16 | "value": "1234" 17 | }, 18 | { 19 | "name": "myTokenId", 20 | "type": "CollByte", 21 | "value": "ae57e4add0f181f5d1e8fd462969e4cc04f13b0da183676660d280ad0b64563f" 22 | }, 23 | { 24 | "name": "myGroupElement", 25 | "type": "GroupElement", 26 | "value": "028182257d34ec7dbfedee9e857aadeb8ce02bb0c757871871cff378bb52107c67" 27 | }, 28 | { 29 | "name": "myErgoTree1", 30 | "type": "ErgoTree", 31 | "value": "10010101D17300" 32 | }, 33 | { 34 | "name": "myAddress", 35 | "type": "Address", 36 | "value": "9f5ZKbECVTm25JTRQHDHGM5ehC8tUw5g1fCBQ4aaE792rWBFrjK" 37 | } 38 | ], 39 | "dataInputs": [ 40 | { 41 | "id": { 42 | "value": "myCollByte" 43 | }, 44 | "address": { 45 | "name": "myAddressName" 46 | }, 47 | "registers": [ 48 | { 49 | "name": "myRegister3", 50 | "num": "R4", 51 | "type": "CollByte" 52 | } 53 | ], 54 | "tokens": [ 55 | { 56 | "index": 1, 57 | "id": { 58 | "name": "myToken1Id" 59 | }, 60 | "amount": { 61 | "name": "someLong1" 62 | } 63 | } 64 | ], 65 | "nanoErgs": { 66 | "name": "input1NanoErgs" 67 | } 68 | }, 69 | { 70 | "address": { 71 | "value": "myAddress" 72 | }, 73 | "registers": [ 74 | { 75 | "name": "myRegister4", 76 | "num": "R4", 77 | "type": "CollByte" 78 | } 79 | ], 80 | "tokens": [ 81 | { 82 | "index": 1, 83 | "id": { 84 | "name": "unreferencedToken2Id" 85 | }, 86 | "amount": { 87 | "value": "myLong1", 88 | "filter": "Gt" 89 | } 90 | } 91 | ], 92 | "nanoErgs": { 93 | "value": "input1NanoErgs", 94 | "filter": "Ne" 95 | }, 96 | "options": [ 97 | "Strict" 98 | ] 99 | } 100 | ], 101 | "inputs": [ 102 | { 103 | "address": { 104 | "value": "myAddress" 105 | }, 106 | "registers": [ 107 | { 108 | "name": "myRegister1", 109 | "num": "R4", 110 | "type": "CollByte" 111 | }, 112 | { 113 | "name": "myRegister2", 114 | "num": "R4", 115 | "type": "CollByte" 116 | } 117 | ], 118 | "tokens": [ 119 | { 120 | "index": 1, 121 | "id": { 122 | "name": "randomName" 123 | }, 124 | "amount": { 125 | "name": "someLong3" 126 | } 127 | } 128 | ], 129 | "nanoErgs": { 130 | "value": "someLong1", 131 | "filter": "Ge" 132 | }, 133 | "options": [ 134 | "Strict" 135 | ] 136 | } 137 | ], 138 | "fee": 10000, 139 | "binaryOps": [ 140 | { 141 | "name": "myLong2", 142 | "first": "myLong1", 143 | "op": "Add", 144 | "second": "myIntToLong" 145 | }, 146 | { 147 | "name": "myLong3", 148 | "first": "myLong2", 149 | "op": "Max", 150 | "second": "myLong1" 151 | }, 152 | { 153 | "name": "myLong4", 154 | "first": "myLong2", 155 | "op": "Add", 156 | "second": "myLong3" 157 | }, 158 | { 159 | "name": "myLong5", 160 | "first": "myLong4", 161 | "op": "Add", 162 | "second": "myLong2" 163 | }, 164 | { 165 | "name": "myLong6", 166 | "first": "myLong5", 167 | "op": "Add", 168 | "second": "myLong4" 169 | } 170 | ], 171 | "unaryOps": [ 172 | { 173 | "name": "myLong7", 174 | "from": "myLong2", 175 | "op": "Neg" 176 | }, 177 | { 178 | "name": "myLong8", 179 | "from": "myLong7", 180 | "op": "Neg" 181 | }, 182 | { 183 | "name": "myErgoTree2", 184 | "from": "myGroupElement", 185 | "op": "ProveDlog" 186 | }, 187 | { 188 | "name": "myCollByte2", 189 | "from": "myErgoTree2", 190 | "op": "ToCollByte" 191 | }, 192 | { 193 | "name": "myIntToLong", 194 | "from": "myInt", 195 | "op": "ToLong" 196 | } 197 | ], 198 | "returns": [ 199 | "myRegister4", 200 | "myCollByte2", 201 | "someLong3", 202 | "myLong4", 203 | "myCollByte", 204 | "myLong7", 205 | "myRegister1", 206 | "myToken1Id", 207 | "myLong6", 208 | "myAddressName", 209 | "myErgoTree1", 210 | "randomName", 211 | "myRegister3", 212 | "myInt", 213 | "myLong3", 214 | "input1NanoErgs", 215 | "myGroupElement", 216 | "someLong1", 217 | "myLong5", 218 | "myLong8", 219 | "myTokenId", 220 | "myRegister2", 221 | "myIntToLong", 222 | "myLong2", 223 | "myAddress", 224 | "HEIGHT", 225 | "unreferencedToken2Id", 226 | "myErgoTree2", 227 | "myLong1", 228 | "myRegister4", 229 | "myCollByte2", 230 | "someLong3", 231 | "myLong4", 232 | "myCollByte", 233 | "myLong7", 234 | "myRegister1", 235 | "myToken1Id", 236 | "myLong6", 237 | "myAddressName", 238 | "myErgoTree1", 239 | "randomName", 240 | "myRegister3", 241 | "myInt", 242 | "myLong3", 243 | "input1NanoErgs", 244 | "myGroupElement", 245 | "someLong1", 246 | "myLong5", 247 | "myLong8", 248 | "myTokenId", 249 | "myRegister2", 250 | "myIntToLong", 251 | "myLong2", 252 | "myAddress", 253 | "HEIGHT", 254 | "unreferencedToken2Id", 255 | "myErgoTree2", 256 | "myLong1" 257 | ] 258 | } -------------------------------------------------------------------------------- /contracts/onchain/note.es: -------------------------------------------------------------------------------- 1 | { 2 | // Note contract 3 | 4 | // It has two execution paths: 5 | 6 | // spend: full spending or with change 7 | 8 | // redeem: 9 | 10 | // when redemption, receipt is created which is allowing to do another redemption against earlier reserve 11 | 12 | // box data: 13 | // 14 | // registers: 15 | // R4 - history of ownership (under AVL+ tree), 16 | // tree contains reserveId as a key, signature as value, 17 | // and message under the signature is position in the tree, note value and token id 18 | // R5 - current holder of the note (public key given as a group element) 19 | // R6 - current length of the spendings chain (as long int) 20 | // 21 | // tokens: 22 | // #0 - token which amount is equal to note value at the moment of issueance, in unit of account (mg of gold). 23 | // The token is denoting notes started from an initial one which has all the tokens of this ID 24 | // 25 | // to create a note (issue new money accounted in milligrams of gols), one needs to create a box locked with this 26 | // contract, R4 containing empty AVL+ tree digest, R5 containing public key (encoded elliptic curve point) of the 27 | // issuer, R6 equals to 0, and tokens slot #0 contains new token with all the issuance locked in the box. If 28 | // any of the conditions not met (any register has another value, some tokens sent to other address or contract), 29 | // the note should be ignored by ChainCash software 30 | 31 | val action = getVar[Byte](0).get // also encodes note output # in tx outputs 32 | 33 | val holder = SELF.R5[GroupElement].get // used in both paths 34 | 35 | if (action >= 0) { 36 | // spending path 37 | 38 | val g: GroupElement = groupGenerator 39 | 40 | val history = SELF.R4[AvlTree].get 41 | 42 | val reserve = CONTEXT.dataInputs(0) 43 | val reserveId = reserve.tokens(0)._1 44 | 45 | val noteTokenId = SELF.tokens(0)._1 46 | val noteValue = SELF.tokens(0)._2 47 | 48 | val selfOutput = OUTPUTS(action) 49 | 50 | val position = SELF.R6[Long].get 51 | val positionBytes = longToByteArray(position) 52 | val noteValueBytes = longToByteArray(noteValue) 53 | val message = positionBytes ++ noteValueBytes ++ noteTokenId 54 | 55 | // a of signature in (a, z) 56 | val a = getVar[GroupElement](1).get 57 | val aBytes = a.getEncoded 58 | 59 | // Computing challenge 60 | val e: Coll[Byte] = blake2b256(aBytes ++ message ++ holder.getEncoded) // strong Fiat-Shamir 61 | val eInt = byteArrayToBigInt(e) // challenge as big integer 62 | 63 | // z of signature in (a, z) 64 | val zBytes = getVar[Coll[Byte]](2).get 65 | val z = byteArrayToBigInt(zBytes) 66 | 67 | // Signature is valid if g^z = a * x^e 68 | val properSignature = g.exp(z) == a.multiply(holder.exp(eInt)) 69 | 70 | val properReserve = holder == reserve.R4[GroupElement].get 71 | 72 | val leafValue = aBytes ++ zBytes 73 | val leafKey = positionBytes ++ reserveId 74 | val keyVal = (leafKey, leafValue) 75 | val proof = getVar[Coll[Byte]](3).get 76 | 77 | val nextTree: Option[AvlTree] = history.insert(Coll(keyVal), proof) 78 | // This will fail if the operation failed or the proof is incorrect due to calling .get on the Option 79 | val outputDigest: Coll[Byte] = nextTree.get.digest 80 | 81 | def nextNoteCorrect(noteOut: Box): Boolean = { 82 | val outHistory = noteOut.R4[AvlTree].get 83 | 84 | val insertionPerformed = outHistory.digest == outputDigest && outHistory.enabledOperations == history.enabledOperations 85 | val sameScript = noteOut.propositionBytes == SELF.propositionBytes 86 | val nextHolderDefined = noteOut.R5[GroupElement].isDefined 87 | val valuePreserved = noteOut.value >= SELF.value 88 | val positionIncreased = noteOut.R6[Long].get == (position + 1) 89 | 90 | positionIncreased && insertionPerformed && sameScript && nextHolderDefined && valuePreserved 91 | } 92 | 93 | val changeIdx = getVar[Byte](4) // optional index of change output 94 | 95 | val outputsValid = if(changeIdx.isDefined) { 96 | val changeOutput = OUTPUTS(changeIdx.get) 97 | 98 | // strict equality to prevent moving tokens to other contracts 99 | (selfOutput.tokens(0)._2 + changeOutput.tokens(0)._2) == SELF.tokens(0)._2 && 100 | nextNoteCorrect(selfOutput) && 101 | nextNoteCorrect(changeOutput) 102 | } else { 103 | selfOutput.tokens(0) == SELF.tokens(0) && nextNoteCorrect(selfOutput) 104 | } 105 | 106 | // proveDlog(holder) is needed to prevent changing output notes in the mempool 107 | sigmaProp(proveDlog(holder) && properSignature && properReserve && outputsValid) 108 | } else { 109 | // action < 0 110 | 111 | // redemption path 112 | // called by setting action variable to any negative value 113 | // absolute value of the action denotes index of reserve in transaction inputs 114 | 115 | // we just check current holder's signature here 116 | 117 | // it is checked that note token is locked in receipt in the reserve contract 118 | 119 | // we check that the note is spent along with a reserve contract box. 120 | // we drop version byte during ergotrees comparison 121 | // signature of note holder is also required 122 | 123 | val index = -action 124 | 125 | val reserveInput = INPUTS(index) 126 | val reserveInputErgoTree = reserveInput.propositionBytes 127 | val treeHash = blake2b256(reserveInputErgoTree.slice(1, reserveInputErgoTree.size)) 128 | val reserveSpent = treeHash == fromBase58("$reserveContractHash") 129 | 130 | // we check receipt contract here, and other fields in reserve contract, see comments in reserve.es 131 | val receiptOutputErgoTree = OUTPUTS(index).propositionBytes 132 | val receiptTreeHash = blake2b256(receiptOutputErgoTree.slice(1, receiptOutputErgoTree.size)) 133 | val receiptCreated = receiptTreeHash == fromBase58("$receiptContractHash") 134 | 135 | proveDlog(holder) && sigmaProp(reserveSpent && receiptCreated) 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /contracts/layer2-old/reserve.es: -------------------------------------------------------------------------------- 1 | { 2 | // ERG variant 3 | 4 | // Data: 5 | // - token #0 - identifying singleton token 6 | // - R4 - signing key (as a group element) 7 | // - R5 - refund init height (Int.MaxValue if not set) 8 | // 9 | // Actions: 10 | // - redeem note (#0) 11 | // - top up (#1) 12 | // - init refund (#2) 13 | // - cancel refund (#3) 14 | // - complete refund (#4) 15 | 16 | val ownerKey = SELF.R4[GroupElement].get // used in notes and unlock/lock/refund actions 17 | val selfOut = OUTPUTS(0) 18 | val selfPreserved = 19 | selfOut.propositionBytes == SELF.propositionBytes && 20 | selfOut.tokens == SELF.tokens && 21 | selfOut.R4[GroupElement].get == SELF.R4[GroupElement].get 22 | 23 | val action = getVar[Byte](0).get 24 | 25 | if (action == 0) { 26 | // redeem path 27 | // oracle provides gold price in nanoErg per kg in its R4 register 28 | 29 | // todo: prevent double redemption by maintaining a tree with redemptions 30 | 31 | val g: GroupElement = groupGenerator 32 | 33 | val redemptionContractTokenId = fromBase58("...") // todo: read from outer environment 34 | 35 | val redemptionInput = INPUTS(0) 36 | val redemptionInputOk == redemptionInput.tokens(0)._1 == redemptionContractTokenId 37 | 38 | val history = redemptionInput.R4[AvlTree].get 39 | val redeemPosition = redemptionInput.R6[Long].get 40 | 41 | // read last lead and redemption leaf 42 | 43 | // checking redemption leaf signature 44 | val rLeafTreeHashDigest = getVar[Coll[Byte]](1).get 45 | val rLeafReserveId = getVar[Coll[Byte]](2).get 46 | val rLeafNoteValue = getVar[Long](3).get 47 | val rLeafHolderId = getVar[Coll[Byte]](4).get 48 | val rLeafA = getVar[GroupElement](5).get 49 | val rLeafABytes = rLeafA.getEncoded 50 | val rLeafZBytes = getVar[Coll[Byte]](6).get 51 | val rLeafProperFormat = rLeafTreeHashDigest.size == 32 && rLeafReserveId.size == 32 && rLeafHolderId.size == 32 52 | val rLeafMessage = rLeafTreeHashDigest ++ rLeafReserveId ++ longToByteArray(rLeafNoteValue) ++ rLeafHolderId 53 | val rLeafEInt = byteArrayToBigInt(blake2b256(rLeafMessage)) // weak Fiat-Shamir - todo: should be strong 54 | val rLeafZ = byteArrayToBigInt(rLeafZBytes) 55 | val rLeafReserveIdValid = rLeafReserve.tokens(0)._1 == rLeafReserveId 56 | val rLeafReservePk = rLeafReserve.R4[GroupElement].get 57 | val rLeafProperSignature = (g.exp(rLeafZ) == rLeafA.multiply(lastLeafReservePk.exp(rLeafEInt))) && rLeafProperFormat && rLeafReserveIdValid 58 | 59 | // check holder's record signed 60 | val holderTreeHashDigest = getVar[Coll[Byte]](7).get 61 | val holderReserveId = getVar[Coll[Byte]](8).get 62 | val holderNoteValue = getVar[Long](9).get 63 | val holderHolderId = getVar[Coll[Byte]](10).get 64 | val holderA = getVar[GroupElement](11).get 65 | val holderABytes = holderA.getEncoded 66 | val holderZBytes = getVar[Coll[Byte]](12).get 67 | val holderProperFormat = holderTreeHashDigest.size == 32 && holderReserveId.size == 32 && holderHolderId.size == 32 68 | val holderMessage = holderTreeHashDigest ++ holderReserveId ++ longToByteArray(holderNoteValue) ++ holderHolderId 69 | val holderEInt = byteArrayToBigInt(blake2b256(holderMessage)) // weak Fiat-Shamir - todo: should be strong 70 | val holderZ = byteArrayToBigInt(holderZBytes) 71 | val holderReserveIdValid = holderReserve.tokens(0)._1 == holderReserveId 72 | val holderReservePk = holderReserve.R4[GroupElement].get 73 | val holderProperSignature = (g.exp(holderZ) == holderA.multiply(holderReservePk.exp(holderEInt))) && holderProperFormat && holderReserveIdValid 74 | 75 | // checking tree proofs of inclusion for redemption and last leafs 76 | val lastLeafPosition = redemptionInput.R5[Long].get 77 | val lastLeafKeyBytes = longToByteArray(lastLeafPosition) 78 | val rLeafPosition = redemptionInput.R6[Long].get 79 | val rLeafKeyBytes = longToByteArray(rLeafPosition) 80 | val proof = getVar[Coll[Byte]](13).get 81 | val properProof = history.get(rLeafKeyBytes, proof).get == (rLeafABytes ++ rLeafZBytes) && 82 | history.get(lastLeafKeyBytes, proof).get == (holderABytes ++ holderZBytes) 83 | 84 | val goldOracle = CONTEXT.dataInputs(0) 85 | val properOracle = goldOracle.tokens(0)._1 == fromBase58("2DfY1K4rW9zPVaQgaDp2KXgnErjxKPbbKF5mq1851MJE") 86 | val oracleRate = goldOracle.R4[Long].get / 1000000 // normalize to nanoerg per mg of gold 87 | val tokensRedeemed = holderNoteValue // 1 token == 1 mg of gold 88 | 89 | // 2% redemption fee 90 | val nanoergsToRedeem = tokensRedeemed * oracleRate * 98 / 100 91 | val redeemCorrect = (SELF.value - selfOut.value) <= nanoergsToRedeem 92 | 93 | sigmaProp(selfPreserved && redeemCorrect && rLeafProperSignature && holderProperSignature && properProof && properOracle) 94 | } else if (action == 1) { 95 | // top up 96 | sigmaProp(selfPreserved && (selfOut.value - SELF.value >= 1000000000)) // at least 1 ERG added 97 | } else { 98 | // refund 99 | // todo: check that refund delay is bigger than max contestation period 100 | // todo: write tests for refund paths, document them 101 | if (action == 2) { 102 | // init refund 103 | val correctHeight = selfOut.R5[Int].get >= HEIGHT - 5 104 | sigmaProp(selfPreserved && correctHeight) && proveDlog(ownerKey) 105 | } else if (action == 3) { 106 | // cancel refund 107 | val correctHeight = !(selfOut.R5[Int].isDefined) 108 | sigmaProp(selfPreserved && correctHeight) && proveDlog(ownerKey) 109 | } else if (action == 4) { 110 | // todo: complete refund is not possible now , there must be some ergs left to allow sigs check 111 | // complete refund 112 | val refundNotificationPeriod = 7200 // 10 days 113 | val correctHeight = (SELF.R5[Int].get + refundNotificationPeriod) >= HEIGHT 114 | sigmaProp(correctHeight) && proveDlog(ownerKey) // todo: check is it ok to check no conditions 115 | } else { 116 | sigmaProp(false) 117 | } 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /src/main/scala/chaincash/contracts/BasisDeployer.scala: -------------------------------------------------------------------------------- 1 | package chaincash.contracts 2 | 3 | import org.ergoplatform.ErgoAddressEncoder 4 | import org.ergoplatform.appkit.{ErgoValue, NetworkType} 5 | import scorex.crypto.encode.Base16 6 | import sigmastate.AvlTreeFlags 7 | import sigmastate.Values.{AvlTreeConstant, GroupElementConstant} 8 | import sigmastate.serialization.{GroupElementSerializer, ValueSerializer} 9 | import special.sigma.AvlTree 10 | import work.lithos.plasma.PlasmaParameters 11 | import work.lithos.plasma.collections.PlasmaMap 12 | 13 | 14 | /** 15 | * Utility for deploying Basis reserve contract on Ergo blockchain mainnet 16 | * Similar to DexySpec deployment pattern 17 | */ 18 | object BasisDeployer extends App { 19 | 20 | /** 21 | * Creates a GroupElementConstant from a hex-encoded public key 22 | * @param hexPublicKey Hex string representing the public key (compressed or uncompressed) 23 | * @return GroupElementConstant for use in Ergo contracts 24 | */ 25 | def createOwnerKeyFromHex(hexPublicKey: String): GroupElementConstant = { 26 | val publicKeyBytes = Base16.decode(hexPublicKey).get 27 | val groupElement = GroupElementSerializer.fromBytes(publicKeyBytes) 28 | GroupElementConstant(groupElement) 29 | } 30 | 31 | /** 32 | * Example owner key using the specified public key 33 | */ 34 | val exampleOwnerKey: GroupElementConstant = { 35 | createOwnerKeyFromHex("025ffe0e1a282fc0249320946af4209eb2dd7f250c16946fdd615533092e054bca") // Alice 36 | } 37 | 38 | // Example values - these should be replaced with actual values 39 | val exampleTrackerNftId = "dbfbbaf91a98c22204de3745e1986463620dcf3525ad566c6924cf9e976f86f8" 40 | val exampleReserveTokenId = "c7510cba80a9b7113e53968f7ff42ad250be2808fef1d36b71a89b0d644178c2" 41 | 42 | // Network configuration 43 | val networkType = NetworkType.MAINNET 44 | val networkPrefix = networkType.networkPrefix 45 | val ergoAddressEncoder = new ErgoAddressEncoder(networkPrefix) 46 | 47 | // Basis contract configuration 48 | val basisContractScript = Constants.readContract("offchain/basis.es", Map.empty) 49 | 50 | println("basis script: " + basisContractScript) 51 | 52 | val basisErgoTree = Constants.compile(basisContractScript) 53 | val basisAddress = Constants.getAddressFromErgoTree(basisErgoTree) 54 | 55 | val chainCashPlasmaParameters = PlasmaParameters(32, None) 56 | val InsertUpdate = AvlTreeFlags(insertAllowed = true, updateAllowed = true, removeAllowed = false) 57 | def emptyPlasmaMap = new PlasmaMap[Array[Byte], Array[Byte]](InsertUpdate, chainCashPlasmaParameters) 58 | val emptyTreeErgoValue: ErgoValue[AvlTree] = emptyPlasmaMap.ergoValue 59 | val emptyTree: AvlTree = emptyTreeErgoValue.getValue 60 | 61 | /** 62 | * Creates deployment request for Basis reserve contract 63 | * @param ownerPublicKey GroupElement of the reserve owner 64 | * @param trackerNftId NFT token ID identifying the tracker (bytes) 65 | * @param reserveTokenId Singleton token ID for the reserve 66 | * @param initialCollateral Initial ERG collateral in nanoERG 67 | * @return JSON string for deployment request 68 | */ 69 | def createBasisDeploymentRequest( 70 | ownerPublicKey: GroupElementConstant, 71 | trackerNftId: String, 72 | reserveTokenId: String, 73 | initialCollateral: Long = 1000000000L // 1 ERG 74 | ): String = { 75 | 76 | // Encode registers 77 | val ownerKeyEncoded = Base16.encode(ValueSerializer.serialize(ownerPublicKey)) 78 | val emptyTreeEncoded = Base16.encode(ValueSerializer.serialize(AvlTreeConstant(emptyTree))) 79 | val trackerNftBytes = Base16.decode(trackerNftId).get 80 | val trackerNftEncoded = Base16.encode(ValueSerializer.serialize(trackerNftBytes)) 81 | 82 | s""" 83 | |[ 84 | | { 85 | | "address": "${basisAddress.toString}", 86 | | "value": $initialCollateral, 87 | | "assets": [ 88 | | { 89 | | "tokenId": "$reserveTokenId", 90 | | "amount": 1 91 | | } 92 | | ], 93 | | "registers": { 94 | | "R4": "$ownerKeyEncoded", 95 | | "R5": "$emptyTreeEncoded", 96 | | "R6": "$trackerNftEncoded" 97 | | } 98 | | } 99 | |] 100 | |""".stripMargin 101 | } 102 | 103 | /** 104 | * Creates scan request for monitoring Basis reserve 105 | * @param reserveTokenId Singleton token ID for the reserve 106 | * @return JSON string for scan request 107 | */ 108 | def createBasisScanRequest(reserveTokenId: String): String = { 109 | s""" 110 | |{ 111 | | "scanName": "Basis Reserve", 112 | | "walletInteraction": "shared", 113 | | "removeOffchain": true, 114 | | "trackingRule": { 115 | | "predicate": "containsAsset", 116 | | "assetId": "$reserveTokenId" 117 | | } 118 | |} 119 | |""".stripMargin 120 | } 121 | 122 | /** 123 | * Prints deployment information for Basis contract 124 | */ 125 | def printDeploymentInfo(): Unit = { 126 | println("=== Basis Reserve Contract Deployment Information ===") 127 | println() 128 | 129 | println(s"Contract Address: ${basisAddress.toString}") 130 | println(s"Network: ${networkType.name}") 131 | println(s"Network Prefix: $networkPrefix") 132 | println() 133 | 134 | println("Contract Script:") 135 | println(basisContractScript) 136 | println() 137 | 138 | println("Deployment Instructions:") 139 | println("1. Issue a singleton NFT token for the reserve") 140 | println("2. Issue an NFT token for the tracker") 141 | println("3. Use createBasisDeploymentRequest() with owner public key, tracker NFT ID, and reserve NFT ID") 142 | println("4. Submit the deployment transaction to the Ergo blockchain") 143 | println("5. Use createBasisScanRequest() to monitor the reserve") 144 | println() 145 | } 146 | 147 | /** 148 | * Main method for testing and deployment 149 | */ 150 | printDeploymentInfo() 151 | 152 | // Example usage 153 | println("=== Example Deployment Request ===") 154 | 155 | println("Example Scan Request:") 156 | println(createBasisScanRequest(exampleReserveTokenId)) 157 | println() 158 | 159 | println("Example Deployment Request:") 160 | println(createBasisDeploymentRequest(exampleOwnerKey, exampleTrackerNftId, exampleReserveTokenId)) 161 | println() 162 | 163 | } 164 | 165 | /** 166 | * Companion object for Basis contract constants and utilities 167 | */ 168 | object BasisConstants { 169 | 170 | // Action codes for Basis contract 171 | val REDEEM_ACTION: Byte = 0 172 | val TOP_UP_ACTION: Byte = 1 173 | 174 | // Minimum top-up amount (1 ERG) 175 | val MIN_TOP_UP_AMOUNT: Long = 1000000000L 176 | 177 | // Emergency redemption time (7 days in milliseconds) 178 | val EMERGENCY_REDEMPTION_TIME: Long = 7L * 24L * 60L * 60L * 1000L 179 | 180 | // Fee percentage for redemption (2%) 181 | val REDEMPTION_FEE_PERCENTAGE: Int = 2 182 | 183 | /** 184 | * Calculates redemption fee 185 | * @param amount Amount to redeem 186 | * @return Fee amount 187 | */ 188 | def calculateRedemptionFee(amount: Long): Long = { 189 | (amount * REDEMPTION_FEE_PERCENTAGE) / 100 190 | } 191 | 192 | /** 193 | * Calculates net redemption amount after fees 194 | * @param amount Amount to redeem 195 | * @return Net amount after fees 196 | */ 197 | def calculateNetRedemption(amount: Long): Long = { 198 | amount - calculateRedemptionFee(amount) 199 | } 200 | } -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # ChainCash Agents Architecture 2 | 3 | This document describes the agent-based architecture of the ChainCash protocol - a peer-to-peer money creation system built on the Ergo blockchain. 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | ## Core On-Chain Agents 10 | 11 | ### Reserve Contract Agent 12 | **File**: `contracts/onchain/reserve.es` 13 | 14 | **Responsibilities**: 15 | - Holds ERG collateral backing issued notes 16 | - Manages reserve owner's public key 17 | - Handles note redemption with 2% fee 18 | - Supports reserve top-up operations 19 | - Issues new notes against collateral 20 | - Maintains ownership history via AVL tree 21 | 22 | **Key Functions**: 23 | - `createReserve`: Initialize new reserve with collateral 24 | - `topUpReserve`: Add more collateral to existing reserve 25 | - `issueNote`: Create new notes backed by reserve collateral 26 | - `redeemNote`: Redeem notes against reserve with fee 27 | 28 | ### Note Contract Agent 29 | **File**: `contracts/onchain/note.es` 30 | 31 | **Responsibilities**: 32 | - Represents ChainCash notes (digital currency) 33 | - Manages note ownership and transfers 34 | - Maintains spending chain history in AVL tree 35 | - Handles redemption operations 36 | - Enforces signature verification for transfers 37 | 38 | **Key Functions**: 39 | - `createNote`: Issue new note from reserve 40 | - `transferNote`: Transfer note to new owner 41 | - `redeemNote`: Redeem note against reserve 42 | - `verifySpendingChain`: Validate spending history 43 | 44 | ### Receipt Contract Agent 45 | **File**: `contracts/onchain/receipt.es` 46 | 47 | **Responsibilities**: 48 | - Created during note redemption 49 | - Allows re-redemption against earlier reserves 50 | - Automatically burns after 3 years 51 | - Tracks redemption position and history 52 | 53 | **Key Functions**: 54 | - `createReceipt`: Generate receipt during redemption 55 | - `reRedeem`: Re-redeem against earlier reserves 56 | - `autoBurn`: Self-destruct after expiration 57 | 58 | ## Off-Chain Management Agents 59 | 60 | ### Wallet Agent 61 | **File**: `src/main/scala/chaincash/offchain/WalletUtils.scala` 62 | 63 | **Responsibilities**: 64 | - Manages user addresses and keys 65 | - Fetches unspent boxes from blockchain 66 | - Handles transaction fee calculations 67 | - Tracks user's notes and reserves 68 | 69 | **Key Operations**: 70 | - Address management and key generation 71 | - Box scanning and filtering 72 | - Fee estimation and transaction building 73 | 74 | ### Reserve Management Agent 75 | **File**: `src/main/scala/chaincash/offchain/ReserveUtils.scala` 76 | 77 | **Responsibilities**: 78 | - Creates new reserves with collateral 79 | - Associates reserves with public keys 80 | - Handles reserve funding operations 81 | - Manages reserve lifecycle 82 | 83 | **Key Operations**: 84 | - Reserve creation and funding 85 | - Collateral management 86 | - Reserve state tracking 87 | 88 | ### Note Management Agent 89 | **File**: `src/main/scala/chaincash/offchain/NoteUtils.scala` 90 | 91 | **Responsibilities**: 92 | - Creates new notes against reserves 93 | - Handles note transfers between users 94 | - Manages signature creation for spending 95 | - Updates ownership history 96 | 97 | **Key Operations**: 98 | - Note issuance from reserves 99 | - Transfer processing and validation 100 | - Spending chain maintenance 101 | 102 | ### Tracking Agent 103 | **File**: `src/main/scala/chaincash/offchain/TrackingUtils.scala` 104 | 105 | **Responsibilities**: 106 | - Scans blockchain for new reserves and notes 107 | - Maintains local database of system state 108 | - Processes blocks and updates local state 109 | - Tracks user-owned reserves and notes 110 | 111 | **Key Operations**: 112 | - Blockchain scanning and indexing 113 | - State synchronization 114 | - Event processing and persistence 115 | 116 | ## Server Agent 117 | 118 | **File**: `src/main/scala/chaincash/offchain/server/model.scala` 119 | 120 | **Responsibilities**: 121 | - Implements acceptance predicates for notes 122 | - Manages trust-based and collateral-based reserves 123 | - Processes exchange transactions 124 | - Implements mutual credit clearing 125 | 126 | **Key Features**: 127 | - Client-side validation of note quality 128 | - Customizable acceptance policies 129 | - Trust scoring and reserve evaluation 130 | - Exchange rate management 131 | 132 | ## Agent Interaction Patterns 133 | 134 | ### Issuance Flow 135 | ``` 136 | Reserve Agent → Note Agent 137 | 1. User funds reserve with collateral 138 | 2. Reserve Agent creates new notes 139 | 3. Note Agent manages note lifecycle 140 | ``` 141 | 142 | ### Transfer Flow 143 | ``` 144 | Note Agent → Note Agent 145 | 1. Current owner signs note transfer 146 | 2. New owner receives note with updated spending chain 147 | 3. Reserve Agent verifies transfer validity 148 | ``` 149 | 150 | ### Redemption Flow 151 | ``` 152 | Note Agent → Reserve Agent → Receipt Agent 153 | 1. Note holder initiates redemption 154 | 2. Reserve Agent processes redemption with fee 155 | 3. Receipt Agent creates redemption receipt 156 | 4. Receipt allows re-redemption against earlier reserves 157 | ``` 158 | 159 | ## System Architecture Principles 160 | 161 | ### Collective Backing 162 | - Notes are backed by all previous spenders in the chain 163 | - Each spender adds their collateral or trust to the backing 164 | - Redemption can occur against any reserve in the spending history 165 | 166 | ### Self-Sovereign Banking 167 | - Each participant acts as their own bank 168 | - Customizable acceptance policies per user/server 169 | - No central authority controlling issuance or acceptance 170 | 171 | ### Trust Integration 172 | - Supports both collateral-based reserves (ERG backing) 173 | - Supports trust-based reserves (reputation/credit) 174 | - Mixed backing combining multiple reserve types 175 | 176 | ### Client-Side Validation 177 | - Each receiver validates notes based on their own criteria 178 | - No global consensus on note quality 179 | - Decentralized acceptance decisions 180 | 181 | ## Agent Communication 182 | 183 | ### On-Chain Communication 184 | - Agents communicate via transaction inputs/outputs 185 | - State changes recorded on blockchain 186 | - Immutable spending chain maintained in AVL trees 187 | 188 | ### Off-Chain Coordination 189 | - Database state synchronization 190 | - Blockchain event monitoring 191 | - Local state management and caching 192 | 193 | ## Implementation Notes 194 | 195 | ### Data Structures 196 | - **AVL Trees**: Used for maintaining spending chains and ownership history 197 | - **Registers**: Store public keys, amounts, and metadata 198 | - **Tokens**: Represent notes and receipts as first-class assets 199 | 200 | ### Security Considerations 201 | - Signature verification for all transfers 202 | - Collateral ratio enforcement 203 | - Time-based expiration for receipts 204 | - Fee mechanisms to prevent spam 205 | 206 | ### Scalability 207 | - Client-side validation reduces blockchain load 208 | - Local state management for performance 209 | - Parallel processing of independent transactions 210 | 211 | ## Testing Agents 212 | 213 | Test coverage includes: 214 | - Reserve creation and funding 215 | - Note issuance and transfers 216 | - Redemption flows 217 | - Spending chain validation 218 | - Fee calculations 219 | - Edge cases and error conditions 220 | 221 | See test files in `src/test/scala/chaincash/` for detailed agent testing. 222 | 223 | --- 224 | 225 | *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.* -------------------------------------------------------------------------------- /contracts/offchain/basis.es: -------------------------------------------------------------------------------- 1 | { 2 | // Contract for on-chain reserve (in ERG only for now) backing offchain payments 3 | // aka Basis 4 | // https://www.ergoforum.org/t/basis-a-foundational-on-chain-reserve-approach-to-support-a-variety-of-offchain-protocols/5153 5 | 6 | // Main use-cases: 7 | // * payments for content (such as 402 HTTP code processing) 8 | // * micropaymentsdebtAmount 9 | // * payments in p2p networks 10 | // * agent-to-agent payments 11 | 12 | // Here are some properties of Basis design: 13 | // * offchain payments with no need to create anything on-chain first, so possibility to create credit 14 | // * usage of minimally trusted trackers to track state of mutual debt offchain 15 | // * onchain contract based redemption with prevention of double redemptions 16 | 17 | // How does that work: 18 | // * a tracker holds A -> B debt (as positive number), along with ever increasing (on every operation) timestamp. 19 | // A key->value dictionary is used to store the data as hash(AB) -> (amount, timestamp, sig_A), where AB is concatenation of public 20 | // keys A and B, "amount" is amount of debt of A before B, timestamp is operation timestamp (in milliseconds), sig_A is signature of A for 21 | // A for message (hash(AB), amount, timestamp). 22 | // * to make a (new payment) to B, A is taking current AB record, increasing debt, signing the updated record and 23 | // sending it to the tracker 24 | // * tracker is periodically committing to its state (dictionary) by posting its digest on chain 25 | // * at any moment it is possible to redeem A debt to B by calling redemption action of the reserve contract below 26 | // B -> timestamp pair is written into the contract box. Calling the contract after with timestamp <= written on is 27 | // prohibited. Tracker signature is needed to redeem. On next operation with tracker, debt of A is decreased. 28 | // If not, A is refusing to sign updated records. Tracker cant steal A's funds as A's signature is checked. 29 | // * if tracker is going offline, possible to redeem without its signature, when at least one week passed 30 | // * always possible to top up the reserve, to redeem, reserve holder is making an offchain payment to self (A -> A) 31 | // and then redeem 32 | 33 | // todo: consider privacy in payments 34 | 35 | // Data: 36 | // - token #0 - identifying singleton token 37 | // - R4 - signing key (as a group element) 38 | // - R5 - tree of timestamps redeemed (to avoid double spending, it should have insert-only flag set) 39 | // - R6 - NFT id of tracker server (bytes) // todo: support multiple payment servers by using a tree 40 | // 41 | // Actions: 42 | // - redeem note (#0) 43 | // - top up (#1) 44 | // 45 | // Tracker box registers: 46 | // - R4 - tracker's signing key 47 | // - R5 - commitment to credit data 48 | 49 | val v = getVar[Byte](0).get 50 | val action = v / 10 51 | val index = v % 10 52 | 53 | val ownerKey = SELF.R4[GroupElement].get // reserve owner's key 54 | val selfOut = OUTPUTS(index) 55 | 56 | // common checks for all the paths (not incl. ERG value check) 57 | val selfPreserved = 58 | selfOut.propositionBytes == SELF.propositionBytes && 59 | selfOut.tokens == SELF.tokens && 60 | selfOut.R4[GroupElement].get == SELF.R4[GroupElement].get && 61 | selfOut.R6[Coll[Byte]].get == SELF.R6[Coll[Byte]].get 62 | 63 | if (action == 0) { 64 | // redemption path 65 | 66 | // Tracker box holds the debt information as key-value pairs: AB -> (amount, timestamp) 67 | val tracker = CONTEXT.dataInputs(0) // Data input: tracker box containing debt records 68 | val trackerNftId = tracker.tokens(0)._1 // NFT token ID identifying the tracker 69 | val trackerTree = tracker.R5[AvlTree].get // AVL tree storing debt commitments from tracker 70 | val expectedTrackerId = SELF.R6[Coll[Byte]].get // Expected tracker ID stored in reserve contract 71 | val trackerIdCorrect = trackerNftId == expectedTrackerId // Verify tracker identity matches 72 | val trackerPubKey = tracker.R4[GroupElement].get // Tracker's public key for signature verification 73 | 74 | val g: GroupElement = groupGenerator // Base point for elliptic curve operations 75 | 76 | // Receiver of the redemption (creditor) 77 | val receiver = getVar[GroupElement](1).get 78 | val receiverBytes = receiver.getEncoded // Receiver's public key bytes 79 | 80 | val ownerKeyBytes = ownerKey.getEncoded // Reserve owner's public key bytes 81 | 82 | // Create key for debt record: hash(ownerKey || receiverKey) 83 | val key = blake2b256(ownerKeyBytes ++ receiverBytes) 84 | 85 | // Reserve owner's signature for the debt record 86 | val reserveSigBytes = getVar[Coll[Byte]](2).get 87 | 88 | // Debt amount and timestamp from the debt record 89 | // todo: save debt being redeemed in the reserve tree, along with the timestamp, and then debtAmount is 90 | // todo: total amount of debt, and only up to the delta can be redeemed 91 | val debtAmount = getVar[Long](3).get 92 | val timestamp = getVar[Long](4).get 93 | val value = longToByteArray(debtAmount) ++ longToByteArray(timestamp) ++ reserveSigBytes 94 | 95 | val reserveId = SELF.tokens(0)._1 // Reserve singleton token ID 96 | 97 | // Output box where redeemed funds are sent 98 | val redemptionOut = OUTPUTS(index + 1) 99 | val redemptionTreeHash = blake2b256(redemptionOut.propositionBytes) 100 | val afterFees = redemptionOut.value 101 | 102 | // Update timestamp tree to prevent double redemption 103 | // Store timestamp to mark it as redeemed 104 | val timestampKeyVal = (key, longToByteArray(timestamp)) // key -> timestamp value 105 | val proof = getVar[Coll[Byte]](5).get // Merkle proof for tree insertion 106 | // Insert redeemed timestamp into AVL tree 107 | val nextTree: AvlTree = SELF.R5[AvlTree].get.insert(Coll(timestampKeyVal), proof).get // todo: tree can have insert or update flags 108 | // Verify tree was properly updated in output 109 | val properTimestampTree = nextTree == selfOut.R5[AvlTree].get // todo: check that the timestamp has increased 110 | 111 | // Message to verify signatures: key || amount || timestamp 112 | val message = key ++ longToByteArray(debtAmount) ++ longToByteArray(timestamp) 113 | 114 | // Tracker's signature authorizing the redemption 115 | val trackerSigBytes = getVar[Coll[Byte]](6).get 116 | 117 | // Split tracker signature into components (Schnorr signature: (a, z)) 118 | val trackerABytes = trackerSigBytes.slice(0, 33) // Random point a 119 | val trackerZBytes = trackerSigBytes.slice(33, trackerSigBytes.size) // Response z 120 | val trackerA = decodePoint(trackerABytes) // Decode random point 121 | val trackerZ = byteArrayToBigInt(trackerZBytes) // Convert response to big integer 122 | 123 | // Compute challenge for tracker signature verification (Fiat-Shamir) 124 | val trackerE: Coll[Byte] = blake2b256(trackerABytes ++ message ++ trackerPubKey.getEncoded) // strong Fiat-Shamir 125 | val trackerEInt = byteArrayToBigInt(trackerE) // challenge as big integer 126 | 127 | // Verify tracker Schnorr signature: g^z = a * x^e 128 | val properTrackerSignature = (g.exp(trackerZ) == trackerA.multiply(trackerPubKey.exp(trackerEInt))) 129 | 130 | // Check if enough time has passed for emergency redemption (without tracker signature) 131 | // tracker signature is still provided but may be invalid 132 | // todo: consider more efficient check where tracker signature is not needed at all 133 | val lastBlockTime = CONTEXT.headers(0).timestamp 134 | val enoughTimeSpent = (timestamp > 0) && (lastBlockTime - timestamp) > 7 * 86400000 // 7 days in milliseconds passed 135 | 136 | // Calculate amount being redeemed and verify it doesn't exceed debt 137 | val redeemed = SELF.value - selfOut.value 138 | val properlyRedeemed = (redeemed <= debtAmount) && (enoughTimeSpent || properTrackerSignature) 139 | 140 | // Split reserve owner signature into components (Schnorr signature: (a, z)) 141 | val reserveABytes = reserveSigBytes.slice(0, 33) // Random point a 142 | val reserveZBytes = reserveSigBytes.slice(33, reserveSigBytes.size) // Response z 143 | val reserveA = decodePoint(reserveABytes) // Decode random point 144 | val reserveZ = byteArrayToBigInt(reserveZBytes) // Convert response to big integer 145 | 146 | // Compute challenge for reserve signature verification (Fiat-Shamir) 147 | val reserveE: Coll[Byte] = blake2b256(reserveABytes ++ message ++ ownerKey.getEncoded) // strong Fiat-Shamir 148 | val reserveEInt = byteArrayToBigInt(reserveE) // challenge as big integer 149 | 150 | // Verify reserve owner Schnorr signature: g^z = a * x^e 151 | val properReserveSignature = (g.exp(reserveZ) == reserveA.multiply(ownerKey.exp(reserveEInt))) 152 | 153 | // Verify receiver's proposition (creditor must be able to spend the redemption output) 154 | val receiverCondition = proveDlog(receiver) 155 | 156 | // Combine all validation conditions 157 | sigmaProp(selfPreserved && 158 | properTimestampTree && 159 | properReserveSignature && 160 | properlyRedeemed && 161 | receiverCondition) 162 | } else if (action == 1) { 163 | // top up 164 | sigmaProp( 165 | selfPreserved && 166 | (selfOut.value - SELF.value >= 1000000000) && // at least 1 ERG added 167 | selfOut.R5[AvlTree].get == SELF.R5[AvlTree].get 168 | ) 169 | } else { 170 | sigmaProp(false) 171 | } 172 | 173 | } -------------------------------------------------------------------------------- /contracts/offchain/basis.md: -------------------------------------------------------------------------------- 1 | # Basis - offchain IOU money for digital economies and communities 2 | 3 | In this writing, we propose Basis, efficient offchain cash system, backed by on-chain reserves but also allowing for 4 | creating credit (unbacked IOU money). Its use cases are now thought as follows: 5 | 6 | * micropayments, such as payments for content, services, resources usage in p2p and distributed systems. Notable 7 | difference from Lightning / FediMint / Cashu etc is that here a service can be provided on credit (within certain limits), 8 | which would boost growth for many services, allow for globally working alternative to free trial, and so on. 9 | 10 | * community currencies, which can be about small circles where there is trust to each other, using fully unbacked offchain cash, 11 | more complex environments using fully or partially backed cash, potentially with tokenized local reserves (such as gold and silver) 12 | etc 13 | 14 | Such use cases would definitely win from simple but secure design, no on-chain fees, and no need to work with blockchain 15 | at all before need to back issued cash or redeem cash for blockchain asssets. 16 | 17 | But there can be more use cases discovered with time! 18 | 19 | ## Basis Design 20 | 21 | As we have offchain cash with possibility to create credit (unbacked money), we have need to track all the money in form 22 | of IOU (I Owe You) notes issued by an issuer, for all the issuers. In comparison with fully on-chain ChainCash design, 23 | we have to deal with some security relaxation in the case of offchain notes. 24 | 25 | As a simple but pretty secure solution, the following design is proposed, which can then be improved in many directions 26 | (see "Future Extensions" section): 27 | 28 | * every participant has a public key over elliptic curve supported by Ergo blockchain (Secp256k1, the same curve is used 29 | in Bitcoin) 30 | * only reserves are on-chain. A reserve can be created at any time. A reserve is bound to public key of its owner. 31 | Anyone (presumably, owner in most cases) can top the reserve up. 32 | * for keeping offchain cash ledgers, we have trackers. Anyone can launch a tracker service (just running open-source 33 | software on top of powerful enough hardware is needed for that). With time a tracker is getting trust and userbase if 34 | behaves honestly. The design is trying to minimize trust in tracker. For example, a tracker cant redeem IOU notes made 35 | to other parties, as they are signed, and the signature is check in redemption on-chain contract. If tracker is 36 | disappearing, after some period last tracker state snapshot committed on-chain becomes redeemable without it. If tracker 37 | is starting censoring notes associated with a public key, by not including them into on-chain update, it is still 38 | possible to redeem them. There could be different improvements to the tracker design, see "Future Extensions" section. 39 | * IOU note from A to B is represented as (B_pubkey, amount, timestamp, sig_A) record, where amount is the **total** amount of 40 | A's debt before B, timestamp is timestamp of latest payment from A to B, and sig_A is a signature for (B_pubkey, amount, 41 | nonce). Only one updateable note is stored by a tracker, and redeemable onchain. Thus a tracker is storing 42 | (amount, timestamp) pairs for all A->B debt relationships. The tracker commits on-chain to the data by storing a digest 43 | of a tree where hash(A ++ B) acts as a key, and (amount, timestamp) acts as a value. 44 | 45 | * If A has on-chain reserve, B may redeem offchain from A->B note, by providing proof of (amount, timestamp). Reserve 46 | contract UTXO is storing tree of hash(AB) -> timestamp pairs. It is impossible to withdraw a note with timestamp <= 47 | redeemed again. After on-chain redemption, A and B should contact offchain to deduct before next payment from A to B done. 48 | A note may be redeemed only one week after creation (timestamp of last block is one week ahead of timestamp in the note, 49 | at least), thus for services it makes sense to have a lot of rotating keys. 50 | 51 | ## Basis Contract 52 | 53 | A basic contract corresponding to the design outlined in the previous section, is available @ [basis.es](basis.es). 54 | 55 | ## Offchain Logic 56 | 57 | ### Tracker 58 | 59 | Tracker is publishing following events via NOSTR protocol as relay: 60 | 61 | * note - new or updated note, along with proof of tracker state transformation and digest after operation 62 | * redemption - redemption done from a reserve 63 | * reserve top-up 64 | * commitment - posting data for on-chain tracker state commitment update (header, proof of UTXO against header, UTXO with commitment) 65 | * 80% alert - tracker is posting it when debt level of some pubkey reaching 80% of collateral 66 | * 100% alert - tracker is posting it when debt level of some pubkey reaching 100% of collateral 67 | 68 | Then it also supports following API requests which can be run separately from relay potentially: 69 | 70 | * getNotesForKey - returns all the notes sssociated with a pubkey 71 | * getProof - get proof for a note against latest digest published by the tracker (not necessarily committed on-chain) 72 | * getKeyStatus - returns current collateralization of a pubkey along with other important information. Useful for light 73 | wallets and clients which are ready 74 | * POST noteUpdate - create or update a note 75 | 76 | ## Security Assumptions 77 | 78 | We assume that tracker is honestly collecting and announcing notes it has. However, malicious trackers may deviate from 79 | honest behaviour. 80 | 81 | Tracker can simply go offline, but then the latest state committed on-chain is still redeemable, 82 | 83 | Tracker may remove debt notes of protocol participants. This problem can be tackled with the anti-censorship protection 84 | from "Future Extensions" section. 85 | 86 | Tracker may collude with a reserve holder to inject a note with fake timestamp in the past to redeem immediately. 87 | Tracker would be caught in this case. For making this case impossible with contract, technique similar to anti-censorship 88 | protection can be used. 89 | 90 | ## Wallet 91 | 92 | ## Future Extensions 93 | 94 | * Anti-Collusion Protection 95 | 96 | Let's suppose that, at time t1, we have: 97 | 98 | (Bob -> Alice, 2, 9), with the 9th note signed by Bob. 99 | 100 | And, at time t2, we have: 101 | 102 | (Bob -> Alice, 3, 10), with the 10th note signed by Bob. 103 | 104 | Bob (at least, he incentivized to) informing tracker, and the tracker commits on-chain 105 | the latest nonce seen. Also, tracker's signature is required for normal redemption. 106 | 107 | So at the moment t2: 108 | 109 | 1) if committed state is (Bob -> Alice, 3, 10) , Alice can't withdraw (Bob -> Alice, 2, 9) 110 | 2) if committed state is (Bob -> Alice, 3, 9) , Alice can withdraw by colluding with the tracker , 111 | and the misbehavior has onchain footprint 112 | 113 | Possible to introduce protection from the collusion by making debt amount ever increasing (so then it is amount of 114 | offchain debt of Bob before Alice, including redeemed), and storing redeemed amount in Bob's reserve contract as well. 115 | 116 | * Anti-Censorship Protection 117 | 118 | If tracker is starting censoring notes associated with a public key, by not including them into on-chain update, it is still 119 | possible to redeem them with anti-censorship protection. For that, tracker box should be protected with a contract which 120 | has condition to include spent tracker input's id into a tree stored in a register. Then tracker is storing commitment to 121 | all it previous states, basically, and we can use that to add a condition to the reserve contract to allow redemption of 122 | a note which was tracked before but not tracked now, and also not withdrawn. 123 | 124 | * Federated trackers 125 | 126 | Instead of a single tracker, we may have federation, like done in Oracle Pools, or double layered federation like done 127 | in Rosen bridge. 128 | 129 | * Tracking sidechains 130 | 131 | As a continuation of federation tracker idea, we may have tracking sidechains, for example, merged-mined sidechains, to 132 | reduce multisig security to majority-of-Ergo-hashrate-following-sidechain security. 133 | 134 | * Programmable cash 135 | 136 | We may store redeeming condition script hash instead of recipient pubkey just in IOU notes, and add the condition to 137 | other redeeming conditions in onchain redemption action. 138 | 139 | * Multi-tracker reserve 140 | 141 | Possible to have reserve contract with support for multiple reserves, put under AVL+ tree or just in collection if there 142 | are few of them. 143 | 144 | For most reserves that does not make sense probably, but multi-tracker reserves can be used as gateways between 145 | different trackers, to rebalance liquidity etc. 146 | 147 | * Privacy 148 | 149 | Not hard to do redemptions to stealth addresses. 150 | 151 | ## Economy 152 | 153 | ## Implementation Roadmap 154 | 155 | The following implementation plan is targeting catching micropayments in P2P networks, agentic networks, etc ASAP and then 156 | develop tools for community trading: 157 | 158 | * Do tests for Basis contract, like ChainCashSpec or Dexy contracts (Scala) 159 | * Do a token-based variant of reserve contract (ErgoScript) 160 | * Do tracker service (Rust), which is collecting offchain notes and also tracking on-chain reserves, writing 161 | periodically commitments on chain, informing clients about state of notes / reserves (collateralization etc) 162 | * Do Celaut payment module, where peers can set credit limits and pay each other. Add support for agentic layer, so AI agents can buy computations 163 | over Celaut, then requests to other APIs as well. 164 | * Do showcase for agent-to-agent payments 165 | * Do a wallet for community trading (maybe in form of telegram bots? like one wallet bot for one community) 166 | * Do alternative for NOSTR zaps 167 | 168 | and so on 169 | 170 | ## References 171 | -------------------------------------------------------------------------------- /src/test/resources/mockwebserver/node_responses/response_LastHeaders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "57d1b3af4e969cf2a3ed86eb33aca6b03cd40d1dafa656d5364b60ba62d7fbc5", 4 | "timestamp": 1576787597586, 5 | "version": 1, 6 | "adProofsRoot": "feb01c6b9ae7abd1e2477a57608fba47e0b603fdf5e6198ec21dee079821caec", 7 | "stateRoot": "bea5ee03a073076e0ac72bcdf32f0c58632b9fbb15784baca4325636a3babfe014", 8 | "transactionsRoot": "a51eb79a5e0974ad7eeed3c71c77dd6dd03491a64fa9580ddb68a7bdb8da80a5", 9 | "nBits": 108309041, 10 | "extensionHash": "e218d2c938d03a1a303fcff14543f066cfc40e2d02e0e425ff14082f248d84e0", 11 | "powSolutions": { 12 | "pk": "03c29d66dee9549618d1eb49460211ef1b1034bae9fb870a523cbedbfb721db18b", 13 | "w": "02562706e07bcbe421b4cc004f3efed5ba2ab2e65b89bfd5553099a069a9226552", 14 | "n": "00196be4012fc26f", 15 | "d": 616874694223017179690764009630362472282948848357736508023796372 16 | }, 17 | "height": 123405, 18 | "difficulty": 128274315345920, 19 | "parentId": "4381721740076486858f3bc1431b27d1cbaea78c3f835163b47e5867eaed81c3", 20 | "votes": "000000", 21 | "size": 281, 22 | "extensionId": "edb8d2352ff35db480d85b2b23728fcf7ec5fce16eef814c21b75d84f82a0e4d", 23 | "transactionsId": "42df77c39611da468a2a7ad2ec1274cb36f400c642900c16c17af6b8dd45d4fa", 24 | "adProofsId": "b060ab9cf7036a4b21d01f4e85013e47fa35bbb1be5aa6e5f16e7687846b01ad" 25 | }, 26 | { 27 | "id": "06e5c74ebe8e154717b1e6ebc5b5ec9de5540f48acc9f65f995f79f423932b35", 28 | "timestamp": 1576787610138, 29 | "version": 1, 30 | "adProofsRoot": "1ac6d3fce9d51a5f37719e65657af66731bc0496f213777b38f8b2c553637c82", 31 | "stateRoot": "42d6bf85e1741e05ffa069fb71ad5cd86d076a5eb9fc2f8eb3491b911b69b13d14", 32 | "transactionsRoot": "bcba7b6411d99a5e5dde3ec99d2d6fe68b8f9b23be26bc51aca13062355f6140", 33 | "nBits": 108309041, 34 | "extensionHash": "e218d2c938d03a1a303fcff14543f066cfc40e2d02e0e425ff14082f248d84e0", 35 | "powSolutions": { 36 | "pk": "037d9116267f8541bfec7376f5af4a99cbee8a4a0761685039e2ad0fd85c9c001b", 37 | "w": "024d9a62b8d07f0a279d0e05ce3128d729b9b992f8c8919eb74f2b9f91c27a7ef6", 38 | "n": "0017e9fb02977892", 39 | "d": 12249250132392732232803670677709149272558824604915256112633100 40 | }, 41 | "height": 123406, 42 | "difficulty": 128274315345920, 43 | "parentId": "57d1b3af4e969cf2a3ed86eb33aca6b03cd40d1dafa656d5364b60ba62d7fbc5", 44 | "votes": "000000", 45 | "size": 280, 46 | "extensionId": "a9cdd9de880e43a82e5254e745b87e3833b1d318d66a7c411f1db6e53f9c8e4a", 47 | "transactionsId": "48dacb2f41840d90cf4804ed82f4c84994de913ddf1d1cd66b72de4cd8197e10", 48 | "adProofsId": "b76169d75ef0083eab65ab66b47db74a7d8646be4940331148eaf3168653d510" 49 | }, 50 | { 51 | "id": "47bfb8c8e98923aef790364eed08c23d2c61475ef13ecf65f5241cfafebf2f52", 52 | "timestamp": 1576787616472, 53 | "version": 1, 54 | "adProofsRoot": "1c549c7cea750bc5b5055b1918791e88860bfaff6b9f5260dd775301ab4418cc", 55 | "stateRoot": "5e7ed0f28ca3c0d2118df23dd34db70ed56bbf79403386b8756c4400e39959f314", 56 | "transactionsRoot": "d91748228ef36f25ca178b6853457bdc874f2fc408fc4ee8f16bb377d8b9f054", 57 | "nBits": 108309041, 58 | "extensionHash": "ceb4064e9e7c1c279ebed80692026ba0ca9dacb5f4fe033008c4f887bdf99ec5", 59 | "powSolutions": { 60 | "pk": "026352d00ff542b27c6c840819bfa3ce20475584044e972b8547458383ce62759c", 61 | "w": "0393935d9356b41b68c91814d2318807ec0f18fe7a7d8a6e10450772ceb0c64ef2", 62 | "n": "00196ac800110764", 63 | "d": 759323199313205919997463350135768652394296000144643214633658265 64 | }, 65 | "height": 123407, 66 | "difficulty": 128274315345920, 67 | "parentId": "06e5c74ebe8e154717b1e6ebc5b5ec9de5540f48acc9f65f995f79f423932b35", 68 | "votes": "000000", 69 | "size": 281, 70 | "extensionId": "93e9bd5bff1ae8c293d554e168b6c660c3d92eea5090d9e9c40d861dc4519792", 71 | "transactionsId": "3fbd8496ab5c5f3fef940ad5fdf622cb8d6a39ba475f449a35e79f4d943974b9", 72 | "adProofsId": "f26ff31d3d750587920b6ac10b2612e8422d836e9d975f059a7dcb1e59887513" 73 | }, 74 | { 75 | "id": "38d32e6b3b992af85dbfd043e5320733bce903cbd4a3bf1e8b30bdec39041903", 76 | "timestamp": 1576787727362, 77 | "version": 1, 78 | "adProofsRoot": "f84f8b16dceab3e53e5b792f48b76d56d59cd1d0e91733f2209af1d3493775d0", 79 | "stateRoot": "b328f85c0e3e5c9fa177a0e476293cc5399e57db7f4b69774efec5a497fb3a6914", 80 | "transactionsRoot": "1553c774ba7280fc837e3d98291382c7929f846f65cd8d45f296e264044d86c1", 81 | "nBits": 108309041, 82 | "extensionHash": "ceb4064e9e7c1c279ebed80692026ba0ca9dacb5f4fe033008c4f887bdf99ec5", 83 | "powSolutions": { 84 | "pk": "02e6beaa3ba2b5bc1df8594446534b324503ca40af618f4bf7a597de74b5de8cb3", 85 | "w": "0309f97c89322af227657379ba7dd8c0792e1a135da2974aaa3d7a30e9045dacc6", 86 | "n": "0017ebc700a8e841", 87 | "d": 529802822502518617289307207415292846641837671794073105945681614 88 | }, 89 | "height": 123408, 90 | "difficulty": 128274315345920, 91 | "parentId": "47bfb8c8e98923aef790364eed08c23d2c61475ef13ecf65f5241cfafebf2f52", 92 | "votes": "000000", 93 | "size": 281, 94 | "extensionId": "b2ef82d3016b1ece1a08ce8b02fe112e80aa7b3abd72f5c21381d18f493eaa3a", 95 | "transactionsId": "defb2b8b8e3b5f795ab46ed831b6ef0ee3b0c4b15d76caadfb39855a7dcc6522", 96 | "adProofsId": "eefed6f41090c3ce51b9830f92fa1d9c3c22978f198643cda0e1fa7a5e5a7819" 97 | }, 98 | { 99 | "id": "6edef68640a95b3661c3728ed108ee1a68d683e564151e0fce8d5c477a0d9bb0", 100 | "timestamp": 1576787967533, 101 | "version": 1, 102 | "adProofsRoot": "b874d0ef32b0e943365783be4e9e68ccca1f378b344df06b3e04e58b41be4d47", 103 | "stateRoot": "799f5017d7e14f8562852b00e031180c70007b21d3c6b98d0916d515c4dd97d114", 104 | "transactionsRoot": "ab8a42f4e0829a4dd06806a128be80be4fedadfebed9073fb8527706e4f0e426", 105 | "nBits": 108309041, 106 | "extensionHash": "ceb4064e9e7c1c279ebed80692026ba0ca9dacb5f4fe033008c4f887bdf99ec5", 107 | "powSolutions": { 108 | "pk": "03f7ee96df40d8df46a2107cce276b1d2d66924ee83f89f93c226c66d61a114854", 109 | "w": "02f2185103808eb3b84a25594bdcf69b4c07751bcbf18acc5ce57132f41f966df6", 110 | "n": "0017eede023cb66c", 111 | "d": 119715053208836951379625045007368405393746288616654011146543129 112 | }, 113 | "height": 123409, 114 | "difficulty": 128274315345920, 115 | "parentId": "38d32e6b3b992af85dbfd043e5320733bce903cbd4a3bf1e8b30bdec39041903", 116 | "votes": "000000", 117 | "size": 280, 118 | "extensionId": "826ac7f6364ea41d7bd1cd4a1586a796b7acf88dcb42153e48493e1102923b4f", 119 | "transactionsId": "24b541b738a0fe2cbf1e020292861bda10ee706872a28d22f9a9325361fa8eb0", 120 | "adProofsId": "ab3b59d072a867a283e19544cec51d5bbcfd592182cdf806cc9dc27783ed82fe" 121 | }, 122 | { 123 | "id": "3e2ce29ced5aa9f0c10e47a2a752f6784e02976e427eb9f171f557edc68f73f4", 124 | "timestamp": 1576788199400, 125 | "version": 1, 126 | "adProofsRoot": "db2ef84182817e4407fa34aec0cbd82428c99159d672ad3f72a6369e77c81375", 127 | "stateRoot": "632b3c2d39c521db55411dba61e5309d3c398e083e8087232658c70c12e03fc714", 128 | "transactionsRoot": "73555d8a6e3fef0f8509548d92ad664f7543e2f8091d35b11964911bb5e18b7a", 129 | "nBits": 108309041, 130 | "extensionHash": "ff8707b481f0c409ef6bd8032b592fde6a8dc2685e8887568485d7a1de1b4311", 131 | "powSolutions": { 132 | "pk": "02894a1806166e4518cc82042605a5774edd4cf521004b28a02d928981bb3250f6", 133 | "w": "02af603f50bd0c1e22cd147db5c0cf8f954bed96e4e855caf1768bd26f97b4ead9", 134 | "n": "0018cfc503badf48", 135 | "d": 781558564215236691224132925929214921397331611449408367035440118 136 | }, 137 | "height": 123410, 138 | "difficulty": 128274315345920, 139 | "parentId": "6edef68640a95b3661c3728ed108ee1a68d683e564151e0fce8d5c477a0d9bb0", 140 | "votes": "000000", 141 | "size": 281, 142 | "extensionId": "80cd9b61c092ea11f53792fe5a7586b201ed2bb97f5fff3f4e3baa994a79241c", 143 | "transactionsId": "c9876f3b2e977c35c5d7807c221cf54512b0cd4126be86fa0122de7ea374566b", 144 | "adProofsId": "3858834aeee4ad03baffdfa9951df24419e4586c66ac65e8c487b334e205a58b" 145 | }, 146 | { 147 | "id": "d3faf9c669d43a5ee7503ea7f1ffe3584549830893b1bf7efabba98525f914db", 148 | "timestamp": 1576788482225, 149 | "version": 1, 150 | "adProofsRoot": "96afda4ae45e4d99908ad4f4fd5f2aa0bcfe8d1b0b359e4be20f8f50e895db85", 151 | "stateRoot": "15e4190dede4d7a0389f4f62c62a3e5a886ca092ed4f2715f4cf88bd9cf503ae14", 152 | "transactionsRoot": "c7a2d5099b6e98e83064f59aa4b7da86da5d5b70bfaad01b62d45618348d027a", 153 | "nBits": 108309041, 154 | "extensionHash": "ff8707b481f0c409ef6bd8032b592fde6a8dc2685e8887568485d7a1de1b4311", 155 | "powSolutions": { 156 | "pk": "03cc51235f08da2c45d8124bcaa065630326d80d236b8b3d19c8493aba1410cf05", 157 | "w": "02f3654fa929403f3b4bce3f197f5d9859b5ad90e36321f796ee87bcc07e63a9bd", 158 | "n": "00001df7038711f2", 159 | "d": 837670224394260869848105947076576265927782468494678752756966687 160 | }, 161 | "height": 123411, 162 | "difficulty": 128274315345920, 163 | "parentId": "3e2ce29ced5aa9f0c10e47a2a752f6784e02976e427eb9f171f557edc68f73f4", 164 | "votes": "000000", 165 | "size": 281, 166 | "extensionId": "3b2503db3bf1fbba8646757d4bc25a4bff27db01d8ed9acf83992c29d7038a93", 167 | "transactionsId": "8d5d68da503a94622432ff5bd93f0e642843cc4bf6e283e9fd79b795c051bfbc", 168 | "adProofsId": "90835c34cae0475a512fb485cb19194652a811d42816ca6dfe75d38d444f98f6" 169 | }, 170 | { 171 | "id": "e0de749a896c0170e3dad14b0f2b4119aab19c2f4639a929ba7812c4f1f77f77", 172 | "timestamp": 1576788534240, 173 | "version": 1, 174 | "adProofsRoot": "1157e7ff2377ddf684ef48ddfa318e5986057d16c12400a596167b5e2a7cdc8a", 175 | "stateRoot": "78acd902220193c2bdf62737ba1664ef72c5c7f0b61231c0b50c23dbd5876f5314", 176 | "transactionsRoot": "048f5ea69d8bded9829ed42df07667eacc5ea879c633b8edd4004508ebd80701", 177 | "nBits": 108309041, 178 | "extensionHash": "ff8707b481f0c409ef6bd8032b592fde6a8dc2685e8887568485d7a1de1b4311", 179 | "powSolutions": { 180 | "pk": "0213f7f50119b635cde94adc797ce99bc2698c421ab3f74a5e6077a7d3a5d9e386", 181 | "w": "0254183066d2eac57f27b1ba940002278732d1a48610c66735fbe803159e83ecd8", 182 | "n": "001943ee00293ed7", 183 | "d": 868223364824661210489397316912371799220417013375955106566666841 184 | }, 185 | "height": 123412, 186 | "difficulty": 128274315345920, 187 | "parentId": "d3faf9c669d43a5ee7503ea7f1ffe3584549830893b1bf7efabba98525f914db", 188 | "votes": "000000", 189 | "size": 281, 190 | "extensionId": "1cbb10eb91c0a3adfd133d3b1cceaa464e1344e670d70229890d9c19752421ee", 191 | "transactionsId": "1b83abf47657d5e2b381ccf23f76ba361aedc0e2ae9a1b388c34cb02936da3a4", 192 | "adProofsId": "21c3da8dd694aae7ab979aae994ae7218c3400b0fb3442ab64c6c49f972a9222" 193 | }, 194 | { 195 | "id": "0895a4a3dddbfd8faee6d9f957c42b64ee328714221809ad20d8fa4d036e1bf9", 196 | "timestamp": 1576788778408, 197 | "version": 1, 198 | "adProofsRoot": "854ff2e9688470c5a6586688a1c9fe3bfa548f791f51f1017cfa7f60caea8a2a", 199 | "stateRoot": "9cb3f3054d0a2d7e88b3f2dba5c3e3588259da99b0432d7275b61a873ea3143e14", 200 | "transactionsRoot": "986f6da6ca160e5d9df454c16c1d4d8e70d461ea8768b5f213fb1951451179b7", 201 | "nBits": 108309041, 202 | "extensionHash": "ff8707b481f0c409ef6bd8032b592fde6a8dc2685e8887568485d7a1de1b4311", 203 | "powSolutions": { 204 | "pk": "02a2ac3698e77e6a9fc5872bb56dad50de9a27594b12fd0dea0559ce43d3a6c08b", 205 | "w": "03a28f786076906404fdc268410665e67df07305bd8a1d08e961cc6025f6e43897", 206 | "n": "0019f9cf02dd166f", 207 | "d": 879995400658947226090967819482925381908353602360798422310149941 208 | }, 209 | "height": 123413, 210 | "difficulty": 128274315345920, 211 | "parentId": "e0de749a896c0170e3dad14b0f2b4119aab19c2f4639a929ba7812c4f1f77f77", 212 | "votes": "000000", 213 | "size": 281, 214 | "extensionId": "e102465ebf751dfdb4e3547910d541d8b70db7cacca986ee928547e65f954079", 215 | "transactionsId": "b7e93054a1363042fdb9e36a63357fb74855d16891ff4006503eb874c7f728ac", 216 | "adProofsId": "a18539ed956b953bd36f12afff207a0fb7abd38bb8cfaa17f1401519c3813c1e" 217 | }, 218 | { 219 | "id": "8d87fd4a7372877462ff7ecb52a6063207ffda2689f4de3254cc9a2877953f4f", 220 | "timestamp": 1576789077841, 221 | "version": 1, 222 | "adProofsRoot": "639b35e46f28aba7976719186788e3553b315a09bb9e9699b03f8f11b76aa498", 223 | "stateRoot": "25ff1da6e5bf14304196c19461a4bd24f8ce6c493153baea3c3d6efe17d4bda814", 224 | "transactionsRoot": "39dd1b70b5990ffcd65833f77bcdfa24577162c8bf40328626c2aa2038c60f15", 225 | "nBits": 108309041, 226 | "extensionHash": "ff8707b481f0c409ef6bd8032b592fde6a8dc2685e8887568485d7a1de1b4311", 227 | "powSolutions": { 228 | "pk": "03d7b37fcacec8759f6321053b3dcf5c468ea5bcfd4adb2dce5ceeeaeeb28eca60", 229 | "w": "02821250d4fdeb2e0d917a3c7037702d901979a859c3311ea22029bf781f016e7b", 230 | "n": "0019d9f40276e5a9", 231 | "d": 177836710894285496567430851674379430857637034510197170731469383 232 | }, 233 | "height": 123414, 234 | "difficulty": 128274315345920, 235 | "parentId": "0895a4a3dddbfd8faee6d9f957c42b64ee328714221809ad20d8fa4d036e1bf9", 236 | "votes": "000000", 237 | "size": 279, 238 | "extensionId": "3a32c875d052885fceb9ff852a707d2bab3aec2a79c58b503b3fbb21cb5bbd6c", 239 | "transactionsId": "1a652eca9a03aa6c5de86d3919bc9189c373771fd433da53ab9c4ea19bccc2f2", 240 | "adProofsId": "e0f85adbe5de06dd15cce477291ffc22b6b734d004b17d6682e8f9feefe28424" 241 | } 242 | ] -------------------------------------------------------------------------------- /docs/whitepaper/chaincash.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} % list options between brackets 2 | 3 | \usepackage{color} 4 | \usepackage{graphicx} 5 | %% The amssymb package provides various useful mathematical symbols 6 | \usepackage{amssymb} 7 | %% The amsthm package provides extended theorem environments 8 | %\usepackage{amsthm} 9 | \usepackage{amsmath} 10 | 11 | \usepackage{listings} 12 | 13 | \usepackage{hyperref} 14 | 15 | \usepackage{systeme} 16 | 17 | \def\shownotes{1} 18 | \def\notesinmargins{0} 19 | 20 | \ifnum\shownotes=1 21 | \ifnum\notesinmargins=1 22 | \newcommand{\authnote}[2]{\marginpar{\parbox{\marginparwidth}{\tiny % 23 | \textsf{#1 {\textcolor{blue}{notes: #2}}}}}% 24 | \textcolor{blue}{\textbf{\dag}}} 25 | \else 26 | \newcommand{\authnote}[2]{ 27 | \textsf{#1 \textcolor{blue}{: #2}}} 28 | \fi 29 | \else 30 | \newcommand{\authnote}[2]{} 31 | \fi 32 | 33 | \newcommand{\knote}[1]{{\authnote{\textcolor{green}{kushti notes}}{#1}}} 34 | \newcommand{\mnote}[1]{{\authnote{\textcolor{red}{scalahub notes}}{#1}}} 35 | 36 | \usepackage[dvipsnames]{xcolor} 37 | \usepackage[colorinlistoftodos,prependcaption,textsize=tiny]{todonotes} 38 | 39 | 40 | % type user-defined commands here 41 | \usepackage[T1]{fontenc} 42 | 43 | \usepackage{flushend} 44 | 45 | 46 | \newcommand{\cc}{ChainCash} 47 | 48 | \newcommand{\ma}{\mathcal{A}} 49 | \newcommand{\mb}{\mathcal{B}} 50 | \newcommand{\he}{\hat{e}} 51 | \newcommand{\sr}{\stackrel} 52 | \newcommand{\ra}{\rightarrow} 53 | \newcommand{\la}{\leftarrow} 54 | \newcommand{\state}{state} 55 | 56 | \newcommand{\ignore}[1]{} 57 | \newcommand{\full}[1]{} 58 | \newcommand{\notfull}[1]{#1} 59 | \newcommand{\rand}{\stackrel{R}{\leftarrow}} 60 | \newcommand{\mypar}[1]{\smallskip\noindent\textbf{#1.}} 61 | 62 | \begin{document} 63 | 64 | \title{ChainCash - elastic peer-to-peer money creation via trust and blockchain assets} 65 | \author{kushti \\ \href{mailto:kushti@protonmail.ch}{kushti@protonmail.ch} \and scalahub} 66 | 67 | 68 | \maketitle 69 | 70 | \begin{abstract} 71 | In this paper we introduce ChainCash, a protocol to create money in self-sovereign way via trust or collateral. The 72 | protocol allows for elastic money creation in peer-to-peer environment, without any authority, being it authoritarian, 73 | democractic or algorithmic. For that, acceptance of notes created by other peers is an individual choice. 74 | \end{abstract} 75 | 76 | 77 | \section{Introduction} 78 | 79 | Currently, most of monetary value is created by private banks~(often, offshore banks as in so-called "eurodollar" system~\cite{machlup1970euro}) following central banks requirements. As an alternative, starting with Bitcoin~\cite{nakamoto2008peer} launch in 2009, a lot of cryptocurrencies 80 | and DeFi applications on top of public blockchains are experimenting with algorithmic money issuance. As another option, we also have alternative, usually local, monetary systems, such as LETS~(local exchange trading systems~\cite{williams1996new}), timebanks, local government currencies~\cite{unterguggenbercer1934end}, and so on. Control in traditional fiat monetary systems is possessed by big players~(with rich getting richer effect) creating money in non-transparent ways~(especially in offshore circuits) without reasonable limits, on the other side, fiat monetary systems (in opposite to commodity money used before fiat, as well as alternative monetary systems) have best supply elasticity. Cryptocurrencies~(and, sometimes, other tokens on top of public blockchains) have strict algorithms emitting new money, thus they have publicly known emission schedule, which makes them perfect digital commodity assets, on the other hand, supply is disconnected from economic activity and not elastic. Local currencies usually considered more fair in segniorage distribution than fiat currencies, they are successfully boosting local economies often, but, in opposite to fiat and crypto-currencies, they are not global and 81 | usually are dying without active core. Elasticity of supply is also limited simply due to entry barriers for external actors. 82 | 83 | In this paper, we propose \cc{}, a new global kind of money, with decentralized issuance, elastic supply. \cc{} notes are collectively backed by collateral and trust. \cc{} acts on top of Ergo blockchain~(possibly, other public blockchains as well, 84 | however, for efficient implementation there is requirement for primitives which can be found in Ergo only at the moment, to the best of our knowledge). Thus collateral for a \cc{} comes from reserves network peers may have, and on spending a note, a peer is attaching its 85 | reserve to collective backing. At the same time, a newly issued note could be accepted by a peer without any backing provided, for example, if such a note is issued by a friend or a trusted charity. Every peer in the system is having own individual rules 86 | for accepting notes~(widely accepted standards may exist at the same time), which provides basis for elasticity of supply. We are providing details in the next section. 87 | 88 | \section{\cc{} Design} 89 | \label{sec-design} 90 | 91 | We consider money here via its medium-of-exchange property. For existing currencies, there are usually many options to represent value, such as coins, paper or plastic banknotes, digital records in different ledgers, etc. For \cc{}, we define money as a set of digital notes, each has some nominal~(not fixed but arbitrary in our case). Value of a note is nominated in some existing widely recognizeable unit-of-account, for example, in milligrams of gold. 92 | 93 | We consider that an economy is consisting of known agents $a_1, ..., a_n$. Then we can define medium-of-exchange property of money via a set of agents accepting monetary objects~(i.e. notes). Usually, set of agents accepting some kind of money~(e.g. local or foreign currency) is fixed. That is, for every monetary object~(e.g. a note) which belongs to a fixed sort of money, an agent is accepting it as a mean of incoming payment, or reject. In opposite, for \cc{} money, similarly to~\cite{saito2003peer}, the set is individual for a note, so when agent $a_i$ sees a note $n$, it applies its personal predicate $P_i(n)$ to decide whether to accept the note or decline it. 94 | 95 | Then how notes are different in case of \cc{}? We consider that every note is collectively backed by all the previous spenders of the note. Every agent may create reserves to be used as collateral. When an agent spends note, whether received previously from another agent or just created by the agent itself, it is attaching its signature to it. A note could be redemeed at any time against any of reserves of agents previously signed the note. However,any agent after the first one in signatures chain is getting redemption receipt which is indicating debt of previous signers before him, and then he may redeem the receipt against a reserve of any previous signer, with a new redeemable receipt being generated. Also, redemption fee should be paid, and the fee is incentivizing reserves provision and also using the notes instead of redeeming them. The protocol does not impose collateralization requirements, it is allowed for an agent to issue and spend notes with empty reserve even. It is up to agent's counter-parties then whether to accept and so back an issued note with collateral or agent's trust or not. 96 | 97 | As an example, consider a small gold mining cooperative in Ghana issuing a note backed by (tokenized) gold. The note is then accepted by the national government as mean of tax payment. Then the government is using the note~(which is now backed by gold and also trust in Ghana government, so, e.g. convertible to Ghanaian Cedi as well) to buy oil from a Saudi oil company. Then the oil company, having its own oil reserve also, is using the note to buy equipment from China. Now a Chinese company has a note which is backed by gold, oil, and Cedis. It could be hard maybe for Chinese company to redeem from a small cooperative in Ghana, so it can redeem from Ghana government, and the government may redeem from the cooperative. 98 | 99 | Agent's note quality estimation predicate $P_i(n)$ is considering collaterals and trust of previous spenders. Different agents may have different 100 | collateralization estimation algorithm~(by analyzing history of the single note $n$, or e.g. all the notes issued by previous signers of $n$, other options are also possible), different whitelists, blacklists, or trust scores assigned to previous spenders of the note $n$ etc. So in general case payment sender first need to consult with the receiver on whether the payment~(consisting of one or multiple notes) can be accepted. However, in the real world likely there will be standard predicates, thus payment receiver~(e.g. an online shop) may publish its predicate (or just predicate id) online, and then a payment can be done without prior interaction. 101 | 102 | We propose to implement \cc{} monetary system on top of a public blockchain as: 103 | 104 | \begin{itemize} 105 | \item{} blockchain provides an instant solution for public-key infrastructure 106 | \item{} public blockchain allows for a global ledger solution with minimal trust assumptions~\cite{kya} 107 | \item{} as a consequence, global public ledger allows for simple analysis of notes in existence 108 | \item{} smart contracts minimize trust issues in payment execution and redemption. If native blockchain currency and assets on top of it~(such as algorithmic stablecoins) used in reserves, trust issues in redemption could be eliminated at all. If tokenized real-world commodities and fiat currencies~(e.g. USDT) are used in reserves, redemption could not be completely trustless~(as smart contracts do not have power off the chain), but at least there is transparent accounting in on-chain part of redemption 109 | \end{itemize} 110 | 111 | We use Ergo as a Proof-of-Work blockchain to implement \cc{}, as it is built on minimal trust assumptions~\cite{kya}, and UTXO transactional model as well as AVL+ trees support are making notes implementation feasible. 112 | 113 | 114 | \section{\cc{} Implementation} 115 | 116 | For blockchain-based \cc{} implementation, we consider implementation of the following two main parts: 117 | 118 | \begin{itemize} 119 | \item{} contracts for notes, reserves, and redemption receipts. Here, we consider on-chain contracts as the most 120 | straightforward option. Then we may consider more scalable options, such as having reserves (and maybe receipts) only 121 | on chain, and have notes making progress on a side-chain or off-chain (on top of some Layer 2 solution) 122 | \item{} client software~(which we refer to as \cc{} Server as well), which is interacting with the blockchain (possibly, also a sidechain, or p2p network 123 | where notes are making progress off-chain). This software is implementing $a_i$ agent's functionality from the Section~\ref{sec-design}, 124 | including note quality estimation predicate $P_i(n)$. For that, the client may potentially track all the reserves and notes. 125 | Client's $P_i(n)$ may be configured via whitelists, blacklists, collateralization requirements provided in config. 126 | \cc{} Server can be seen as a bank as in \"be your own bank\" used in Bitcoin community, however, in Bitcoin a node 127 | is a passive and indistinguishable from others bank just validating common history, while \cc{} Server has individual behavior defined by its config. 128 | \end{itemize} 129 | 130 | On-chain contracts are available at~\cite{contracts}. Three contracts can be found there, namely, reserve, note and receipt contracts. Reserve contract locks ERG native tokens on top of Ergo blockchain and allow to redeem native or custom tokens when a note is presented. Note contract ensures that the note has proper history, that is, on every spending a valid signature of corresponding reserve owner is added. It also and allows for a note to be split into two parts~(payment and change), and allows for note redemption. 131 | On redemption, where both reserve and note contracts are involved, an output with receipt contract is created, which contains history of ownership copied from the note input, as well as position of reserve redeemed in ownership chain and note's value. With receipt it is possible then to redeem againt a re~(reserve contract allows for that). 132 | 133 | Reference \cc{} Server implementation (in Rust programming language) can be found at~\cite{server}. 134 | 135 | Basic contracts implementation described is good for starters, but can be extended in many ways. We note that it is possible 136 | to add new features without need for the whole network to update. New features, such as new reserve and note contracts, 137 | can be proposed in form of CCIPs~(ChainCash Improvement Proposals). ChainCash 138 | Server may support new features, in particular, new forms of notes. If client is asked to accept a note with unknown 139 | contract, or a note backed by unknown contract, it is just refusing to accept the note. 140 | 141 | \section{Applications} 142 | \label{sec-apps} 143 | 144 | \cc{} could be seen as a powerful foundation for other monetary systems, and we are going to show it in this section. 145 | 146 | \subsection{LETS} 147 | 148 | To implement a local exchange trading system on top of \cc{}, every LETS member needs to whitelist every over member, so they will accept notes of each other regardless reserves backing the notes, and thus LETS can create money within the community (the LETS circle) with no limits. On the other hand, unlike traditional LETS, notes can circulate outside the LETS circle easily as well. Implementations may vary from LETS members whitelisting unconditionally only notes issued by other members to members whitelisting notes ever signed by LETS members. 149 | 150 | 151 | \subsection{Local Currencies} 152 | 153 | A local or even national government may issue notes and enforce their acceptance within its jurisdiction by enforcing economic agents to accept notes issued or spend by the government. As well as in a LETS implementation, enforced acceptance rules may vary. 154 | 155 | Often local currencies are introducing redemption fee, to promote local usage. In \cc{}, similar goals can be achieved via modifying the reserve contract in a way that non-locals need to pay redemption fee while locals need not, alternatively, the note contract could be modified in a way that spending to non-local addresses incurs a fee. Local currencies are often associated with demurrage, after well-known Woergl experiment~\cite{unterguggenbercer1934end}. Demmurage could be implemented by modifying note contract. However, modifying note contract makes notes locked by it less appealing to others, but that is common for local currencies already. 156 | 157 | \subsection{Multilateral Trade-Credit Set-off} 158 | 159 | Multilateral Trade-Credit Set-off~\cite{mtcs} is a technique which allows invoices in closed loops to be cleared against one another. 160 | In \cc{}, it is possible to clear mutual debts by just burning atomically tokens backed by counter-partis in a single 161 | transaction. This will allow them to issue more notes after. 162 | 163 | \section{\cc{} Advantages and Drawbacks} 164 | 165 | In this section, we are providing some thoughts on possible advantages and drawbacks of \cc{}. Note that practice can show 166 | completely different picture from what we are providing here~(as often happens). 167 | 168 | Advantages: 169 | \begin{itemize} 170 | \item ChainCash is quite unique, to the best of our knowledge, framework, where trust and backing with collateral are 171 | seamlessly combined in money issuance. 172 | \item unlike native cryptocurrrencies and algorithmic stablecoin, \cc{} provides elasticity of supply without enforcing 173 | individual users to accept notes of lower quality - it is always up to users what to accept. 174 | \item as Section~\ref{sec-apps} shows, a variety of known monetary systems can be built on top of \cc{}. 175 | \end{itemize} 176 | 177 | Drawbacks: 178 | \begin{itemize} 179 | \item ChainCash notes are non-fungible, while they share the same unit-of-account, each note has unique backing. This prevents ChainCash usage 180 | in many DeFi applications, such as liquidity pools, lending pools etc. We note that, similarly, DAI stablecoins issued against CDPs 181 | (collateralized debt posistions) with different collateralization also should be priced differently. And like DAI is assigning the same price to 182 | DAIs of different quality, there could be services on top of \cc{} combining notes of certain quality, buy e.g. exchanging them with service tokens 183 | which then can be used in DeFi services. 184 | \item There is no privacy in \cc{} payments now. This topic is fully left for further research. 185 | \end{itemize} 186 | 187 | 188 | \newpage 189 | \bibliography{sources} 190 | \bibliographystyle{ieeetr} 191 | 192 | \end{document} 193 | -------------------------------------------------------------------------------- /contracts/layer2-old/redemption.es: -------------------------------------------------------------------------------- 1 | { 2 | // Redemption box contract 3 | // 4 | // A box with this contract contains collateral and redemption data, and allows anyone to claim the collateral if 5 | // redemption data is malformed 6 | // 7 | // Tokens: 8 | // #0 - redemption contract token 9 | // 10 | // Registers: 11 | // R4: history tree (position -> (a, z)), where (a, z) is sig for (prev tree hash, reserve id, note value, holder id) 12 | // R5: max position in the tree 13 | // R6: redeem position 14 | // R7: (max contested position, contestMode) - initially (-1, false) 15 | // R8: deadline 16 | 17 | // 720 blocks for contestation 18 | 19 | // Dispute actions: 20 | // * wrong position (there is a leaf in the tree with the same position) - collateral seized - done 21 | // * wrong collateral - done in redemption request contract 22 | // * tree leaf not known (collateral not seized) - done 23 | // * earlier reserve exists (collateral not seized) - done 24 | // * tree cut - collateral seized - done 25 | // * double spend - collateral seized - to be done in reserve 26 | // * wrong value transition - collateral seized 27 | // * wrong leaf (signature) in the tree - collateral seized 28 | // * wrong link in the tree - collateral seized 29 | // * negative position in the tree - collateral seized 30 | 31 | val action = getVar[Byte](0).get 32 | 33 | // todo: split into action contracts like done in dexy? 34 | if (action < 0) { 35 | // all the dispute are labelled with negative action ids 36 | if (action == -1) { 37 | // wrong max position (R5 register) claim 38 | // we check that there is leaf in the tree with current position exists 39 | // if so, collateral can be spent 40 | 41 | val pos = SELF.R5[Long].get + 1 42 | 43 | val treeHashDigest = getVar[Coll[Byte]](1).get 44 | val reserveId = getVar[Coll[Byte]](2).get 45 | val noteValue = getVar[Long](3).get 46 | val holderId = getVar[Coll[Byte]](4).get 47 | val a = getVar[GroupElement](5).get 48 | val aBytes = a.getEncoded 49 | val zBytes = getVar[Coll[Byte]](7).get 50 | val properFormat = treeHashDigest.size == 32 && reserveId.size == 32 && holderId.size == 32 51 | val message = treeHashDigest ++ reserveId ++ longToByteArray(noteValue) ++ holderId 52 | 53 | // Computing challenge 54 | val e: Coll[Byte] = blake2b256(message) // weak Fiat-Shamir - todo: should be strong 55 | val eInt = byteArrayToBigInt(e) // challenge as big integer 56 | 57 | val g: GroupElement = groupGenerator 58 | val z = byteArrayToBigInt(zBytes) 59 | val reserve = CONTEXT.dataInputs(0) 60 | val reserveIdValid = reserve.tokens(0)._1 == reserveId 61 | val reservePk = reserve.R4[GroupElement].get 62 | val properSignature = (g.exp(z) == a.multiply(reservePk.exp(eInt))) && properFormat && reserveIdValid 63 | 64 | val proof = getVar[Coll[Byte]](8).get 65 | val history = SELF.R4[AvlTree].get 66 | val keyBytes = longToByteArray(pos) 67 | val properProof = history.get(keyBytes, proof).get == (aBytes ++ zBytes) 68 | 69 | // preservation not checked so collateral could be fully spent 70 | sigmaProp(properSignature && properProof) 71 | } else if (action == -2) { 72 | // tree leaf contents is asked or provided 73 | 74 | val selfOutput = OUTPUTS(0) 75 | 76 | val selfPreservationExceptR7 = selfOutput.tokens == SELF.tokens && 77 | selfOutput.value == SELF.value && 78 | selfOutput.R4[AvlTree].get == SELF.R4[AvlTree].get && 79 | selfOutput.R5[Long].get == SELF.R5[Long].get && 80 | selfOutput.R6[Long].get == SELF.R6[Long].get && 81 | selfOutput.R7[(Long, Boolean)].get == SELF.R7[(Long, Boolean)].get && 82 | selfOutput.R8[Int].get == SELF.R8[Int].get 83 | 84 | val r7 = SELF.R7[(Long, Boolean)].get 85 | val maxContestedPosition = r7._1 86 | val contested = r7._2 87 | if (contested) { 88 | // tree leaf provided 89 | // todo: change path should be provided also likely 90 | val currentContestedPosition = maxContestedPosition + 1 91 | val reserve = CONTEXT.dataInputs(0) 92 | 93 | val treeHashDigest = getVar[Coll[Byte]](1).get 94 | val reserveId = getVar[Coll[Byte]](2).get 95 | val noteValue = getVar[Long](3).get 96 | val holderId = getVar[Coll[Byte]](4).get 97 | val a = getVar[GroupElement](5).get 98 | val aBytes = a.getEncoded 99 | val zBytes = getVar[Coll[Byte]](6).get 100 | val properFormat = treeHashDigest.size == 32 && reserveId.size == 32 && holderId.size == 32 101 | val message = treeHashDigest ++ reserveId ++ longToByteArray(noteValue) ++ holderId 102 | 103 | // Computing challenge 104 | val e: Coll[Byte] = blake2b256(message) // weak Fiat-Shamir - todo: should be strong 105 | val eInt = byteArrayToBigInt(e) // challenge as big integer 106 | 107 | val g: GroupElement = groupGenerator 108 | val z = byteArrayToBigInt(zBytes) 109 | val reserveIdValid = reserve.tokens(0)._1 == reserveId 110 | val reservePk = reserve.R4[GroupElement].get 111 | val properSignature = (g.exp(z) == a.multiply(reservePk.exp(eInt))) && properFormat && reserveIdValid 112 | 113 | val keyBytes = longToByteArray(currentContestedPosition) 114 | 115 | val proof = getVar[Coll[Byte]](7).get 116 | val currentPosition = SELF.R5[Long].get 117 | val history = SELF.R4[AvlTree].get 118 | val properProof = history.get(keyBytes, proof).get == (aBytes ++ zBytes) 119 | 120 | val outR7 = selfOutput.R7[(Long, Boolean)].get 121 | val outPositionCorrect = currentContestedPosition 122 | val outR7Valid = outR7._1 == outPositionCorrect && outR7._2 == false 123 | 124 | sigmaProp(properProof && properSignature && selfPreservationExceptR7 && outR7Valid) 125 | } else { 126 | // tree leaf asked 127 | val outR7 = selfOutput.R7[(Long, Boolean)].get 128 | val outR7Valid = outR7._1 == maxContestedPosition && outR7._2 == true 129 | // todo: move deadline 130 | if (maxContestedPosition == SELF.R5[Long].get) { 131 | // can't ask for more leafs when the whole tree walked through 132 | sigmaProp(false) 133 | } else { 134 | sigmaProp(selfPreservationExceptR7 && outR7Valid) 135 | } 136 | } 137 | } else if (action == -3) { 138 | 139 | // earlier reserve exists (collateral not seized, as a reserve can be increased after redemption box created) 140 | 141 | val selfOutput = OUTPUTS(0) 142 | val redeemPosition = SELF.R6[Long].get 143 | val alternativePosition = getVar[Long](1).get 144 | val redeemReserve = CONTEXT.dataInputs(0) 145 | val altReserve = CONTEXT.dataInputs(1) 146 | 147 | val selfPreservation = selfOutput.tokens == SELF.tokens && 148 | selfOutput.value == SELF.value && 149 | selfOutput.R4[AvlTree].get == SELF.R4[AvlTree].get && 150 | selfOutput.R5[Long].get == SELF.R5[Long].get && 151 | selfOutput.R6[Long].get == SELF.R6[Long].get && 152 | selfOutput.R7[(Long, Boolean)].get == SELF.R7[(Long, Boolean)].get && 153 | selfOutput.R8[Int].get == SELF.R8[Int].get 154 | 155 | val g: GroupElement = groupGenerator 156 | 157 | // checking alt redeem leaf signature 158 | val altReserveLeafTreeHash = getVar[Coll[Byte]](2).get 159 | val altReserveLeafReserveId = getVar[Coll[Byte]](3).get 160 | val altReserveLeafNoteValue = getVar[Long](4).get 161 | val altReserveLeafHolderId = getVar[Coll[Byte]](5).get 162 | val altReserveLeafLeafA = getVar[GroupElement](6).get 163 | val altReserveLeafABytes = altReserveLeafLeafA.getEncoded 164 | val altReserveLeafZBytes = getVar[Coll[Byte]](7).get 165 | val altReserveLeafProperFormat = altReserveLeafTreeHash.size == 32 && altReserveLeafReserveId.size == 32 && altReserveLeafHolderId.size == 32 166 | val altReserveLeafMessage = altReserveLeafTreeHash ++ altReserveLeafReserveId ++ longToByteArray(altReserveLeafNoteValue) ++ altReserveLeafHolderId 167 | val altReserveLeafEInt = byteArrayToBigInt(blake2b256(altReserveLeafMessage)) // weak Fiat-Shamir - todo: should be strong 168 | val altReserveLeafZ = byteArrayToBigInt(altReserveLeafZBytes) 169 | val altReserveLeafReserveIdValid = altReserve.tokens(0)._1 == altReserveLeafReserveId 170 | val altReserveLeafReservePk = altReserve.R4[GroupElement].get 171 | val altReserveLeafProperSignature = (g.exp(altReserveLeafZ) == altReserveLeafLeafA.multiply(altReserveLeafReservePk.exp(altReserveLeafEInt))) && altReserveLeafProperFormat && altReserveLeafReserveIdValid 172 | 173 | // checking last leaf tree proof 174 | val keyBytes = longToByteArray(alternativePosition) 175 | val proof = getVar[Coll[Byte]](8).get 176 | val history = SELF.R4[AvlTree].get 177 | val properProof = history.get(keyBytes, proof).get == (altReserveLeafABytes ++ altReserveLeafZBytes) 178 | 179 | sigmaProp(altReserve.value >= redeemReserve.value && alternativePosition < redeemPosition && 180 | selfPreservation && altReserveLeafProperSignature && properProof) 181 | } else if (action == -4) { 182 | // tree cut - collateral seized 183 | // here, we check that there is a signature from current holder for a record after last tree's leaf 184 | // we have to check last leaf also, to check holder correctness 185 | val lastLeafReserve = CONTEXT.dataInputs(0) 186 | val holderReserve = CONTEXT.dataInputs(1) 187 | 188 | val g: GroupElement = groupGenerator 189 | 190 | // checking last leaf signature 191 | val lastLeafTreeHashDigest = getVar[Coll[Byte]](1).get 192 | val lastLeafReserveId = getVar[Coll[Byte]](2).get 193 | val lastLeafNoteValue = getVar[Long](3).get 194 | val lastLeafHolderId = getVar[Coll[Byte]](4).get 195 | val lastLeafA = getVar[GroupElement](5).get 196 | val lastLeafABytes = lastLeafA.getEncoded 197 | val lastLeafZBytes = getVar[Coll[Byte]](6).get 198 | val lastLeafProperFormat = lastLeafTreeHashDigest.size == 32 && lastLeafReserveId.size == 32 && lastLeafHolderId.size == 32 199 | val lastLeafMessage = lastLeafTreeHashDigest ++ lastLeafReserveId ++ longToByteArray(lastLeafNoteValue) ++ lastLeafHolderId 200 | val lastLeafEInt = byteArrayToBigInt(blake2b256(lastLeafMessage)) // weak Fiat-Shamir - todo: should be strong 201 | val lastLeafZ = byteArrayToBigInt(lastLeafZBytes) 202 | val lastLeafReserveIdValid = lastLeafReserve.tokens(0)._1 == lastLeafReserveId 203 | val lastLeafReservePk = lastLeafReserve.R4[GroupElement].get 204 | val lastLeafProperSignature = (g.exp(lastLeafZ) == lastLeafA.multiply(lastLeafReservePk.exp(lastLeafEInt))) && lastLeafProperFormat && lastLeafReserveIdValid 205 | 206 | // checking last leaf tree proof 207 | val lastLeafPosition = SELF.R5[Long].get 208 | val keyBytes = longToByteArray(lastLeafPosition) 209 | val proof = getVar[Coll[Byte]](7).get 210 | val history = SELF.R4[AvlTree].get 211 | val properProof = history.get(keyBytes, proof).get == (lastLeafABytes ++ lastLeafZBytes) 212 | 213 | // check holder's record signed 214 | val holderTreeHashDigest = getVar[Coll[Byte]](8).get 215 | val holderReserveId = getVar[Coll[Byte]](9).get 216 | val holderNoteValue = getVar[Long](10).get 217 | val holderHolderId = getVar[Coll[Byte]](11).get 218 | val holderA = getVar[GroupElement](12).get 219 | val holderABytes = holderA.getEncoded 220 | val holderZBytes = getVar[Coll[Byte]](13).get 221 | val holderProperFormat = holderTreeHashDigest.size == 32 && holderReserveId.size == 32 && holderHolderId.size == 32 222 | val holderMessage = holderTreeHashDigest ++ holderReserveId ++ longToByteArray(holderNoteValue) ++ holderHolderId 223 | val holderEInt = byteArrayToBigInt(blake2b256(holderMessage)) // weak Fiat-Shamir - todo: should be strong 224 | val holderZ = byteArrayToBigInt(holderZBytes) 225 | val holderReserveIdValid = holderReserve.tokens(0)._1 == holderReserveId 226 | val holderReservePk = holderReserve.R4[GroupElement].get 227 | val holderProperSignature = (g.exp(holderZ) == holderA.multiply(holderReservePk.exp(holderEInt))) && holderProperFormat && holderReserveIdValid 228 | 229 | // checking link between the last leaf and holder record 230 | val linkCorrect = SELF.R4[AvlTree].get.digest == holderTreeHashDigest && holderReserveId == lastLeafHolderId 231 | 232 | sigmaProp(lastLeafProperSignature && properProof && holderProperSignature && linkCorrect) 233 | } else if (action == -6) { 234 | // double spend 235 | // double spend should be checked twice: 236 | sigmaProp(false) 237 | } else if (action == -7) { 238 | // negative position in the tree - collateral seized 239 | val position = getVar[Long](1).get 240 | 241 | val g: GroupElement = groupGenerator 242 | 243 | val posReserve = CONTEXT.dataInputs(1) 244 | 245 | // checking alt redeem leaf signature 246 | val posLeafTreeHash = getVar[Coll[Byte]](2).get 247 | val posLeafReserveId = getVar[Coll[Byte]](3).get 248 | val posLeafNoteValue = getVar[Long](4).get 249 | val posLeafHolderId = getVar[Coll[Byte]](5).get 250 | val posLeafLeafA = getVar[GroupElement](6).get 251 | val posLeafABytes = posLeafLeafA.getEncoded 252 | val posLeafZBytes = getVar[Coll[Byte]](7).get 253 | val posLeafProperFormat = posLeafTreeHash.size == 32 && posLeafReserveId.size == 32 && posLeafHolderId.size == 32 254 | val posLeafMessage = posLeafTreeHash ++ posLeafReserveId ++ longToByteArray(posLeafNoteValue) ++ posLeafHolderId 255 | val posLeafEInt = byteArrayToBigInt(blake2b256(posLeafMessage)) // weak Fiat-Shamir - todo: should be strong 256 | val posLeafZ = byteArrayToBigInt(posLeafZBytes) 257 | val posLeafReserveIdValid = posReserve.tokens(0)._1 == posLeafReserveId 258 | val posLeafReservePk = posReserve.R4[GroupElement].get 259 | val posLeafProperSignature = (g.exp(posLeafZ) == posLeafLeafA.multiply(posLeafReservePk.exp(posLeafEInt))) && posLeafProperFormat && posLeafReserveIdValid 260 | 261 | // checking last leaf tree proof 262 | val keyBytes = longToByteArray(position) 263 | val proof = getVar[Coll[Byte]](8).get 264 | val history = SELF.R4[AvlTree].get 265 | val properProof = history.get(keyBytes, proof).get == (posLeafABytes ++ posLeafZBytes) 266 | 267 | // collateral seized if position is negative and leaf with such position is known 268 | sigmaProp((position < 0) && posLeafProperSignature && properProof) 269 | } else { 270 | // no more actions supported 271 | sigmaProp(false) 272 | } 273 | } else { 274 | // redemption 275 | 276 | val g: GroupElement = groupGenerator 277 | 278 | // checking last leaf signature 279 | val lastLeafReserve = CONTEXT.dataInputs(1) 280 | val lastLeafTreeHashDigest = getVar[Coll[Byte]](1).get 281 | val lastLeafReserveId = getVar[Coll[Byte]](2).get 282 | val lastLeafNoteValue = getVar[Long](3).get 283 | val lastLeafHolderId = getVar[Coll[Byte]](4).get 284 | val lastLeafA = getVar[GroupElement](5).get 285 | val lastLeafABytes = lastLeafA.getEncoded 286 | val lastLeafZBytes = getVar[Coll[Byte]](6).get 287 | val lastLeafProperFormat = lastLeafTreeHashDigest.size == 32 && lastLeafReserveId.size == 32 && lastLeafHolderId.size == 32 288 | val lastLeafMessage = lastLeafTreeHashDigest ++ lastLeafReserveId ++ longToByteArray(lastLeafNoteValue) ++ lastLeafHolderId 289 | val lastLeafEInt = byteArrayToBigInt(blake2b256(lastLeafMessage)) // weak Fiat-Shamir - todo: should be strong 290 | val lastLeafZ = byteArrayToBigInt(lastLeafZBytes) 291 | val lastLeafReserveIdValid = lastLeafReserve.tokens(0)._1 == lastLeafReserveId 292 | val lastLeafReservePk = lastLeafReserve.R4[GroupElement].get 293 | val lastLeafProperSignature = (g.exp(lastLeafZ) == lastLeafA.multiply(lastLeafReservePk.exp(lastLeafEInt))) && lastLeafProperFormat && lastLeafReserveIdValid 294 | 295 | // checking last leaf tree proof 296 | val lastLeafPosition = SELF.R5[Long].get 297 | val keyBytes = longToByteArray(lastLeafPosition) 298 | val proof = getVar[Coll[Byte]](7).get 299 | val history = SELF.R4[AvlTree].get 300 | val properProof = history.get(keyBytes, proof).get == (lastLeafABytes ++ lastLeafZBytes) 301 | 302 | // check holder pubkey 303 | val holderReserve = CONTEXT.dataInputs(2) 304 | val holderPk = holderReserve.R4[GroupElement].get 305 | val holderCorrect = holderReserve.tokens(0)._1 == lastLeafHolderId 306 | 307 | sigmaProp(lastLeafProperSignature && properProof && holderCorrect) && proveDlog(lastLeafReservePk) 308 | } 309 | } --------------------------------------------------------------------------------