diff --git a/ride4dapps/blockchain/README.md b/ride4dapps/blockchain/README.md
new file mode 100644
index 0000000..a4798d4
--- /dev/null
+++ b/ride4dapps/blockchain/README.md
@@ -0,0 +1,43 @@
+**Rudechain** - Proof-of-Work блокчейн в виде dApp. Валюта - **Rude**.
+Любая транзакция - это скрипт на специальном языке *RUDE*. Типов транзакций нет - поведение определяется скриптом.
+
+**RUDE** - простой интерпретируемый язык. Сейчас поддерживает две операции:
+1. `SEND recipient amount` - отправка _руды_ на другой аккаунт Rudechain-а.
+2. `SWAP [IN/OUT] amount` - ввод _руды_ с Waves-аккаунта в его Rude-аккаунт и, наоборот, вывод _руды_ в соответствующий ассет блокчейна Waves.
+
+В дальнейшем можно добавить в интерпретатор поддержку и других операций.
+
+Клиент доступен по ссылке: http://rudechain.mak.im/ (или можно локально запустить из Docker образа) \
+Для авторизации и регистрации **переключите Waves Keeper в режим Testnet!**
+
+Кратко о том, как это работает:
+1. для работы с Rudechain нужно выполнить регистрацию (стоит 5 Waves, чтобы не случилось спама учеток).
+2. любая операция с блокчейном - через отправку InvokeScript транзакции.
+3. чтобы смайнить новый блок, нужно перебрать `nonce` (тип Long) так, чтобы получившийся хеш нового блока начинался с нескольких (**difficulty**) нулевых байт.
+4. каждый блок ссылается на высоту блокчейна Waves. На одной высоте можно смайнить только один блок.
+5. каждая транзакция представляет из себя **RUDE** скрипт, исполняемый майнером. В зависимости от сложности скрипта транзакции взимается газ.
+6. при отправке транзакции пользователь указывает, сколько газа готов заплатить. При попадании в блок списывается только фактически потраченный газ.
+7. после отправки транзакция валидируется и кладется в **UTX**. При валидации проверяется баланс и корректность скрипта. Средства для транзакции резервируются до её попадания в блок.
+8. текущий майнер может обрабатывать **UTX**, отправляя InvokeScript на каждую неподтвержденную транзакцию. При обработке скрипт прогоняется через интерпретатор **RUDE**, а результат исполнения фиксируется как разультат InvokeScript-а.
+9. если смайнен новый блок, то майнер нового блока получает награду - _руду_. Старый блок фиксируется, а его автор получает половину газа, затраченного на транзакции в нем.
+10. если блоки майнятся часто - **difficulty** растет, реже - **difficulty** падает.
+
+Roadmap по дальнейшим улучшениям:
+
+Майнер
+* сейчас есть баг, что неправильно считаются нулевые байты. В ближайшее время поправлю
+* автоматизировать поведение майнера. Сейчас майнер не работает нонстоп, а разово выполняет попытку смайнить блок или обработать одну транзакцию из UTX (если аккаунт - майнер текущего блока)
+
+Клиент
+* выводить подробную информацию о выбранном блоке, в т.ч. список его транзакций
+* выводить информацию о выбранном аккаунте
+
+Язык RUDE
+* поддержка последовательности команд в одном скрипте
+* расширить набор команд
+* возможно, сделать язык стековым
+
+Блокчейн
+* наверно стоит пересмотреть саму идею, сделать еще чуть серьезнее. Например, сейчас у транзакций нет id - они просто пакуются в блок
+* выровнять расчет сложности
+* возможно, заменить пазл (что-нибудь интереснее, чем считать первые нулевые байты)
\ No newline at end of file
diff --git a/ride4dapps/blockchain/client/Dockerfile b/ride4dapps/blockchain/client/Dockerfile
new file mode 100644
index 0000000..903d156
--- /dev/null
+++ b/ride4dapps/blockchain/client/Dockerfile
@@ -0,0 +1,11 @@
+FROM node:slim
+
+RUN npm install -g node-static
+
+WORKDIR /static
+COPY index.html .
+COPY favicon.ico .
+
+EXPOSE 80
+CMD static --host-address 0.0.0.0 -p 80 --gzip
+
diff --git a/ride4dapps/blockchain/client/favicon.ico b/ride4dapps/blockchain/client/favicon.ico
new file mode 100644
index 0000000..11146b8
Binary files /dev/null and b/ride4dapps/blockchain/client/favicon.ico differ
diff --git a/ride4dapps/blockchain/client/index.html b/ride4dapps/blockchain/client/index.html
new file mode 100644
index 0000000..b29415d
--- /dev/null
+++ b/ride4dapps/blockchain/client/index.html
@@ -0,0 +1,363 @@
+
+
+
+
+
+
+
+ Rudechain
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RUDECHAIN
+
+ General Info
+
+ Height: {{height}}
+ Waves height: {{wavesHeight}}
+ Difficulty: {{base}}
+
+
+
+
+
+
+
+
+ ACCOUNT
+
+ Account Info
+
+ Name: {{user.name}}
+ Total balance: {{user.totalBalance}}
+ Available balance: {{user.availableBalance}}
+ Asset balance in Waves: {{user.assetBalance}}
+ Registered at height: {{user.registrationHeight}}
+
+
+ Create Transaction
+
+
+
+
+
+
+
+
+ Sign and send
+
+
+
+
+ Please register to be able to mine blocks and send transactions
+
+
+
+
+
+ Register
+
+
+
+
+ You're not logged in
+ Auth
+
+
+
+
+
+
+
+
+ UTX ({{ utxSize }})
+
+
+ {{ tx.script }}
+ Sender: {{ tx.sender }}
+
+
+ {{ tx.gas }} gas
+
+
+
+
+
+
+
+ Blocks (last {{blocksLimit}})
+
+
+
+ Block {{block.height}} is mined by {{block.miner}}
+ with {{block.txs.length}} txs and {{block.gas}} gas
+
+ hash: {{ block.hash }}
+
+
+ {{ block.refHeight }}
+ {{ block.timestamp }}
+
+
+
+
+
+
+ Github
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ride4dapps/blockchain/dapp.ride b/ride4dapps/blockchain/dapp.ride
new file mode 100644
index 0000000..e7321aa
--- /dev/null
+++ b/ride4dapps/blockchain/dapp.ride
@@ -0,0 +1,413 @@
+{-# STDLIB_VERSION 3 #-}
+{-# CONTENT_TYPE DAPP #-}
+{-# SCRIPT_TYPE ACCOUNT #-}
+
+# SPECIFY BEFORE SETTING THE CONTRACT!
+let registrationCost = 5
+let initBalance = 5
+let blockMinerReward = 10
+let utxLimit = 100
+
+let keyAccountPrefix = "@"
+let keyAddressPrefix = "$"
+let keyAssetId = "assetId"
+let keyHeight = "height"
+let keyLast = "last"
+let keyUtx = "utx"
+let keyUtxSize = "utx-size"
+
+func h() = getIntegerValue(this, keyHeight)
+func assetId() = this.getStringValue(keyAssetId).fromBase58String()
+
+# hash, timestamp, refHeight, minerAccount, nonce, prevHash, difficulty, gas
+func blockInfo(h: Int) = this.getStringValue(if h == -1 || h == h() then keyLast else h.toString())
+func blockHash(h: Int) = {
+ let blInfo = blockInfo(h)
+ let right = blInfo.indexOf(",").extract()
+ blInfo.take(right)
+}
+func blockTimestamp(h: Int) = {
+ let blInfo = blockInfo(h)
+ let left = blInfo.indexOf(",").extract() + 1
+ let right = blInfo.indexOf(",", left).extract()
+ blInfo.drop(left).take(right - left)
+}
+func blockReferenceHeight(h: Int) = {
+ let blInfo = blockInfo(h)
+ let left = blInfo.indexOf(",", blInfo.indexOf(",").extract() + 1).extract() + 1
+ let right = blInfo.indexOf(",", left).extract()
+ blInfo.drop(left).take(right - left).parseIntValue()
+}
+func blockMinerAccount(h: Int) = {
+ let blInfo = blockInfo(h)
+ let left = blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",").extract() + 1).extract() + 1).extract() + 1
+ let right = blInfo.indexOf(",", left).extract()
+ blInfo.drop(left).take(right - left)
+}
+func blockNonce(h: Int) = {
+ let blInfo = blockInfo(h)
+ let left = blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",").extract() + 1).extract() + 1).extract() + 1).extract() + 1
+ let right = blInfo.indexOf(",", left).extract()
+ blInfo.drop(left).take(right - left)
+}
+func blockPrevHash(h: Int) = {
+ let blInfo = blockInfo(h)
+ let left = blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",").extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ let right = blInfo.indexOf(",", left).extract()
+ blInfo.drop(left).take(right - left)
+}
+func blockDifficulty(h: Int) = {
+ let blInfo = blockInfo(h)
+ let left = blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",",
+ blInfo.indexOf(",").extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ ).extract() + 1
+ let right = blInfo.indexOf(",", left).extract()
+ blInfo.drop(left).take(right - left).parseIntValue()
+}
+func blockGasReward(h: Int) = {
+ let blHeaders = blockInfo(h).split(";")[0].split(",")
+ blHeaders[7].parseIntValue()
+}
+func blockTxs(h: Int) = {
+ let blInfo = blockInfo(h)
+ let semicolon = blInfo.indexOf(";")
+ if isDefined(semicolon) then
+ blInfo.drop(semicolon.extract() + 1)
+ else ""
+}
+
+func address(addr: Address) = addr.bytes.toBase58String()
+func isRegistered(addr: Address) = isDefined(this.getString(keyAddressPrefix + address(addr)))
+func isTaken(name: String) = isDefined(this.getString(keyAccountPrefix + name))
+func accountOf(addr: Address) = this.getStringValue(keyAddressPrefix + address(addr))
+func accountInfo(name: String) = this.getStringValue(keyAccountPrefix + name)
+
+func addressOf(account: String) = {
+ let accInfo = accountInfo(account)
+ let right = accInfo.indexOf(",").extract()
+ take(accInfo, right)
+}
+func regHeightOf(account: String) = {
+ let accInfo = accountInfo(account)
+ let left = accInfo.indexOf(",").extract() + 1
+ let right = accInfo.indexOf(",", left).extract()
+ accInfo.drop(left).take(right - left).parseIntValue()
+}
+func totalBalanceOf(account: String) = {
+ let accInfo = accountInfo(account)
+ let left = accInfo.indexOf(",", accInfo.indexOf(",").extract() + 1).extract() + 1
+ let right = accInfo.indexOf(",", left).extract()
+ accInfo.drop(left).take(right - left).parseIntValue()
+}
+func availableBalanceOf(account: String) = {
+ let accInfo = accountInfo(account)
+ let left = accInfo.indexOf(",", accInfo.indexOf(",", accInfo.indexOf(",").extract() + 1).extract() + 1).extract() + 1
+ let right = accInfo.size()
+ accInfo.drop(left).take(right - left).parseIntValue()
+}
+
+func estimate(script: String) = {
+ let words = script.split(" ")
+
+ if words[0] == "SEND" then
+ let gasRequired = 1
+ let amount = words[2].parseIntValue()
+ [gasRequired, amount]
+ else if words[0] == "SWAP" then
+ let gasRequired = 2
+ let direction = words[1].split(" ")[1]
+ let amount = if direction == "IN" then 0 else words[2].parseIntValue()
+ [gasRequired, amount]
+ else [1000000, 0] # unreachable state because validated
+}
+
+func evaluate(script: String, inv: Invocation) = {
+ let words = script.split(" ")
+ func send(recipient: String, amount: Int) = {
+ ScriptResult(
+ WriteSet([
+ DataEntry(keyAccountPrefix + recipient, addressOf(recipient) + "," + regHeightOf(recipient).toString() + ","
+ + (totalBalanceOf(recipient) + amount).toString() + "," + (availableBalanceOf(recipient) + amount).toString())
+ ]),
+ TransferSet([])
+ )
+ }
+ func swap(acc: String, direction: String, amount: Int) = {
+ if direction == "IN" then
+ ScriptResult(
+ WriteSet([
+ DataEntry(keyAccountPrefix + acc, addressOf(acc) + "," + regHeightOf(acc).toString() + ","
+ + (totalBalanceOf(acc) + amount).toString() + "," + (availableBalanceOf(acc) + amount).toString())
+ ]),
+ TransferSet([])
+ )
+ else
+ ScriptResult(
+ WriteSet([
+ DataEntry(keyAccountPrefix + acc, addressOf(acc) + "," + regHeightOf(acc).toString() + ","
+ + (totalBalanceOf(acc) - amount).toString() + "," + availableBalanceOf(acc).toString())
+ ]),
+ TransferSet([
+ ScriptTransfer(inv.caller, amount, assetId())
+ ])
+ )
+ }
+
+ if words[0] == "SEND" then
+ send(words[1].split(" ")[1], words[2].parseIntValue())
+ else if words[0] == "SWAP" then
+ swap(accountOf(inv.caller), words[1].split(" ")[1], words[2].parseIntValue())
+ else throw("can't evaluate script") # unreachable state because validated
+}
+
+func validate(acc: String, gas: Int, script: String, inv: Invocation, checkBalance: Boolean) = {
+ let words = script.split(" ")
+
+ if words[0] == "SEND" then
+ if words.size() != 3 then "Missed args: SEND recipient amount"
+ else
+ let gasRequired = estimate(script)[0]
+ let recipient = words[1].split(" ")[1]
+ let amount = words[2].parseIntValue()
+
+ if !isTaken(recipient) then "recipient '" + recipient + "' doesn't exist"
+ else if recipient == acc then "sender can't do SEND to itself"
+ else if amount < 1 then "amount " + amount.toString() + " must be a positive number"
+ else if !(gas > 0) then "Gas amount must be positive!"
+ else if gas < gasRequired then "Not enough gas: actual " + gas.toString() + " but " + gasRequired.toString() + " estimated"
+ else if checkBalance && availableBalanceOf(acc) < gas + amount then "Not enough available balance for payment and gas"
+ else ""
+ else if words[0] == "SWAP" then
+ if words.size() != 3 then "Missed args: SWAP [IN/OUT] amount"
+ else
+ let gasRequired = estimate(script)[0]
+ let direction = words[1].split(" ")[1]
+ let amount = words[2].parseIntValue()
+
+ if direction == "IN" then
+ if !isDefined(inv.payment) then "Payment is required for the transaction!"
+ else
+ let pmt = extract(inv.payment)
+ if pmt.amount != amount || pmt.assetId != assetId() then "Required payment is exactly " + amount.toString() + " of asset " + assetId().toBase58String()
+ else if !(gas > 0) then "Gas amount must be positive!"
+ else if gas < gasRequired then "Not enough gas: actual " + gas.toString() + " but " + gasRequired.toString() + " estimated"
+ else if checkBalance && availableBalanceOf(acc) < gas then "Not enough available balance for gas"
+ else ""
+ else if direction == "OUT" then
+ if !(gas > 0) then "Gas amount must be positive!"
+ else if gas < gasRequired then "Not enough gas: actual " + gas.toString() + " but " + gasRequired.toString() + " estimated"
+ else if checkBalance && availableBalanceOf(acc) < gas + amount then "Not enough available balance for SWAP and gas"
+ else ""
+ else "Argument \"direction\" must be \"IN\" or \"OUT\""
+ else
+ "unknown command " + words[0]
+}
+
+@Callable(i)
+func genesis(assetId: String) = {
+ let asset = assetInfo(assetId.fromBase58String())
+ if i.caller != this then
+ throw("Rudechain can be created only by the dApp")
+ else if isDefined(this.getString(keyLast))
+ || isDefined(this.getInteger(keyHeight))
+ || isDefined(this.getInteger(keyUtx))
+ || isDefined(this.getInteger(keyUtxSize))
+ then
+ throw("Rudechain is already created")
+ else if !isDefined(asset) then throw("Asset '" + assetId + "' doesn't exist!")
+ else if this.assetBalance(assetId.fromBase58String()) != asset.extract().totalAmount || asset.extract().decimals != 0 || asset.extract().issuer != this.bytes then
+ throw("Incorrect asset. It must be issued by the dApp with 0 decimals and the dApp must have the entire quantity")
+ else
+ let minerName = "dapp"
+ let gHeight = 0
+ # hash, timestamp, refHeight, minerAccount, nonce, prevHash, difficulty, gas
+ let hash = (lastBlock.timestamp.toString() + lastBlock.height.toString() + minerName + "0").toBytes().blake2b256().toBase58String()
+ let genesisBlock = hash + "," + lastBlock.timestamp.toString() + "," + lastBlock.height.toString() + "," + minerName + ",0,0,1,0"
+ WriteSet([
+ DataEntry(keyAssetId, assetId),
+ DataEntry(keyLast, genesisBlock),
+ DataEntry(keyHeight, gHeight),
+ DataEntry(keyUtx, ""),
+ DataEntry(keyUtxSize, 0),
+ DataEntry(keyAddressPrefix + address(this), "dapp"),
+ DataEntry(keyAccountPrefix + "dapp", address(this) + "," + gHeight.toString() + ",0,0")
+ ])
+}
+
+@Callable(i)
+func register(name: String) = {
+ let validChars = "abcdefghijklmnopqrstuvwxyz0123456789"
+
+ if (!isDefined(i.payment) || isDefined(i.payment.extract().assetId) || i.payment.extract().amount != registrationCost * 100000000) then
+ throw("Registration costs " + registrationCost.toString() + " Waves!")
+ else if !(name.size() > 1 && name.size() <= 8
+ && isDefined(validChars.indexOf(name.take(1)))
+ && isDefined(validChars.indexOf(name.drop(1).take(1)))
+ && (if (name.size() > 2) then isDefined(validChars.indexOf(name.drop(2).take(1))) else true)
+ && (if (name.size() > 3) then isDefined(validChars.indexOf(name.drop(3).take(1))) else true)
+ && (if (name.size() > 4) then isDefined(validChars.indexOf(name.drop(4).take(1))) else true)
+ && (if (name.size() > 5) then isDefined(validChars.indexOf(name.drop(5).take(1))) else true)
+ && (if (name.size() > 6) then isDefined(validChars.indexOf(name.drop(6).take(1))) else true)
+ && (if (name.size() > 7) then isDefined(validChars.indexOf(name.drop(7).take(1))) else true))
+ then
+ throw("Account name must have [2..8] length and contain only [a-z0-9] chars")
+ else if isRegistered(i.caller) then
+ throw("Address of the caller is already registered as '" + accountOf(i.caller) + "'")
+ else if isTaken(name) then
+ throw("Account name '" + name + "' is already taken")
+ else
+ WriteSet([
+ DataEntry(keyAddressPrefix + address(i.caller), name),
+ DataEntry(keyAccountPrefix + name, address(i.caller) + "," + h().toString() + "," + initBalance.toString() + "," + initBalance.toString())
+ ])
+}
+
+@Callable(i)
+func mine(nonce: Int) = {
+ let delta = lastBlock.height - blockReferenceHeight(-1)
+ let difficulty = blockDifficulty(-1)
+ let newDifficulty = if delta == 1 then difficulty + 3 else if delta == 2 || delta == 3 then difficulty + 1 else if difficulty - (delta / 2) > 0 then difficulty - (delta / 2) else 1
+
+ let hash = blake2b256((
+ lastBlock.timestamp.toString()
+ + lastBlock.height.toString()
+ + accountOf(i.caller)
+ + nonce.toString()
+ + blockPrevHash(-1)
+ ).toBytes())
+
+ let byte0LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(1).toBase58String())
+ let byte1LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(2).takeRight(1).toBase58String())
+ let byte2LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(3).takeRight(1).toBase58String())
+ let byte3LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(4).takeRight(1).toBase58String())
+ let byte4LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(5).takeRight(1).toBase58String())
+ let byte5LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(6).takeRight(1).toBase58String())
+ let byte6LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(7).takeRight(1).toBase58String())
+ let byte7LeadingZeros = Alias("zerobytes").getIntegerValue(hash.take(8).takeRight(1).toBase58String())
+
+ let firstZeroBits = if byte0LeadingZeros != 8 then byte0LeadingZeros else ( 8 +
+ if byte1LeadingZeros != 8 then byte1LeadingZeros else ( 8 +
+ if byte2LeadingZeros != 8 then byte2LeadingZeros else ( 8 +
+ if byte3LeadingZeros != 8 then byte3LeadingZeros else ( 8 +
+ if byte4LeadingZeros != 8 then byte4LeadingZeros else ( 8 +
+ if byte5LeadingZeros != 8 then byte5LeadingZeros else ( 8 +
+ if byte6LeadingZeros != 8 then byte6LeadingZeros else ( 8 +
+ byte7LeadingZeros)))))))
+
+ if i.caller == this then throw("The dApp can't mine!")
+ else if !isRegistered(i.caller) then throw("Miner must be registered!")
+ else if delta == 0 then throw("Can't mine on same reference height as last block: " + lastBlock.height.toString())
+ else if firstZeroBits < newDifficulty then throw("Hash has difficulty " + firstZeroBits.toString() + ", but at least " + newDifficulty.toString() + " is required")
+ else
+ let prevMinerAccount = blockMinerAccount(-1)
+ let newMinerAccount = accountOf(i.caller)
+ let newHeight = h() + 1
+ let newBlock = hash.toBase58String()
+ + "," + lastBlock.timestamp.toString()
+ + "," + lastBlock.height.toString()
+ + "," + newMinerAccount
+ + "," + nonce.toString()
+ + "," + blockPrevHash(-1)
+ + "," + newDifficulty.toString()
+ + ",0"
+
+ WriteSet([
+ DataEntry(keyHeight, newHeight),
+ DataEntry(keyAccountPrefix + prevMinerAccount, addressOf(prevMinerAccount) + "," + regHeightOf(prevMinerAccount).toString() + ","
+ + (totalBalanceOf(prevMinerAccount) + blockGasReward(-1)).toString() + "," + (availableBalanceOf(prevMinerAccount) + blockGasReward(-1)).toString()),
+ DataEntry(h().toString(), blockInfo(-1)),
+ DataEntry(keyAccountPrefix + newMinerAccount, addressOf(newMinerAccount) + "," + regHeightOf(newMinerAccount).toString() + ","
+ + (totalBalanceOf(newMinerAccount) + blockMinerReward).toString() + "," + (availableBalanceOf(newMinerAccount) + blockMinerReward).toString()),
+ DataEntry(keyLast, newBlock)
+ ])
+}
+
+@Callable(i)
+func utxProcessing() = {
+ let utx = this.getStringValue(keyUtx)
+ let utxSize = this.getIntegerValue(keyUtxSize)
+
+ if i.caller.bytes != addressOf(blockMinerAccount(-1)).fromBase58String() then throw("Only the current miner can processing UTX!")
+ else if utxSize == 0 then WriteSet([])
+ else
+ let tx = utx.split(";")[0]
+ let txFields = tx.split(",")
+
+ let txSenderAccount = txFields[0]
+ let txGas = txFields[1].split(",")[1].parseIntValue()
+ let txScript = txFields[2]
+
+ let txSender = addressOf(txSenderAccount)
+ let validation = validate(txSender, txGas, txScript, i, false)
+ let costs = estimate(txScript)
+
+ if validation.size() > 0 then
+ WriteSet([DataEntry(keyUtxSize, utxSize - 1), DataEntry(keyUtx, utx.drop(tx.size() + 1))])
+ else
+ let increasedReward = blockGasReward(-1) + costs[0]
+ let txs = if isDefined(blockInfo(-1).indexOf(";")) then ";" + blockTxs(-1) else ""
+ let result = evaluate(txScript, i)
+
+ ScriptResult(
+ WriteSet(
+ DataEntry(keyLast, blockHash(-1) + "," + blockTimestamp(-1) + "," + blockReferenceHeight(-1).toString() + "," + blockMinerAccount(-1) + ","
+ + blockNonce(-1) + "," + blockPrevHash(-1) + "," + blockDifficulty(-1).toString() + "," + increasedReward.toString() + txs + ";" + tx)
+ ::DataEntry(keyUtxSize, utxSize - 1)
+ ::DataEntry(keyUtx, utx.drop(tx.size() + 1))
+ ::DataEntry( keyAccountPrefix + txSenderAccount, txSender + "," + regHeightOf(txSenderAccount).toString() + ","
+ + (totalBalanceOf(txSenderAccount) - costs[0] - costs[1]).toString() + ","
+ + (availableBalanceOf(txSenderAccount) - costs[0] + txGas).toString() )
+ ::result.data.data
+ ),
+ TransferSet(result.transfers.transfers)
+ )
+}
+
+@Callable(i)
+func transaction(gas: Int, script: String) = {
+ if i.caller == this then throw("The Rudechain dApp can't send transactions!")
+ else if !isRegistered(i.caller) then throw("Only registered accounts can send transactions!")
+ else if this.getIntegerValue(keyUtxSize) == utxLimit then throw("UTX size limit reached! Please try later")
+ else
+ let sender = accountOf(i.caller)
+ let txBody = sender + "," + gas.toString() + "," + script
+ let validation = validate(sender, gas, script, i, true)
+ let costs = estimate(script)
+ let reserved = costs[0] + costs[1]
+
+ if validation.size() > 0 then throw(validation)
+ else
+ let utxPool = this.getStringValue(keyUtx)
+ let utxSize = this.getIntegerValue(keyUtxSize)
+ let newUtxPool = if (utxSize > 0) then utxPool + ";" + txBody else txBody
+ WriteSet([
+ DataEntry(keyUtx, newUtxPool),
+ DataEntry(keyUtxSize, utxSize + 1),
+ DataEntry(keyAccountPrefix + sender, addressOf(sender) + "," + regHeightOf(sender).toString() + "," + totalBalanceOf(sender).toString() + ","
+ + (availableBalanceOf(sender) - reserved).toString())
+ ])
+}
+
+@Verifier(tx)
+func verify() = {
+ match tx {
+ case d:DataTransaction => false # rudechain can be changed only via dApp actions
+ case _ => tx.bodyBytes.sigVerify(tx.proofs[0], tx.senderPublicKey)
+ }
+}
diff --git a/ride4dapps/blockchain/dapp_test.js b/ride4dapps/blockchain/dapp_test.js
new file mode 100644
index 0000000..6f42d59
--- /dev/null
+++ b/ride4dapps/blockchain/dapp_test.js
@@ -0,0 +1,31 @@
+const pubKey = '' // TODO SET!
+
+describe('Blockchain tests', () => {
+
+ it('genesis', async function(){
+ const b = await balance();
+ const h = await currentHeight()
+
+ const invS = invokeScript({ dApp: address(env.accounts[1]), call: {function: "genesis", args: []} })
+ await broadcast(invS)
+ })
+
+ it('register', async function(){
+ const name = "bob1"
+ const invS = invokeScript({ dApp: address(env.accounts[1]), call: {function: "register", args: [{type: "string", value: name}]}, payment: [{amount: 500000000, assetId: null}] })
+ await broadcast(invS)
+ })
+
+ it('send tx', async function(){
+ const script64 = Base64.encode("SEND alice 2")
+ const gaz = 1
+ const sig = sign(pubKey + gaz + script64)
+ const invS = invokeScript({ dApp: address(env.accounts[1]), call: {function: "transaction", args: [
+ {type: "string", value: sig},
+ {type: "integer", value: gaz},
+ {type: "string", value: script64}
+ ]} })
+ await broadcast(invS)
+ })
+
+})
\ No newline at end of file
diff --git a/ride4dapps/blockchain/miner/.gitignore b/ride4dapps/blockchain/miner/.gitignore
new file mode 100644
index 0000000..5eba97c
--- /dev/null
+++ b/ride4dapps/blockchain/miner/.gitignore
@@ -0,0 +1,15 @@
+.idea/
+*.iml
+*.ipr
+*.iws
+
+.settings/
+.classpath
+.project
+
+out/
+target/
+*.jar
+
+seed.conf
+
diff --git a/ride4dapps/blockchain/miner/pom.xml b/ride4dapps/blockchain/miner/pom.xml
new file mode 100644
index 0000000..6c1fbed
--- /dev/null
+++ b/ride4dapps/blockchain/miner/pom.xml
@@ -0,0 +1,62 @@
+
+
+ 4.0.0
+
+ com.example
+ miner
+ 0.1
+
+
+ ${project.basedir}/src
+
+
+ ${project.basedir}/resources
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.0
+
+ 8
+ 8
+
+
+
+ maven-assembly-plugin
+ 3.1.1
+
+
+ jar-with-dependencies
+
+
+
+ Main
+
+
+
+
+
+ pack-all
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ com.wavesplatform
+ wavesj
+ 0.14.1
+
+
+
+
diff --git a/ride4dapps/blockchain/miner/src/Main.java b/ride4dapps/blockchain/miner/src/Main.java
new file mode 100644
index 0000000..107d0b5
--- /dev/null
+++ b/ride4dapps/blockchain/miner/src/Main.java
@@ -0,0 +1,24 @@
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static java.util.Arrays.asList;
+
+public class Main {
+
+ public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException {
+ RudechainMiner rc = new RudechainMiner(
+ Files.readAllLines(Paths.get("seed.conf")).get(0)
+ );
+
+ if (args.length == 0 || !asList("mine", "utx").contains(args[0]))
+ System.err.println("Provide command 'mine' or 'utx'!");
+ else if ("mine".equals(args[0])) {
+ rc.tryToMineOnce();
+ } else if ("utx".equals(args[0])) {
+ rc.utxProcessing();
+ }
+ }
+
+}
diff --git a/ride4dapps/blockchain/miner/src/RudechainMiner.java b/ride4dapps/blockchain/miner/src/RudechainMiner.java
new file mode 100644
index 0000000..9453d08
--- /dev/null
+++ b/ride4dapps/blockchain/miner/src/RudechainMiner.java
@@ -0,0 +1,159 @@
+import com.wavesplatform.wavesj.*;
+import com.wavesplatform.wavesj.json.WavesJsonMapper;
+import com.wavesplatform.wavesj.transactions.InvokeScriptTransaction;
+import com.wavesplatform.wavesj.transactions.InvokeScriptTransaction.FunctionalArg;
+import com.wavesplatform.wavesj.transactions.InvokeScriptTransaction.FunctionCall;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.*;
+
+import static com.wavesplatform.wavesj.Hash.*;
+import static java.lang.Integer.parseInt;
+import static java.lang.System.currentTimeMillis;
+import static java.time.format.DateTimeFormatter.ofPattern;
+import static java.util.Arrays.asList;
+import static java.util.Map.Entry.comparingByKey;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.*;
+import static java.util.Map.Entry.comparingByValue;
+
+public class RudechainMiner {
+
+ Node node;
+ PrivateKeyAccount miner;
+ String minerAccount;
+ String rudechain;
+
+ public RudechainMiner(String seedText) throws URISyntaxException, IOException {
+ node = new Node("https://testnode4.wavesnodes.com", 'T'); //TODO host to conf file
+ miner = PrivateKeyAccount.fromSeed(seedText, 0, (byte) 'T'); //TODO chainId to conf file
+ rudechain = "3MwgRB2FJzbquEZEnySc2jwhpaZnoWvCMJX"; //TODO to conf file
+ System.out.println(miner.getAddress());
+ minerAccount = node.getDataByKey(
+ rudechain, "$" + miner.getAddress()).getValue().toString();
+ }
+
+ void tryToMineOnce() throws IOException, InterruptedException {
+ log("Preparing...");
+
+ long nonce = 0, to = Long.MIN_VALUE;
+
+ int difficulty = parseInt(node.getDataByKey(rudechain, "last").getValue().toString().split(",")[6]);
+ BlockHeader refLast = node.getLastBlockHeader();
+ String[] blockHeaders;
+ while (true) {
+ int wavesHeight = node.getHeight();
+ blockHeaders = node.getDataByKey(rudechain, "last")
+ .getValue().toString().split(";")[0].split(",");
+ int refHeight = parseInt(blockHeaders[2]);
+ int delta = wavesHeight - refHeight;
+
+ if (refHeight == wavesHeight) {
+ Thread.sleep(1000);
+ continue;
+ } else if (asList(2, 3).contains(delta) || difficulty == 1) {
+ difficulty += 1;
+ break;
+ } else if (delta == 1) {
+ difficulty += 3;
+ break;
+ } else if (difficulty - (delta / 2) > 0) {
+ difficulty -= delta / 2;
+ break;
+ } else {
+ difficulty = 1;
+ }
+ }
+ //TODO if I'm a new miner - spy and process utx until height arise. Else mine again
+ //TODO When height is up - mine again
+ log("Start mining with difficulty " + difficulty);
+ String hash;
+ do {
+ nonce--;
+
+ //timestamp, refHeight, minerAccountName, nonce, prevHash (of current last block)
+ String source = refLast.getTimestamp() + refLast.getHeight() + minerAccount + nonce + blockHeaders[0];
+ hash = Base58.encode(
+ Hash.hash(source.getBytes(), 0, source.getBytes().length, BLAKE2B256)
+ );
+ byte[] hashBytes = (hash + source).getBytes();
+
+ short zeros = 0;
+ for (byte b : hashBytes) {
+ if (b == 0) {
+ zeros += 8;
+ } else {
+ zeros += 7 - (int) Math.floor(mathLog2(b)); //TODO пропадают 8, 16, 24, 32
+ break;
+ }
+ }
+ if (zeros < difficulty) {
+ continue;
+ }
+
+ log("generated '" + hash + "' with first " + zeros + " zero bytes from source '" + source + "'");
+ break;
+ } while (nonce > to);
+
+ String id = sendKeyBlock(nonce);
+ log(id);
+
+ boolean isTxInBlockchain = waitForTx(id);
+
+ if (!isTxInBlockchain) {
+ throw new IOException("can't mine key block: tx '" + id + "' is not in blockchain");
+ } else log("you're the miner now!");
+
+ }
+
+ String sendKeyBlock(long nonce) throws IOException {
+ InvokeScriptTransaction tx = new InvokeScriptTransaction((byte) 'T', miner, rudechain,
+ new FunctionCall("mine").addArg(nonce),
+ new ArrayList<>(), 500000, null, System.currentTimeMillis(), new ArrayList<>()
+ ).sign(miner);
+ return node.send(tx);
+ }
+
+ void utxProcessing() throws IOException, InterruptedException {
+ String utx = node.getDataByKey(rudechain, "utx").getValue().toString();
+ if (utx.length() < 2) {
+ log("UTX is empty now");
+ } else {
+ InvokeScriptTransaction tx = new InvokeScriptTransaction((byte) 'T', miner, rudechain,
+ "utxProcessing", 500000, null, System.currentTimeMillis()
+ ).sign(miner);
+ String id = node.send(tx);
+ boolean isTxInBlockchain = waitForTx(id);
+ log(String.valueOf(isTxInBlockchain));
+ }
+ }
+
+ private boolean waitForTx(String id) throws InterruptedException {
+ boolean isTxInBlockchain = false;
+ for (int attempt = 0; attempt < 120; attempt++) {
+ try {
+ node.getTransaction(id);
+ isTxInBlockchain = true;
+ break;
+ } catch (IOException e) {
+ Thread.sleep(1000);
+ }
+ }
+ return isTxInBlockchain;
+ }
+
+ double mathLog2(int num) {
+ return Math.log10(num) / Math.log10(2);
+ }
+
+ private void log(String message) {
+ String time = LocalDateTime.now().format(ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
+ System.out.print(time + "> " + message + "\n");
+ }
+
+}
\ No newline at end of file