Skip to content

Commit 7a3f5f1

Browse files
committed
new file: Blockchain/Blockchain.sm
new file: Blockchain/SigningKey.sm new file: Blockchain/main.sf
1 parent 6f0fba2 commit 7a3f5f1

File tree

5 files changed

+349
-2
lines changed

5 files changed

+349
-2
lines changed

Blockchain/Blockchain.sm

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#!/usr/bin/ruby
2+
3+
# Code translated from:
4+
# https://github.com/Savjee/SavjeeCoin
5+
6+
include SigningKey
7+
import SigningKey::SigningKey
8+
9+
class Transaction(fromAddress, String toAddress, Number amount) {
10+
11+
has timestamp = Date()
12+
has signature = nil
13+
14+
# Creates a SHA256 hash of the transaction
15+
-> calculateHash() -> String {
16+
[fromAddress, toAddress, amount, timestamp].dump.sha256
17+
}
18+
19+
/*
20+
* Signs a transaction with the given signingKey (which is an Elliptic keypair
21+
* object that contains a private key). The signature is then stored inside the
22+
* transaction object and later stored on the blockchain.
23+
*/
24+
-> signTransaction(SigningKey signingKey) {
25+
26+
# You can only send a transaction from the wallet that is linked to your
27+
# key. So here we check if the fromAddress matches your publicKey
28+
if (signingKey.getPublic != fromAddress) {
29+
die('You cannot sign transactions for other wallets!')
30+
}
31+
32+
# Calculate the hash of this transaction, sign it with the key
33+
# and store it inside the transaction obect
34+
const hashTx = self.calculateHash
35+
const sig = signingKey.sign(hashTx)
36+
37+
signature = sig
38+
}
39+
40+
/**
41+
* Checks if the signature is valid (transaction has not been tampered with).
42+
* It uses the fromAddress as the public key.
43+
*/
44+
-> isValid() -> Bool {
45+
# If the transaction doesn't have a from address we assume it's a
46+
# mining reward and that it's valid. You could verify this in a
47+
# different way (special field for instance)
48+
if (fromAddress == nil) {
49+
return true
50+
}
51+
52+
if (signature == nil) {
53+
die ('No signature in this transaction');
54+
}
55+
56+
SigningKey().keyFromPublic(fromAddress).verify(signature, self.calculateHash)
57+
}
58+
}
59+
60+
class Blockchain::Block (Date timestamp, Array transactions, String previousHash = '') {
61+
62+
has nonce = 0
63+
has hash = nil
64+
65+
method init {
66+
hash = self.calculateHash
67+
}
68+
69+
/**
70+
* Returns the SHA256 of this block (by processing all the data stored
71+
* inside this block)
72+
*/
73+
-> calculateHash() -> String {
74+
[previousHash, timestamp, transactions, nonce].dump.sha256
75+
}
76+
77+
/**
78+
* Starts the mining process on the block. It changes the 'nonce' until the hash
79+
* of the block starts with enough zeros (= difficulty)
80+
*/
81+
-> mineBlock(Number difficulty) {
82+
var target_prefix = "0"*difficulty
83+
84+
while (hash.first(difficulty) != target_prefix) {
85+
++nonce
86+
hash = self.calculateHash
87+
}
88+
89+
STDERR.say("Block mined: #{hash}")
90+
}
91+
92+
/**
93+
* Validates all the transactions inside this block (signature + hash) and
94+
* returns true if everything checks out. False if the block is invalid.
95+
*/
96+
-> hasValidTransactions() -> Bool {
97+
transactions.all { .isValid }
98+
}
99+
}
100+
101+
class Blockchain (Number difficulty = 2, Array pendingTransactions = [], Number miningReward = 100) {
102+
103+
has chain = []
104+
105+
method init {
106+
chain = [self.createGenesisBlock]
107+
}
108+
109+
-> createGenesisBlock() -> Blockchain::Block {
110+
Blockchain::Block(Date.parse('2021-10-07', '%Y-%m-%d'), [], '0')
111+
}
112+
113+
/**
114+
* Returns the latest block on our chain. Useful when you want to create a
115+
* new Block and you need the hash of the previous Block.
116+
*/
117+
-> getLatestBlock() -> Blockchain::Block {
118+
chain.last
119+
}
120+
121+
/**
122+
* Takes all the pending transactions, puts them in a Block and starts the
123+
* mining process. It also adds a transaction to send the mining reward to
124+
* the given address.
125+
*/
126+
-> minePendingTransactions(String miningRewardAddress) {
127+
const rewardTx = Transaction(nil, miningRewardAddress, miningReward)
128+
pendingTransactions.push(rewardTx)
129+
130+
const block = Blockchain::Block(Date.now, pendingTransactions, self.getLatestBlock.hash)
131+
block.mineBlock(difficulty)
132+
133+
STDERR.say('Block successfully mined!')
134+
chain.push(block)
135+
pendingTransactions = []
136+
}
137+
138+
/**
139+
* Add a new transaction to the list of pending transactions (to be added
140+
* next time the mining process starts). This verifies that the given
141+
* transaction is properly signed.
142+
*/
143+
-> addTransaction(Transaction transaction) {
144+
if (!transaction.fromAddress || !transaction.toAddress) {
145+
die('Transaction must include from and to address')
146+
}
147+
148+
# Verify the transaction
149+
if (!transaction.isValid()) {
150+
die('Cannot add invalid transaction to chain')
151+
}
152+
153+
if (transaction.amount <= 0) {
154+
die('Transaction amount should be higher than 0')
155+
}
156+
157+
# Making sure that the amount sent is not greater than existing balance
158+
if (self.getBalanceOfAddress(transaction.fromAddress) < transaction.amount) {
159+
die('Not enough balance')
160+
}
161+
162+
pendingTransactions.push(transaction)
163+
self
164+
}
165+
166+
/**
167+
* Returns the balance of a given wallet address.
168+
*/
169+
-> getBalanceOfAddress(String address) -> Number {
170+
var balance = 0;
171+
172+
chain.each {|block|
173+
block.transactions.each {|trans|
174+
if (trans.fromAddress == address) {
175+
balance -= trans.amount
176+
}
177+
178+
if (trans.toAddress == address) {
179+
balance += trans.amount
180+
}
181+
}
182+
}
183+
184+
return balance
185+
}
186+
187+
/**
188+
* Returns a list of all transactions that happened
189+
* to and from the given wallet address.
190+
*/
191+
-> getAllTransactionsForWallet(String address) -> Array {
192+
var txs = []
193+
194+
chain.each {|block|
195+
block.transactions.each {|tx|
196+
if ((tx.fromAddress == address) || (tx.toAddress == address)) {
197+
txs.push(tx)
198+
}
199+
}
200+
}
201+
202+
STDERR.printf("get transactions for wallet count: %s\n", txs.length)
203+
return txs
204+
}
205+
206+
/**
207+
* Loops over all the blocks in the chain and verify if they are properly
208+
* linked together and nobody has tampered with the hashes. By checking
209+
* the blocks it also verifies the (signed) transactions inside of them.
210+
*/
211+
-> isChainValid() -> Bool {
212+
213+
# Check if the Genesis block hasn't been tampered with by comparing
214+
# the output of createGenesisBlock with the first block on our chain
215+
if (self.createGenesisBlock.dump != chain.first.dump) {
216+
return false
217+
}
218+
219+
# Check the remaining blocks on the chain to see if there hashes and
220+
# signatures are correct
221+
chain.each_cons(2, {|previousBlock, currentBlock|
222+
223+
if (previousBlock.hash != currentBlock.previousHash) {
224+
return false
225+
}
226+
227+
if (!currentBlock.hasValidTransactions) {
228+
return false
229+
}
230+
231+
if (currentBlock.hash != currentBlock.calculateHash) {
232+
return false
233+
}
234+
})
235+
236+
return true
237+
}
238+
}

Blockchain/SigningKey.sm

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/ruby
2+
3+
var Ed25519 = require('Crypt::PK::Ed25519')
4+
5+
class SigningKey() {
6+
7+
has key = Ed25519.new.generate_key
8+
9+
method keyFromPublic(String public) {
10+
key = Ed25519.new
11+
key.import_key_raw(pack('H*', public), 'public')
12+
self
13+
}
14+
15+
method keyFromPrivate(String private) {
16+
key = Ed25519.new
17+
key.import_key_raw(pack('H*', private), 'private')
18+
self
19+
}
20+
21+
method load(String file) {
22+
var private = File(file).read
23+
self.keyFromPrivate(private)
24+
}
25+
26+
method store(String file) {
27+
File(file).write(self.getPrivate)
28+
self
29+
}
30+
31+
method getPublic() {
32+
unpack('H*', key.export_key_raw('public'))
33+
}
34+
35+
method getPrivate() {
36+
unpack('H*', key.export_key_raw('private'))
37+
}
38+
39+
method sign(String message) {
40+
unpack('H*', key.sign_message(message))
41+
}
42+
43+
method verify(String sig, String message) {
44+
key.verify_message(pack('H*', sig), message) ? true : false
45+
}
46+
}

Blockchain/main.sf

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/ruby
2+
3+
include Blockchain
4+
include SigningKey
5+
6+
import Blockchain::Blockchain
7+
import Blockchain::Transaction
8+
import SigningKey::SigningKey
9+
10+
# Your private key goes here
11+
var myKey = SigningKey()
12+
13+
# Another key
14+
var evilKey = SigningKey()
15+
16+
STDERR.printf("Your private key: %s\n", myKey.getPrivate)
17+
18+
# From that we can calculate your public key (which doubles as your wallet address)
19+
var myWalletAddress = myKey.getPublic
20+
21+
# Create new instance of Blockchain class
22+
var sidefCoin = Blockchain(difficulty: 2)
23+
24+
# Mine first block
25+
sidefCoin.minePendingTransactions(myWalletAddress)
26+
27+
# Create a transaction & sign it with your key
28+
const tx1 = Transaction(myWalletAddress, evilKey.getPublic, 100)
29+
tx1.signTransaction(myKey)
30+
#tx1.signTransaction(evilKey)
31+
sidefCoin.addTransaction(tx1)
32+
33+
# Mine block
34+
sidefCoin.minePendingTransactions(myWalletAddress)
35+
36+
# Create second transaction
37+
const tx2 = Transaction(myWalletAddress, evilKey.getPublic, 50)
38+
tx2.signTransaction(myKey)
39+
sidefCoin.addTransaction(tx2)
40+
41+
# Mine block
42+
sidefCoin.minePendingTransactions(evilKey.getPublic)
43+
44+
# Create third transaction
45+
const tx3 = Transaction(evilKey.getPublic, myWalletAddress, 10)
46+
tx3.signTransaction(evilKey)
47+
sidefCoin.addTransaction(tx3)
48+
49+
# Mine block
50+
sidefCoin.minePendingTransactions(myWalletAddress)
51+
52+
STDERR.say("\nMy balance is: #{sidefCoin.getBalanceOfAddress(myWalletAddress)}")
53+
STDERR.say("Evil balance is: #{sidefCoin.getBalanceOfAddress(evilKey.getPublic)}")
54+
55+
# Uncomment this line if you want to test tampering with the chain
56+
#sidefCoin.chain[1].transactions[0].amount = 10;
57+
58+
# Check if the chain is valid
59+
STDERR.say("\nBlockchain valid? ", sidefCoin.isChainValid() ? 'Yes' : 'No')

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ A nice collection of day-to-day Sidef scripts.
55

66
### Summary
77

8+
* Blockchain
9+
* [Blockchain](./Blockchain/Blockchain.sm)
10+
* [Main](./Blockchain/main.sf)
11+
* [SigningKey](./Blockchain/SigningKey.sm)
812
* Converters
913
* [Any2audio](./Converters/any2audio.sf)
1014
* [Any2mp3](./Converters/any2mp3.sf)

update_readme.pl

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ sub add_section {
6464
my $title = $file->{name} =~ tr/_/ /r =~ s/ s /'s /gr;
6565

6666
if ($file->{name} =~ /\.(\w{2,3})\z/) {
67-
next if $1 !~ /^sf\z/i;
67+
next if $1 !~ /^(?:sf|sm)\z/i;
6868
}
6969

7070
if (-d $file->{path}) {
@@ -74,7 +74,7 @@ sub add_section {
7474
}
7575
else {
7676
next if $dir eq $main_dir;
77-
my $naked_title = $title =~ s/\.sf\z//ri;
77+
my $naked_title = $title =~ s/\.(?:sf|sm)\z//ri;
7878
my $url_path = uri_escape($make_section_url->($file->{name}), ' ');
7979
$section .= (' ' x $spaces) . "* [\u$naked_title]($url_path)\n";
8080
}

0 commit comments

Comments
 (0)