From bb0ca693668ffe2a6b20f777ac50fbbdbc8b7b22 Mon Sep 17 00:00:00 2001 From: Maxim Smolyakov Date: Sun, 12 May 2019 23:57:56 +0300 Subject: [PATCH 1/6] ride4dapps blockchain example --- ride4dapps/blockchain/blockchain.ride | 270 +++++++++++++++++++++++ ride4dapps/blockchain/blockchain_test.js | 31 +++ 2 files changed, 301 insertions(+) create mode 100644 ride4dapps/blockchain/blockchain.ride create mode 100644 ride4dapps/blockchain/blockchain_test.js diff --git a/ride4dapps/blockchain/blockchain.ride b/ride4dapps/blockchain/blockchain.ride new file mode 100644 index 0000000..969d3e2 --- /dev/null +++ b/ride4dapps/blockchain/blockchain.ride @@ -0,0 +1,270 @@ +{-# STDLIB_VERSION 3 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +let keyAccountPrefix = "@" +let keyPubKeyPrefix = "$" +let keyBase = "base" +let keyHeight = "height" +let keyLast = "last" +let keyUtx = "utx" +let keyUtxSize = "utx-size" + +let dappPublicKey = base58'' # TODO SET! + +let registrationCost = 5 +let utxLimit = 100 +let blockMinerReward = 20 +let initBalance = 5 + +func h() = getIntegerValue(this, keyHeight) +func base() = getIntegerValue(this, keyBase) + +func blockInfo(h: Int) = getStringValue(this, if h == -1 || h == h() then keyLast else h.toString()) +func blockReferenceHeight(h: Int) = { + let blInfo = blockInfo(h) + let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1 + let right = extract(indexOf(blInfo, ",", left)) + take(drop(blInfo, left), right - left).parseIntValue() +} +func blockMinerPublicKey(h: Int) = { + let blInfo = blockInfo(h) + let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1)) + 1 + let right = extract(indexOf(blInfo, ",", left)) + take(drop(blInfo, left), right - left) +} +func blockPrevHash(h: Int) = { + let blInfo = blockInfo(h) + let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1)) + 1)) + 1 + let right = extract(indexOf(blInfo, ",", left)) + take(drop(blInfo, left), right - left) +} +func blockGasReward(h: Int) = { + let blInfo = blockInfo(h) + let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1)) + 1)) + 1)) + 1 + let right = extract(indexOf(blInfo, ",", left)) + take(drop(blInfo, left), right - left).parseIntValue() +} +func blockTxs(h: Int) = { + let blInfo = blockInfo(h) + let semicolon = indexOf(blInfo, ";") + if isDefined(semicolon) then + drop(blInfo, extract(semicolon) + 1) + else "" +} + +func pubKey(pk: ByteVector) = pk.toBase58String() +func accountOf(pk: ByteVector) = getString(this, keyPubKeyPrefix + pubKey(pk)) +func accountInfo(name: String) = getString(this, keyAccountPrefix + name) +func isRegistered(pK: ByteVector) = isDefined(accountOf(pK)) +func isTaken(acc: String) = isDefined(accountInfo(acc)) + +func pubKeyOf(account: String) = { + let accInfo = extract(accountInfo(account)) + let right = extract(indexOf(accInfo, ",")) + take(accInfo, right) +} +func regHeightOf(account: String) = { + let accInfo = extract(accountInfo(account)) + let left = extract(indexOf(accInfo, ",")) + 1 + let right = extract(indexOf(accInfo, ",", left)) + take(drop(accInfo, left), right - left).parseIntValue() +} +func totalBalanceOf(account: String) = { + let accInfo = extract(accountInfo(account)) + let left = extract(indexOf(accInfo, ",", extract(indexOf(accInfo, ",")) + 1)) + 1 + let right = extract(indexOf(accInfo, ",", left)) + take(drop(accInfo, left), right - left).parseIntValue() +} +func availableBalanceOf(account: String) = { + let accInfo = extract(accountInfo(account)) + let left = extract(indexOf(accInfo, ",", extract(indexOf(accInfo, ",", extract(indexOf(accInfo, ",")) + 1)) + 1)) + 1 + let right = accInfo.size() + take(drop(accInfo, left), 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 [1000000, 0] # unreachable state because validated +} + +func evaluate(script: String) = { + let words = script.split(" ") + func send(recipient: String, amount: Int) = { + DataEntry(keyAccountPrefix + recipient, pubKeyOf(recipient) + "," + regHeightOf(recipient).toString() + "," + + (totalBalanceOf(recipient) + amount).toString() + "," + (availableBalanceOf(recipient) + amount).toString()) + } + + if words[0] == "SEND" then + send(words[1], words[2].parseIntValue()) + else throw("can't evaluate script") # unreachable state because validated +} + +func validate(acc: String, gas: Int, script: String, checkBalance: Boolean) = { + let words = script.split(" ") + + if words[0] == "SEND" then + if words.size() != 3 then "SEND command expects args: recipient amount" + else + let gasRequired = estimate(script)[0] + let recipient = words[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 + "unknown command " + words[0] +} + +@Callable(i) +func genesis() = { + if i.caller != this then + throw("blockchain can be created only by the dApp") + else if isDefined(getString(this, keyLast)) + || isDefined(getInteger(this, keyHeight)) + || isDefined(getInteger(this, keyBase)) + || isDefined(getInteger(this, keyUtx)) + || isDefined(getInteger(this, keyUtxSize)) + then + throw("blockchain is already created") + else + let gHeight = 0 + let genesisBlock = "," + gHeight.toString() + "," + height.toString() + "," + toBase58String(i.callerPublicKey) + ",0,0" + WriteSet( + DataEntry(keyLast, genesisBlock) + ::DataEntry(keyHeight, gHeight) + ::DataEntry(keyBase, 1) + ::DataEntry(keyUtx, "") + ::DataEntry(keyUtxSize, 0) + ::nil + ) +} + +@Callable(i) +func register(name: String) = { + let callerPubKey = i.callerPublicKey.toBase58String() + let validChars = "abcdefghijklmnopqrstuvwxyz0123456789" + + if (!isDefined(i.payment) || isDefined(extract(i.payment).assetId) || extract(i.payment).amount != registrationCost * 100000000) then + throw("Registration costs " + registrationCost.toString() + " Waves!") + else if !(size(name) > 0 && size(name) <= 8 && isDefined(indexOf(validChars, take(name, 1))) + && (if (size(name) > 1) then isDefined(indexOf(validChars, take(drop(name, 1), 1))) else true) + && (if (size(name) > 2) then isDefined(indexOf(validChars, take(drop(name, 2), 1))) else true) + && (if (size(name) > 3) then isDefined(indexOf(validChars, take(drop(name, 3), 1))) else true) + && (if (size(name) > 4) then isDefined(indexOf(validChars, take(drop(name, 4), 1))) else true) + && (if (size(name) > 5) then isDefined(indexOf(validChars, take(drop(name, 5), 1))) else true) + && (if (size(name) > 6) then isDefined(indexOf(validChars, take(drop(name, 6), 1))) else true) + && (if (size(name) > 7) then isDefined(indexOf(validChars, take(drop(name, 7), 1))) else true)) + then + throw("Account name must have [1..8] length and contain only [a-z0-9] chars") + else if isRegistered(i.callerPublicKey) then + throw("Public key of the caller is already registered as '" + extract(accountOf(i.callerPublicKey)) + "'") + else if isTaken(name) then + throw("Account name '" + name + "' is already taken") + else + WriteSet([ + DataEntry(keyPubKeyPrefix + i.callerPublicKey.toBase58String(), name), + DataEntry(keyAccountPrefix + name, pubKey(i.callerPublicKey) + "," + h().toString() + "," + initBalance.toString() + "," + initBalance.toString()) + ]) +} + +@Callable(i) +func mine(nonce: String) = { + let delta = height - h() + + if i.caller == this then throw("The dApp can't mine!") + else if !isRegistered(i.callerPublicKey) then throw("Miner must be registered!") + else if delta == 0 then throw("Can't mine on same reference height as last block: " + height.toString()) + else + let minerAccount = extract(accountOf(i.callerPublicKey)) + let nextHeight = h() + 1 + let liquidBlock = h().toString() + blockReferenceHeight(-1).toString() + blockMinerPublicKey(-1) + blockPrevHash(-1) + blockGasReward(-1).toString() + blockTxs(-1) + let liquidBlockHash = sha256(liquidBlock.toBytes()).toBase58String() + + WriteSet([ + DataEntry(keyAccountPrefix + minerAccount, pubKeyOf(minerAccount) + "," + regHeightOf(minerAccount).toString() + "," + + (totalBalanceOf(minerAccount) + blockGasReward(-1)).toString() + "," + (availableBalanceOf(minerAccount) + blockGasReward(-1)).toString()) + , DataEntry(h().toString(), liquidBlockHash + blockInfo(-1)) + , DataEntry(keyHeight, nextHeight) + , DataEntry(keyBase, base() + 1) + , DataEntry(keyLast, "," + nextHeight.toString() + "," + height.toString() + "," + toBase58String(i.callerPublicKey) + "," + liquidBlockHash + ",0") + ]) +} + +@Callable(i) +func utxProcessing() = { + if i.callerPublicKey != blockMinerPublicKey(-1).fromBase58String() then throw("Only the current miner can processing UTX!") + else if getIntegerValue(this, keyUtxSize) == 0 then WriteSet([]) + else + let utx = getStringValue(this, keyUtx) + let utxSize = getIntegerValue(this, keyUtxSize) + let marker = extract(indexOf(utx, ";")) + let tx = take(utx, marker) + + let txSignature = take(tx, extract(indexOf(tx, ","))) + let txSenderPublicKey = { let raw = drop(tx, txSignature.size() + 1); take(raw, extract(indexOf(raw, ","))) } + let txGas = { let raw = drop(tx, txSignature.size() + txSenderPublicKey.size() + 2); take(raw, extract(indexOf(raw, ","))) } + let txScript = drop(tx, txSignature.size() + txSenderPublicKey.size() + txGas.size() + 3) + + let txSenderAcc = extract(accountOf(txSenderPublicKey.toBytes())) + let validation = validate(txSenderAcc, txGas.parseIntValue(), txScript, false) + let costs = estimate(txScript) + + if validation.size() > 0 then + WriteSet([DataEntry(keyUtxSize, utxSize - 1), DataEntry(keyUtx, drop(utx, marker + 1))]) + else + WriteSet( + DataEntry(keyLast, "," + h().toString() + "," + blockReferenceHeight(-1).toString() + "," + blockMinerPublicKey(-1) + "," + + blockPrevHash(-1) + "," + (blockGasReward(-1) + costs[0]).toString() + blockTxs(-1) + ";" + tx) + ::DataEntry(keyUtxSize, utxSize - 1) + ::DataEntry(keyUtx, drop(utx, marker + 1)) + ::evaluate(txScript) + ::DataEntry( keyAccountPrefix + txSenderAcc, txSenderPublicKey + "," + regHeightOf(txSenderAcc).toString() + "," + + (totalBalanceOf(txSenderAcc) - costs[0] - costs[1]).toString() + "," + + (availableBalanceOf(txSenderAcc) + txGas.parseIntValue() - costs[0]).toString() ) + ::nil + ) +} + +@Callable(i) +func transaction(signatureBase64: String, gas: Int, script: String) = { + if i.caller == this then throw("The dApp can't send transactions!") + else if !isRegistered(i.callerPublicKey) then throw("Only registered accounts can send transactions!") + else if !(getIntegerValue(this, keyUtxSize) < utxLimit) then throw("UTX size limit reached! Please try later") + else + let txBody = extract(accountOf(i.callerPublicKey)) + "," + gas.toString() + "," + script + let acc = extract(accountOf(i.callerPublicKey)) + let validation = validate(acc, gas, script, true) + let costs = estimate(script) + let reserved = costs[0] + costs[1] + + if validation.size() > 0 then throw(validation) + else if !sigVerify(txBody.toBytes(), signatureBase64.fromBase64String(), i.callerPublicKey) then throw("Incorrect signature!") + else + let utxPool = getStringValue(this, keyUtx) + WriteSet([ + DataEntry(keyUtx, utxPool + signatureBase64 + "," + txBody + ";"), + DataEntry(keyUtxSize, getIntegerValue(this, keyUtxSize) + 1), + DataEntry(keyAccountPrefix + acc, pubKeyOf(acc) + "," + regHeightOf(acc).toString() + "," + totalBalanceOf(acc).toString() + "," + + (availableBalanceOf(acc) - reserved).toString()) + ]) +} + +@Verifier(tx) +func verify() = { + match tx { + case d:DataTransaction => false + case t:TransferTransaction => isDefined(t.assetId) + case _ => sigVerify(tx.bodyBytes, tx.proofs[0], dappPublicKey) + } +} diff --git a/ride4dapps/blockchain/blockchain_test.js b/ride4dapps/blockchain/blockchain_test.js new file mode 100644 index 0000000..6f42d59 --- /dev/null +++ b/ride4dapps/blockchain/blockchain_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 From 9fb0c74942db106c205a3904ebfc87ee0d617a13 Mon Sep 17 00:00:00 2001 From: Maxim Smolyakov Date: Thu, 16 May 2019 11:42:33 +0300 Subject: [PATCH 2/6] updated mining; refactor --- ride4dapps/blockchain/blockchain.ride | 273 +++++++++++++------------- 1 file changed, 136 insertions(+), 137 deletions(-) diff --git a/ride4dapps/blockchain/blockchain.ride b/ride4dapps/blockchain/blockchain.ride index 969d3e2..df76de8 100644 --- a/ride4dapps/blockchain/blockchain.ride +++ b/ride4dapps/blockchain/blockchain.ride @@ -2,86 +2,51 @@ {-# CONTENT_TYPE DAPP #-} {-# SCRIPT_TYPE ACCOUNT #-} +let dappPublicKey = base58'' # SPECIFY BEFORE SETTING THE CONTRACT! + let keyAccountPrefix = "@" let keyPubKeyPrefix = "$" -let keyBase = "base" let keyHeight = "height" let keyLast = "last" let keyUtx = "utx" let keyUtxSize = "utx-size" -let dappPublicKey = base58'' # TODO SET! - -let registrationCost = 5 +let registrationCost = 5 # TODO CHECK! let utxLimit = 100 let blockMinerReward = 20 let initBalance = 5 func h() = getIntegerValue(this, keyHeight) -func base() = getIntegerValue(this, keyBase) -func blockInfo(h: Int) = getStringValue(this, if h == -1 || h == h() then keyLast else h.toString()) -func blockReferenceHeight(h: Int) = { - let blInfo = blockInfo(h) - let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1 - let right = extract(indexOf(blInfo, ",", left)) - take(drop(blInfo, left), right - left).parseIntValue() -} -func blockMinerPublicKey(h: Int) = { - let blInfo = blockInfo(h) - let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1)) + 1 - let right = extract(indexOf(blInfo, ",", left)) - take(drop(blInfo, left), right - left) -} -func blockPrevHash(h: Int) = { - let blInfo = blockInfo(h) - let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1)) + 1)) + 1 - let right = extract(indexOf(blInfo, ",", left)) - take(drop(blInfo, left), right - left) -} -func blockGasReward(h: Int) = { - let blInfo = blockInfo(h) - let left = extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",", extract(indexOf(blInfo, ",")) + 1)) + 1)) + 1)) + 1)) + 1 - let right = extract(indexOf(blInfo, ",", left)) - take(drop(blInfo, left), right - left).parseIntValue() -} +func blockInfo(h: Int) = this.getStringValue(if h == -1 || h == h() then keyLast else h.toString()) +func blockHeaders(h: Int) = blockInfo(h).split(";")[0].split(",") + +func blockHash(h: Int) = blockHeaders(h)[0] +func blockTimestamp(h: Int) = blockHeaders(h)[1].parseIntValue() +func blockReferenceHeight(h: Int) = blockHeaders(h)[2].parseIntValue() +func blockMinerAccount(h: Int) = blockHeaders(h)[3] +func blockNonce(h: Int) = blockHeaders(h)[4].parseIntValue() +func blockPrevHash(h: Int) = blockHeaders(h)[5] +func blockDifficulty(h: Int) = blockHeaders(h)[6].parseIntValue() +func blockGasReward(h: Int) = blockHeaders(h)[7].parseIntValue() func blockTxs(h: Int) = { let blInfo = blockInfo(h) - let semicolon = indexOf(blInfo, ";") + let semicolon = blInfo.indexOf(";") if isDefined(semicolon) then - drop(blInfo, extract(semicolon) + 1) + blInfo.drop(semicolon.extract() + 1) else "" } func pubKey(pk: ByteVector) = pk.toBase58String() -func accountOf(pk: ByteVector) = getString(this, keyPubKeyPrefix + pubKey(pk)) -func accountInfo(name: String) = getString(this, keyAccountPrefix + name) -func isRegistered(pK: ByteVector) = isDefined(accountOf(pK)) -func isTaken(acc: String) = isDefined(accountInfo(acc)) +func isRegistered(pK: ByteVector) = isDefined(this.getString(keyPubKeyPrefix + pubKey(pK))) +func isTaken(name: String) = isDefined(this.getString(keyAccountPrefix + name)) +func accountOf(pK: ByteVector) = this.getStringValue(keyPubKeyPrefix + pubKey(pK)) +func accountInfo(name: String) = this.getStringValue(keyAccountPrefix + name).split(",") -func pubKeyOf(account: String) = { - let accInfo = extract(accountInfo(account)) - let right = extract(indexOf(accInfo, ",")) - take(accInfo, right) -} -func regHeightOf(account: String) = { - let accInfo = extract(accountInfo(account)) - let left = extract(indexOf(accInfo, ",")) + 1 - let right = extract(indexOf(accInfo, ",", left)) - take(drop(accInfo, left), right - left).parseIntValue() -} -func totalBalanceOf(account: String) = { - let accInfo = extract(accountInfo(account)) - let left = extract(indexOf(accInfo, ",", extract(indexOf(accInfo, ",")) + 1)) + 1 - let right = extract(indexOf(accInfo, ",", left)) - take(drop(accInfo, left), right - left).parseIntValue() -} -func availableBalanceOf(account: String) = { - let accInfo = extract(accountInfo(account)) - let left = extract(indexOf(accInfo, ",", extract(indexOf(accInfo, ",", extract(indexOf(accInfo, ",")) + 1)) + 1)) + 1 - let right = accInfo.size() - take(drop(accInfo, left), right - left).parseIntValue() -} +func pubKeyOf(account: String) = accountInfo(account)[0] +func regHeightOf(account: String) = accountInfo(account)[1].parseIntValue() +func totalBalanceOf(account: String) = accountInfo(account)[2].parseIntValue() +func availableBalanceOf(account: String) = accountInfo(account)[3].parseIntValue() func estimate(script: String) = { let words = script.split(" ") @@ -96,7 +61,7 @@ func estimate(script: String) = { func evaluate(script: String) = { let words = script.split(" ") func send(recipient: String, amount: Int) = { - DataEntry(keyAccountPrefix + recipient, pubKeyOf(recipient) + "," + regHeightOf(recipient).toString() + "," + DataEntry(keyAccountPrefix + recipient, pubKeyOf(recipient) + "," + regHeightOf(recipient).toString() + "," + (totalBalanceOf(recipient) + amount).toString() + "," + (availableBalanceOf(recipient) + amount).toString()) } @@ -129,142 +94,176 @@ func validate(acc: String, gas: Int, script: String, checkBalance: Boolean) = { @Callable(i) func genesis() = { if i.caller != this then - throw("blockchain can be created only by the dApp") - else if isDefined(getString(this, keyLast)) - || isDefined(getInteger(this, keyHeight)) - || isDefined(getInteger(this, keyBase)) - || isDefined(getInteger(this, keyUtx)) - || isDefined(getInteger(this, keyUtxSize)) + 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("blockchain is already created") + throw("Rudechain is already created") else let gHeight = 0 - let genesisBlock = "," + gHeight.toString() + "," + height.toString() + "," + toBase58String(i.callerPublicKey) + ",0,0" - WriteSet( - DataEntry(keyLast, genesisBlock) - ::DataEntry(keyHeight, gHeight) - ::DataEntry(keyBase, 1) - ::DataEntry(keyUtx, "") - ::DataEntry(keyUtxSize, 0) - ::nil - ) + let genesisBlock = "0," + lastBlock.timestamp.toString() + "," + gHeight.toString() + "," + pubKey(i.callerPublicKey) + ",0,0,1,0" + WriteSet([ + DataEntry(keyLast, genesisBlock), + DataEntry(keyHeight, gHeight), + DataEntry(keyUtx, ""), + DataEntry(keyUtxSize, 0) + ]) } @Callable(i) func register(name: String) = { - let callerPubKey = i.callerPublicKey.toBase58String() let validChars = "abcdefghijklmnopqrstuvwxyz0123456789" - if (!isDefined(i.payment) || isDefined(extract(i.payment).assetId) || extract(i.payment).amount != registrationCost * 100000000) then + if (!isDefined(i.payment) || isDefined(i.payment.extract().assetId) || i.payment.extract().amount != registrationCost * 100000000) then throw("Registration costs " + registrationCost.toString() + " Waves!") - else if !(size(name) > 0 && size(name) <= 8 && isDefined(indexOf(validChars, take(name, 1))) - && (if (size(name) > 1) then isDefined(indexOf(validChars, take(drop(name, 1), 1))) else true) - && (if (size(name) > 2) then isDefined(indexOf(validChars, take(drop(name, 2), 1))) else true) - && (if (size(name) > 3) then isDefined(indexOf(validChars, take(drop(name, 3), 1))) else true) - && (if (size(name) > 4) then isDefined(indexOf(validChars, take(drop(name, 4), 1))) else true) - && (if (size(name) > 5) then isDefined(indexOf(validChars, take(drop(name, 5), 1))) else true) - && (if (size(name) > 6) then isDefined(indexOf(validChars, take(drop(name, 6), 1))) else true) - && (if (size(name) > 7) then isDefined(indexOf(validChars, take(drop(name, 7), 1))) else true)) + 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 [1..8] length and contain only [a-z0-9] chars") + throw("Account name must have [2..8] length and contain only [a-z0-9] chars") else if isRegistered(i.callerPublicKey) then - throw("Public key of the caller is already registered as '" + extract(accountOf(i.callerPublicKey)) + "'") + throw("Public key of the caller is already registered as '" + accountOf(i.callerPublicKey) + "'") else if isTaken(name) then throw("Account name '" + name + "' is already taken") else WriteSet([ - DataEntry(keyPubKeyPrefix + i.callerPublicKey.toBase58String(), name), + DataEntry(keyPubKeyPrefix + pubKey(i.callerPublicKey), name), DataEntry(keyAccountPrefix + name, pubKey(i.callerPublicKey) + "," + h().toString() + "," + initBalance.toString() + "," + initBalance.toString()) ]) } @Callable(i) -func mine(nonce: String) = { - let delta = height - h() +func mine(nonce: Int) = { + let delta = lastBlock.height - blockReferenceHeight(-1) + let difficulty = blockDifficulty(-1) + let newDifficulty = if delta == 1 then difficulty + 2 else if delta == 2 || delta == 3 || difficulty == 1 then difficulty else difficulty - 1 + + let hash = blake2b256(( + lastBlock.timestamp.toString() + + lastBlock.height.toString() + + i.callerPublicKey.toBase58String() + + 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.callerPublicKey) then throw("Miner must be registered!") - else if delta == 0 then throw("Can't mine on same reference height as last block: " + height.toString()) + 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 minerAccount = extract(accountOf(i.callerPublicKey)) - let nextHeight = h() + 1 - let liquidBlock = h().toString() + blockReferenceHeight(-1).toString() + blockMinerPublicKey(-1) + blockPrevHash(-1) + blockGasReward(-1).toString() + blockTxs(-1) - let liquidBlockHash = sha256(liquidBlock.toBytes()).toBase58String() + let prevMinerAccount = blockMinerAccount(-1) + let newMinerAccount = accountOf(i.callerPublicKey) + let newHeight = h() + 1 + let newBlock = hash.toBase58String() + + "," + lastBlock.timestamp.toString() + + "," + lastBlock.height.toString() + + "," + newMinerAccount + + "," + nonce.toString() + + "," + blockPrevHash(-1) + + "," + newDifficulty.toString() + + ",0" WriteSet([ - DataEntry(keyAccountPrefix + minerAccount, pubKeyOf(minerAccount) + "," + regHeightOf(minerAccount).toString() + "," - + (totalBalanceOf(minerAccount) + blockGasReward(-1)).toString() + "," + (availableBalanceOf(minerAccount) + blockGasReward(-1)).toString()) - , DataEntry(h().toString(), liquidBlockHash + blockInfo(-1)) - , DataEntry(keyHeight, nextHeight) - , DataEntry(keyBase, base() + 1) - , DataEntry(keyLast, "," + nextHeight.toString() + "," + height.toString() + "," + toBase58String(i.callerPublicKey) + "," + liquidBlockHash + ",0") + DataEntry(keyHeight, newHeight), + DataEntry(keyAccountPrefix + prevMinerAccount, pubKeyOf(prevMinerAccount) + "," + regHeightOf(prevMinerAccount).toString() + "," + + (totalBalanceOf(prevMinerAccount) + blockGasReward(-1)).toString() + "," + (availableBalanceOf(prevMinerAccount) + blockGasReward(-1)).toString()), + DataEntry(h().toString(), blockInfo(-1)), + DataEntry(keyAccountPrefix + newMinerAccount, pubKeyOf(newMinerAccount) + "," + regHeightOf(newMinerAccount).toString() + "," + + (totalBalanceOf(newMinerAccount) + blockMinerReward).toString() + "," + (availableBalanceOf(newMinerAccount) + blockMinerReward).toString()), + DataEntry(keyLast, newBlock) ]) } @Callable(i) func utxProcessing() = { - if i.callerPublicKey != blockMinerPublicKey(-1).fromBase58String() then throw("Only the current miner can processing UTX!") - else if getIntegerValue(this, keyUtxSize) == 0 then WriteSet([]) + if i.callerPublicKey != pubKeyOf(blockMinerAccount(-1)).fromBase58String() then throw("Only the current miner can processing UTX!") + else if this.getIntegerValue(keyUtxSize) == 0 then WriteSet([]) else - let utx = getStringValue(this, keyUtx) - let utxSize = getIntegerValue(this, keyUtxSize) - let marker = extract(indexOf(utx, ";")) - let tx = take(utx, marker) + let utx = this.getStringValue(keyUtx) + let utxSize = this.getIntegerValue(keyUtxSize) + let marker = utx.indexOf(";").extract() + let tx = utx.take(marker) + let txFields = tx.split(",") - let txSignature = take(tx, extract(indexOf(tx, ","))) - let txSenderPublicKey = { let raw = drop(tx, txSignature.size() + 1); take(raw, extract(indexOf(raw, ","))) } - let txGas = { let raw = drop(tx, txSignature.size() + txSenderPublicKey.size() + 2); take(raw, extract(indexOf(raw, ","))) } - let txScript = drop(tx, txSignature.size() + txSenderPublicKey.size() + txGas.size() + 3) + let txSenderAccount = txFields[0] + let txGas = txFields[1].parseIntValue() + let txScript = txFields[2] - let txSenderAcc = extract(accountOf(txSenderPublicKey.toBytes())) - let validation = validate(txSenderAcc, txGas.parseIntValue(), txScript, false) + let txSenderPubKey = pubKeyOf(txSenderAccount) + let validation = validate(txSenderPubKey, txGas, txScript, false) let costs = estimate(txScript) - if validation.size() > 0 then - WriteSet([DataEntry(keyUtxSize, utxSize - 1), DataEntry(keyUtx, drop(utx, marker + 1))]) + if validation.size() > 0 then + WriteSet([DataEntry(keyUtxSize, utxSize - 1), DataEntry(keyUtx, utx.drop(marker + 1))]) else + let increasedReward = blockGasReward(-1) + costs[0] + let txs = if isDefined(blockInfo(-1).indexOf(";")) then ";" + blockTxs(-1) else "" WriteSet( - DataEntry(keyLast, "," + h().toString() + "," + blockReferenceHeight(-1).toString() + "," + blockMinerPublicKey(-1) + "," - + blockPrevHash(-1) + "," + (blockGasReward(-1) + costs[0]).toString() + blockTxs(-1) + ";" + tx) + DataEntry(keyLast, blockHash(-1) + "," + blockTimestamp(-1).toString() + "," + blockReferenceHeight(-1).toString() + "," + blockMinerAccount(-1) + "," + + blockNonce(-1).toString() + "," + blockPrevHash(-1) + "," + blockDifficulty(-1).toString() + "," + increasedReward.toString() + txs + ";" + tx) ::DataEntry(keyUtxSize, utxSize - 1) - ::DataEntry(keyUtx, drop(utx, marker + 1)) + ::DataEntry(keyUtx, utx.drop(marker + 1)) ::evaluate(txScript) - ::DataEntry( keyAccountPrefix + txSenderAcc, txSenderPublicKey + "," + regHeightOf(txSenderAcc).toString() + "," - + (totalBalanceOf(txSenderAcc) - costs[0] - costs[1]).toString() + "," - + (availableBalanceOf(txSenderAcc) + txGas.parseIntValue() - costs[0]).toString() ) + ::DataEntry( keyAccountPrefix + txSenderAccount, txSenderPubKey + "," + regHeightOf(txSenderAccount).toString() + "," + + (totalBalanceOf(txSenderAccount) - costs[0] - costs[1]).toString() + "," + + (availableBalanceOf(txSenderAccount) + txGas - costs[0]).toString() ) ::nil ) } @Callable(i) -func transaction(signatureBase64: String, gas: Int, script: String) = { - if i.caller == this then throw("The dApp can't send transactions!") +func transaction(gas: Int, script: String) = { + if i.caller == this then throw("The Rudechain dApp can't send transactions!") else if !isRegistered(i.callerPublicKey) then throw("Only registered accounts can send transactions!") - else if !(getIntegerValue(this, keyUtxSize) < utxLimit) then throw("UTX size limit reached! Please try later") + else if this.getIntegerValue(keyUtxSize) == utxLimit then throw("UTX size limit reached! Please try later") else - let txBody = extract(accountOf(i.callerPublicKey)) + "," + gas.toString() + "," + script - let acc = extract(accountOf(i.callerPublicKey)) - let validation = validate(acc, gas, script, true) + let sender = accountOf(i.callerPublicKey) + let txBody = sender + "," + gas.toString() + "," + script + let validation = validate(sender, gas, script, true) let costs = estimate(script) let reserved = costs[0] + costs[1] if validation.size() > 0 then throw(validation) - else if !sigVerify(txBody.toBytes(), signatureBase64.fromBase64String(), i.callerPublicKey) then throw("Incorrect signature!") else - let utxPool = getStringValue(this, keyUtx) + let utxPool = this.getStringValue(keyUtx) WriteSet([ - DataEntry(keyUtx, utxPool + signatureBase64 + "," + txBody + ";"), - DataEntry(keyUtxSize, getIntegerValue(this, keyUtxSize) + 1), - DataEntry(keyAccountPrefix + acc, pubKeyOf(acc) + "," + regHeightOf(acc).toString() + "," + totalBalanceOf(acc).toString() + "," - + (availableBalanceOf(acc) - reserved).toString()) + DataEntry(keyUtx, utxPool + txBody + ";"), + DataEntry(keyUtxSize, this.getIntegerValue(keyUtxSize) + 1), + DataEntry(keyAccountPrefix + sender, pubKeyOf(sender) + "," + regHeightOf(sender).toString() + "," + totalBalanceOf(sender).toString() + "," + + (availableBalanceOf(sender) - reserved).toString()) ]) } @Verifier(tx) func verify() = { match tx { - case d:DataTransaction => false - case t:TransferTransaction => isDefined(t.assetId) - case _ => sigVerify(tx.bodyBytes, tx.proofs[0], dappPublicKey) + case d:DataTransaction => false # rudechain can be changed only via dApp actions + case _ => tx.bodyBytes.sigVerify(tx.proofs[0], dappPublicKey) } } From 7222ee02655b382cfcb9e42c5e0b96056fda035c Mon Sep 17 00:00:00 2001 From: Maxim Smolyakov Date: Sat, 18 May 2019 17:11:33 +0300 Subject: [PATCH 3/6] fixed utx processing and mining --- ride4dapps/blockchain/blockchain.ride | 157 +++++++++++++++++++------- 1 file changed, 118 insertions(+), 39 deletions(-) diff --git a/ride4dapps/blockchain/blockchain.ride b/ride4dapps/blockchain/blockchain.ride index df76de8..7476760 100644 --- a/ride4dapps/blockchain/blockchain.ride +++ b/ride4dapps/blockchain/blockchain.ride @@ -11,24 +11,78 @@ let keyLast = "last" let keyUtx = "utx" let keyUtxSize = "utx-size" -let registrationCost = 5 # TODO CHECK! +let registrationCost = 5 let utxLimit = 100 let blockMinerReward = 20 let initBalance = 5 func h() = getIntegerValue(this, keyHeight) +# 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 blockHeaders(h: Int) = blockInfo(h).split(";")[0].split(",") - -func blockHash(h: Int) = blockHeaders(h)[0] -func blockTimestamp(h: Int) = blockHeaders(h)[1].parseIntValue() -func blockReferenceHeight(h: Int) = blockHeaders(h)[2].parseIntValue() -func blockMinerAccount(h: Int) = blockHeaders(h)[3] -func blockNonce(h: Int) = blockHeaders(h)[4].parseIntValue() -func blockPrevHash(h: Int) = blockHeaders(h)[5] -func blockDifficulty(h: Int) = blockHeaders(h)[6].parseIntValue() -func blockGasReward(h: Int) = blockHeaders(h)[7].parseIntValue() +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(";") @@ -41,12 +95,31 @@ func pubKey(pk: ByteVector) = pk.toBase58String() func isRegistered(pK: ByteVector) = isDefined(this.getString(keyPubKeyPrefix + pubKey(pK))) func isTaken(name: String) = isDefined(this.getString(keyAccountPrefix + name)) func accountOf(pK: ByteVector) = this.getStringValue(keyPubKeyPrefix + pubKey(pK)) -func accountInfo(name: String) = this.getStringValue(keyAccountPrefix + name).split(",") +func accountInfo(name: String) = this.getStringValue(keyAccountPrefix + name) -func pubKeyOf(account: String) = accountInfo(account)[0] -func regHeightOf(account: String) = accountInfo(account)[1].parseIntValue() -func totalBalanceOf(account: String) = accountInfo(account)[2].parseIntValue() -func availableBalanceOf(account: String) = accountInfo(account)[3].parseIntValue() +func pubKeyOf(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(" ") @@ -66,7 +139,7 @@ func evaluate(script: String) = { } if words[0] == "SEND" then - send(words[1], words[2].parseIntValue()) + send(words[1].split(" ")[1], words[2].parseIntValue()) else throw("can't evaluate script") # unreachable state because validated } @@ -77,7 +150,7 @@ func validate(acc: String, gas: Int, script: String, checkBalance: Boolean) = { if words.size() != 3 then "SEND command expects args: recipient amount" else let gasRequired = estimate(script)[0] - let recipient = words[1] + let recipient = words[1].split(" ")[1] let amount = words[2].parseIntValue() if !isTaken(recipient) then "recipient '" + recipient + "' doesn't exist" @@ -102,13 +175,18 @@ func genesis() = { then throw("Rudechain is already created") else + let minerName = "dapp" let gHeight = 0 - let genesisBlock = "0," + lastBlock.timestamp.toString() + "," + gHeight.toString() + "," + pubKey(i.callerPublicKey) + ",0,0,1,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(keyLast, genesisBlock), DataEntry(keyHeight, gHeight), DataEntry(keyUtx, ""), - DataEntry(keyUtxSize, 0) + DataEntry(keyUtxSize, 0), + DataEntry(keyPubKeyPrefix + pubKey(dappPublicKey), "dapp"), + DataEntry(keyAccountPrefix + "dapp", pubKey(dappPublicKey) + "," + gHeight.toString() + ",0,0") ]) } @@ -149,7 +227,7 @@ func mine(nonce: Int) = { let hash = blake2b256(( lastBlock.timestamp.toString() + lastBlock.height.toString() - + i.callerPublicKey.toBase58String() + + accountOf(i.callerPublicKey) + nonce.toString() + blockPrevHash(-1) ).toBytes()) @@ -202,17 +280,17 @@ func mine(nonce: Int) = { @Callable(i) func utxProcessing() = { + let utx = this.getStringValue(keyUtx) + let utxSize = this.getIntegerValue(keyUtxSize) + if i.callerPublicKey != pubKeyOf(blockMinerAccount(-1)).fromBase58String() then throw("Only the current miner can processing UTX!") - else if this.getIntegerValue(keyUtxSize) == 0 then WriteSet([]) + else if utxSize == 0 then WriteSet([]) else - let utx = this.getStringValue(keyUtx) - let utxSize = this.getIntegerValue(keyUtxSize) - let marker = utx.indexOf(";").extract() - let tx = utx.take(marker) + let tx = utx.split(";")[0] let txFields = tx.split(",") let txSenderAccount = txFields[0] - let txGas = txFields[1].parseIntValue() + let txGas = txFields[1].split(",")[1].parseIntValue() let txScript = txFields[2] let txSenderPubKey = pubKeyOf(txSenderAccount) @@ -220,21 +298,20 @@ func utxProcessing() = { let costs = estimate(txScript) if validation.size() > 0 then - WriteSet([DataEntry(keyUtxSize, utxSize - 1), DataEntry(keyUtx, utx.drop(marker + 1))]) + 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 "" - WriteSet( - DataEntry(keyLast, blockHash(-1) + "," + blockTimestamp(-1).toString() + "," + blockReferenceHeight(-1).toString() + "," + blockMinerAccount(-1) + "," - + blockNonce(-1).toString() + "," + blockPrevHash(-1) + "," + blockDifficulty(-1).toString() + "," + increasedReward.toString() + txs + ";" + tx) - ::DataEntry(keyUtxSize, utxSize - 1) - ::DataEntry(keyUtx, utx.drop(marker + 1)) - ::evaluate(txScript) - ::DataEntry( keyAccountPrefix + txSenderAccount, txSenderPubKey + "," + regHeightOf(txSenderAccount).toString() + "," + 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)), + evaluate(txScript), + DataEntry( keyAccountPrefix + txSenderAccount, txSenderPubKey + "," + regHeightOf(txSenderAccount).toString() + "," + (totalBalanceOf(txSenderAccount) - costs[0] - costs[1]).toString() + "," + (availableBalanceOf(txSenderAccount) + txGas - costs[0]).toString() ) - ::nil - ) + ]) } @Callable(i) @@ -252,9 +329,11 @@ func transaction(gas: Int, script: String) = { 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, utxPool + txBody + ";"), - DataEntry(keyUtxSize, this.getIntegerValue(keyUtxSize) + 1), + DataEntry(keyUtx, newUtxPool), + DataEntry(keyUtxSize, utxSize + 1), DataEntry(keyAccountPrefix + sender, pubKeyOf(sender) + "," + regHeightOf(sender).toString() + "," + totalBalanceOf(sender).toString() + "," + (availableBalanceOf(sender) - reserved).toString()) ]) From 56b299c1d0d3343e08a92b47c8002b8476efa211 Mon Sep 17 00:00:00 2001 From: Maxim Smolyakov Date: Mon, 20 May 2019 18:43:33 +0300 Subject: [PATCH 4/6] miner, client, many improvements and optimisations --- ride4dapps/blockchain/README.md | 43 +++ ride4dapps/blockchain/client/Dockerfile | 11 + ride4dapps/blockchain/client/favicon.ico | Bin 0 -> 15406 bytes ride4dapps/blockchain/client/index.html | 363 ++++++++++++++++++ .../blockchain/{blockchain.ride => dapp.ride} | 182 ++++++--- .../{blockchain_test.js => dapp_test.js} | 0 ride4dapps/blockchain/miner/.gitignore | 14 + ride4dapps/blockchain/miner/pom.xml | 62 +++ ride4dapps/blockchain/miner/src/Main.java | 24 ++ .../blockchain/miner/src/RudechainMiner.java | 159 ++++++++ 10 files changed, 800 insertions(+), 58 deletions(-) create mode 100644 ride4dapps/blockchain/README.md create mode 100644 ride4dapps/blockchain/client/Dockerfile create mode 100644 ride4dapps/blockchain/client/favicon.ico create mode 100644 ride4dapps/blockchain/client/index.html rename ride4dapps/blockchain/{blockchain.ride => dapp.ride} (62%) rename ride4dapps/blockchain/{blockchain_test.js => dapp_test.js} (100%) create mode 100644 ride4dapps/blockchain/miner/.gitignore create mode 100644 ride4dapps/blockchain/miner/pom.xml create mode 100644 ride4dapps/blockchain/miner/src/Main.java create mode 100644 ride4dapps/blockchain/miner/src/RudechainMiner.java 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 0000000000000000000000000000000000000000..11146b879dd720fc02e18a639c70b7c66a2f53ab GIT binary patch literal 15406 zcmeHOTSygM6du#Gt7#}$njM46icq9X6T}O7c`+2Cs6Z-yeo;3baXV0kB>W~RiZzA`ZU*GdwaW6S|R<~wQH%g zw3O!0pHIt{Eu-JRf9s;3J$p8VgoKcnmlt_^ds9hCiA(s0hlgqX`t@8NnwpxNVvx!Y z1n=Ixdn3%}b4G0LJ@bGZ*@$uol z(Z<-=m_z#S-@g;|m(RzJ9i!^%YPx&(u0y(s{87;;=qQApv9URc&9F~uX(Ib*@t*t= zc>n%Ay?OISF-vVW?7Xz@uV23mbuuU@h@0oE{>q-Yxw)dY4;?xrtJB-tE2^XC12#&D zFJC6)4;vL27$~c6g#V8pKj_k>OH@=;Bx)CHU|U-o4Gs?4yL0~hc~PA`d-hOgXD6Wz zqv^$q7oxVY_6QT!z{to*d!5CL7n9L(7q;(=e@91$oj1m&uC7iVm;ToYaS}E}%jB`yij$~miB)b%z>>hJMj+;3{XNs0tE*Li(+F!EPj4|T+AXlIhn3r zy($tN_B~lySu*TXrc9xQ3l~yKN(%M#^oVH8{J`A87(9ReoSK`PWjJzjatLuh#9{3p>;6=)wQs!2^njh>*$t%$YN$ zo=52Eq*nZ}nH3ckG;P{65&n7e=22f?pB|Q`*42t1b_lkV*BSnK;J^V>W6^p$rxkx! zR~JcR={S4DdFDhWCH$*bucqV2kJHJMCkgjhs;a6eGc!{pd&H(rojNs<&^g{p_<7v` zMr54j;K`TciSvXG}}ZEfY^ky`F3xudg= z&e~MM54mpJwvBe|*g=~&Z|3Zdqk96-FT|^{C-QVs!ms52V6S!a=1tnPX%pwSO2+}; z7za=HK`VYsq_3PJs6}LZ);D7x1u?+w6<;w~CJ5MDe_@PJAeHe7glk1vM{L`mT7mdM! z1q%q{;;Cc=KlUtA{r{Wv2Y#It|KBiw;p-rN1z8L6!+zpi+KfACTJ_Hr{dxNIDQ(!W zfu>HKD#AZ&)-1Yw`Ld?_+FJ2BtEUw|WIn*!MeGZ(XGE+@D0{r8rlxZ7TxUFu{*D+i z;!}6-+@Xes1{sd1s3_XIcQ2<4`T6;jot;f9R;&=+<&t8@okE<)dmSrI{AU3Egr^c>lX@_!)rAh>9x3jhU@h}has@xeivMo~{+9v0nyCBzkk{eEhc#vV z|Kr4o6Wra!#>PgvapMNPdiBbqHby$Guq7C8tr_*LWJ$o1fF*(dssz|I(n^*DT$aE; DPNexF literal 0 HcmV?d00001 diff --git a/ride4dapps/blockchain/client/index.html b/ride4dapps/blockchain/client/index.html new file mode 100644 index 0000000..10d3f81 --- /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 {{user.registrationHeight}} Waves height
  • +
+ + 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/blockchain.ride b/ride4dapps/blockchain/dapp.ride similarity index 62% rename from ride4dapps/blockchain/blockchain.ride rename to ride4dapps/blockchain/dapp.ride index 7476760..a292068 100644 --- a/ride4dapps/blockchain/blockchain.ride +++ b/ride4dapps/blockchain/dapp.ride @@ -2,21 +2,22 @@ {-# CONTENT_TYPE DAPP #-} {-# SCRIPT_TYPE ACCOUNT #-} -let dappPublicKey = base58'' # SPECIFY BEFORE SETTING THE CONTRACT! +# SPECIFY BEFORE SETTING THE CONTRACT! +let registrationCost = 5 +let initBalance = 5 +let blockMinerReward = 10 +let utxLimit = 100 let keyAccountPrefix = "@" -let keyPubKeyPrefix = "$" +let keyAddressPrefix = "$" +let keyAssetId = "assetId" let keyHeight = "height" let keyLast = "last" let keyUtx = "utx" let keyUtxSize = "utx-size" -let registrationCost = 5 -let utxLimit = 100 -let blockMinerReward = 20 -let initBalance = 5 - 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()) @@ -51,8 +52,8 @@ func blockNonce(h: Int) = { } func blockPrevHash(h: Int) = { let blInfo = blockInfo(h) - let left = blInfo.indexOf(",", - blInfo.indexOf(",", + let left = blInfo.indexOf(",", + blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",").extract() + 1 @@ -65,8 +66,8 @@ func blockPrevHash(h: Int) = { } func blockDifficulty(h: Int) = { let blInfo = blockInfo(h) - let left = blInfo.indexOf(",", - blInfo.indexOf(",", + let left = blInfo.indexOf(",", + blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",", blInfo.indexOf(",", @@ -91,13 +92,13 @@ func blockTxs(h: Int) = { else "" } -func pubKey(pk: ByteVector) = pk.toBase58String() -func isRegistered(pK: ByteVector) = isDefined(this.getString(keyPubKeyPrefix + pubKey(pK))) +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(pK: ByteVector) = this.getStringValue(keyPubKeyPrefix + pubKey(pK)) +func accountOf(addr: Address) = this.getStringValue(keyAddressPrefix + address(addr)) func accountInfo(name: String) = this.getStringValue(keyAccountPrefix + name) -func pubKeyOf(account: String) = { +func addressOf(account: String) = { let accInfo = accountInfo(account) let right = accInfo.indexOf(",").extract() take(accInfo, right) @@ -128,26 +129,57 @@ func estimate(script: String) = { let gasRequired = 1 let amount = words[2].parseIntValue() [gasRequired, amount] + else if words[0] == "SWAP" then + let gasRequired = 2 + let amount = words[2].parseIntValue() + [gasRequired, amount] else [1000000, 0] # unreachable state because validated } -func evaluate(script: String) = { +func evaluate(script: String, inv: Invocation) = { let words = script.split(" ") func send(recipient: String, amount: Int) = { - DataEntry(keyAccountPrefix + recipient, pubKeyOf(recipient) + "," + regHeightOf(recipient).toString() + "," - + (totalBalanceOf(recipient) + amount).toString() + "," + (availableBalanceOf(recipient) + amount).toString()) + 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, checkBalance: Boolean) = { +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 "SEND command expects args: recipient amount" + if words.size() != 3 then "Missed args: SEND recipient amount" else let gasRequired = estimate(script)[0] let recipient = words[1].split(" ")[1] @@ -160,12 +192,37 @@ func validate(acc: String, gas: Int, script: String, checkBalance: Boolean) = { 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\"" + # balance in, balance out + # gas req, gas attached else "unknown command " + words[0] } @Callable(i) -func genesis() = { +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)) @@ -174,6 +231,9 @@ func genesis() = { || 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 @@ -181,12 +241,13 @@ func genesis() = { 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(keyPubKeyPrefix + pubKey(dappPublicKey), "dapp"), - DataEntry(keyAccountPrefix + "dapp", pubKey(dappPublicKey) + "," + gHeight.toString() + ",0,0") + DataEntry(keyAddressPrefix + address(this), "dapp"), + DataEntry(keyAccountPrefix + "dapp", address(this) + "," + gHeight.toString() + ",0,0") ]) } @@ -196,7 +257,7 @@ func register(name: String) = { 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 + 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) @@ -207,14 +268,14 @@ func register(name: String) = { && (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.callerPublicKey) then - throw("Public key of the caller is already registered as '" + accountOf(i.callerPublicKey) + "'") + 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(keyPubKeyPrefix + pubKey(i.callerPublicKey), name), - DataEntry(keyAccountPrefix + name, pubKey(i.callerPublicKey) + "," + h().toString() + "," + initBalance.toString() + "," + initBalance.toString()) + DataEntry(keyAddressPrefix + address(i.caller), name), + DataEntry(keyAccountPrefix + name, address(i.caller) + "," + h().toString() + "," + initBalance.toString() + "," + initBalance.toString()) ]) } @@ -222,12 +283,12 @@ func register(name: String) = { func mine(nonce: Int) = { let delta = lastBlock.height - blockReferenceHeight(-1) let difficulty = blockDifficulty(-1) - let newDifficulty = if delta == 1 then difficulty + 2 else if delta == 2 || delta == 3 || difficulty == 1 then difficulty else difficulty - 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.callerPublicKey) + + accountOf(i.caller) + nonce.toString() + blockPrevHash(-1) ).toBytes()) @@ -247,32 +308,32 @@ func mine(nonce: Int) = { 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 + + if byte6LeadingZeros != 8 then byte6LeadingZeros else ( 8 + byte7LeadingZeros))))))) if i.caller == this then throw("The dApp can't mine!") - else if !isRegistered(i.callerPublicKey) then throw("Miner must be registered!") + 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.callerPublicKey) + let newMinerAccount = accountOf(i.caller) let newHeight = h() + 1 let newBlock = hash.toBase58String() - + "," + lastBlock.timestamp.toString() - + "," + lastBlock.height.toString() - + "," + newMinerAccount - + "," + nonce.toString() + + "," + lastBlock.timestamp.toString() + + "," + lastBlock.height.toString() + + "," + newMinerAccount + + "," + nonce.toString() + "," + blockPrevHash(-1) + "," + newDifficulty.toString() + ",0" WriteSet([ DataEntry(keyHeight, newHeight), - DataEntry(keyAccountPrefix + prevMinerAccount, pubKeyOf(prevMinerAccount) + "," + regHeightOf(prevMinerAccount).toString() + "," + 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, pubKeyOf(newMinerAccount) + "," + regHeightOf(newMinerAccount).toString() + "," + DataEntry(keyAccountPrefix + newMinerAccount, addressOf(newMinerAccount) + "," + regHeightOf(newMinerAccount).toString() + "," + (totalBalanceOf(newMinerAccount) + blockMinerReward).toString() + "," + (availableBalanceOf(newMinerAccount) + blockMinerReward).toString()), DataEntry(keyLast, newBlock) ]) @@ -283,7 +344,7 @@ func utxProcessing() = { let utx = this.getStringValue(keyUtx) let utxSize = this.getIntegerValue(keyUtxSize) - if i.callerPublicKey != pubKeyOf(blockMinerAccount(-1)).fromBase58String() then throw("Only the current miner can processing UTX!") + 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] @@ -293,36 +354,41 @@ func utxProcessing() = { let txGas = txFields[1].split(",")[1].parseIntValue() let txScript = txFields[2] - let txSenderPubKey = pubKeyOf(txSenderAccount) - let validation = validate(txSenderPubKey, txGas, txScript, false) + let txSender = addressOf(txSenderAccount) + let validation = validate(txSender, txGas, txScript, i, false) let costs = estimate(txScript) - if validation.size() > 0 then + 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 "" - 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)), - evaluate(txScript), - DataEntry( keyAccountPrefix + txSenderAccount, txSenderPubKey + "," + regHeightOf(txSenderAccount).toString() + "," - + (totalBalanceOf(txSenderAccount) - costs[0] - costs[1]).toString() + "," - + (availableBalanceOf(txSenderAccount) + txGas - costs[0]).toString() ) - ]) + 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) + txGas - costs[0]).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.callerPublicKey) then throw("Only registered accounts can 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.callerPublicKey) + let sender = accountOf(i.caller) let txBody = sender + "," + gas.toString() + "," + script - let validation = validate(sender, gas, script, true) + let validation = validate(sender, gas, script, i, true) let costs = estimate(script) let reserved = costs[0] + costs[1] @@ -334,7 +400,7 @@ func transaction(gas: Int, script: String) = { WriteSet([ DataEntry(keyUtx, newUtxPool), DataEntry(keyUtxSize, utxSize + 1), - DataEntry(keyAccountPrefix + sender, pubKeyOf(sender) + "," + regHeightOf(sender).toString() + "," + totalBalanceOf(sender).toString() + "," + DataEntry(keyAccountPrefix + sender, addressOf(sender) + "," + regHeightOf(sender).toString() + "," + totalBalanceOf(sender).toString() + "," + (availableBalanceOf(sender) - reserved).toString()) ]) } @@ -343,6 +409,6 @@ func transaction(gas: Int, script: String) = { func verify() = { match tx { case d:DataTransaction => false # rudechain can be changed only via dApp actions - case _ => tx.bodyBytes.sigVerify(tx.proofs[0], dappPublicKey) + case _ => tx.bodyBytes.sigVerify(tx.proofs[0], tx.senderPublicKey) } } diff --git a/ride4dapps/blockchain/blockchain_test.js b/ride4dapps/blockchain/dapp_test.js similarity index 100% rename from ride4dapps/blockchain/blockchain_test.js rename to ride4dapps/blockchain/dapp_test.js diff --git a/ride4dapps/blockchain/miner/.gitignore b/ride4dapps/blockchain/miner/.gitignore new file mode 100644 index 0000000..2506956 --- /dev/null +++ b/ride4dapps/blockchain/miner/.gitignore @@ -0,0 +1,14 @@ +.idea/ +*.iml +*.ipr +*.iws + +.settings/ +.classpath +.project + +out/ +target/ + +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 From 8d50e0324fc25bab1ea557d16f5fc7a8108604b3 Mon Sep 17 00:00:00 2001 From: Maxim Smolyakov Date: Mon, 20 May 2019 20:33:46 +0300 Subject: [PATCH 5/6] fixed script estimating --- ride4dapps/blockchain/client/index.html | 6 +++--- ride4dapps/blockchain/dapp.ride | 7 +++---- ride4dapps/blockchain/miner/.gitignore | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ride4dapps/blockchain/client/index.html b/ride4dapps/blockchain/client/index.html index 10d3f81..f06efc9 100644 --- a/ride4dapps/blockchain/client/index.html +++ b/ride4dapps/blockchain/client/index.html @@ -51,7 +51,7 @@
  • Total balance: {{user.totalBalance}}
  • Available balance: {{user.availableBalance}}
  • Asset balance in Waves: {{user.assetBalance}}
  • -
  • Registered at {{user.registrationHeight}} Waves height
  • +
  • Registered at height: {{user.registrationHeight}}
  • Create Transaction @@ -132,7 +132,7 @@

    Blocks (last {{blocksLimit}})

    el: '#app', data: { node: 'https://testnode4.wavesnodes.com/', - dappAddress: '3N2BSBNwXaSm8ZQs6TW7KhwNJp6vu4HEQqz', + dappAddress: '3MwgRB2FJzbquEZEnySc2jwhpaZnoWvCMJX', assetId: '', keeperState: {}, @@ -210,7 +210,7 @@

    Blocks (last {{blocksLimit}})

    {type: 'string', value: this.newAccountName} ] }, - payment: [{tokens: 1, assetId: "WAVES"}] + payment: [{tokens: 5, assetId: "WAVES"}] } }; WavesKeeper.signAndPublishTransaction(txInvokeScript).then((inv) => { diff --git a/ride4dapps/blockchain/dapp.ride b/ride4dapps/blockchain/dapp.ride index a292068..e7321aa 100644 --- a/ride4dapps/blockchain/dapp.ride +++ b/ride4dapps/blockchain/dapp.ride @@ -131,7 +131,8 @@ func estimate(script: String) = { [gasRequired, amount] else if words[0] == "SWAP" then let gasRequired = 2 - let amount = words[2].parseIntValue() + 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 } @@ -214,8 +215,6 @@ func validate(acc: String, gas: Int, script: String, inv: Invocation, checkBalan 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\"" - # balance in, balance out - # gas req, gas attached else "unknown command " + words[0] } @@ -373,7 +372,7 @@ func utxProcessing() = { ::DataEntry(keyUtx, utx.drop(tx.size() + 1)) ::DataEntry( keyAccountPrefix + txSenderAccount, txSender + "," + regHeightOf(txSenderAccount).toString() + "," + (totalBalanceOf(txSenderAccount) - costs[0] - costs[1]).toString() + "," - + (availableBalanceOf(txSenderAccount) + txGas - costs[0]).toString() ) + + (availableBalanceOf(txSenderAccount) - costs[0] + txGas).toString() ) ::result.data.data ), TransferSet(result.transfers.transfers) diff --git a/ride4dapps/blockchain/miner/.gitignore b/ride4dapps/blockchain/miner/.gitignore index 2506956..5eba97c 100644 --- a/ride4dapps/blockchain/miner/.gitignore +++ b/ride4dapps/blockchain/miner/.gitignore @@ -9,6 +9,7 @@ out/ target/ +*.jar seed.conf From e4f5deb1dcad4fda9ffebd669ffaa09e2c344d4d Mon Sep 17 00:00:00 2001 From: Maxim Smolyakov Date: Tue, 21 May 2019 09:11:51 +0300 Subject: [PATCH 6/6] client: fixed bocks sorting --- ride4dapps/blockchain/client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ride4dapps/blockchain/client/index.html b/ride4dapps/blockchain/client/index.html index f06efc9..b29415d 100644 --- a/ride4dapps/blockchain/client/index.html +++ b/ride4dapps/blockchain/client/index.html @@ -162,7 +162,7 @@

    Blocks (last {{blocksLimit}})

    else return +this.lastBlock.difficulty - (delta / 2) }, blocks: function() { - return this.rawBlocks.slice().sort((a, b) => a.height < b.height) + return this.rawBlocks.slice().sort((a, b) => b.height - a.height) } }, methods: {