├── README.md ├── main.php ├── block.php ├── blockchain.php └── CuteDB.php /README.md: -------------------------------------------------------------------------------- 1 | # blockchain-php 2 | Blockchain implementation in PHP 3 | -------------------------------------------------------------------------------- /main.php: -------------------------------------------------------------------------------- 1 | addBlock('This is block1'); 8 | // $bc->addBlock('This is block2'); 9 | 10 | $bc->printBlockchain(); 11 | -------------------------------------------------------------------------------- /block.php: -------------------------------------------------------------------------------- 1 | prevHash = $prevHash; 14 | $this->timeStamp = time(); 15 | $this->data = $data; 16 | $this->findBlockHash(); 17 | } 18 | 19 | public function getBlockHash() 20 | { 21 | return $this->hash; 22 | } 23 | 24 | public function prepareData($nonce) 25 | { 26 | return json_encode([ 27 | $this->prevHash, 28 | $this->timeStamp, 29 | $this->data, 30 | $nonce, 31 | ]); 32 | } 33 | 34 | public function findBlockHash() 35 | { 36 | $found = false; 37 | $condition = '0000'; 38 | $condlength = strlen($condition); 39 | 40 | printf("Mining the block containing \"%s\"\n", $this->data); 41 | 42 | for ($nonce = 0; $nonce < PHP_INT_MAX; $nonce++) { 43 | 44 | $data = $this->prepareData($nonce); 45 | 46 | $hash = hash('sha256', $data); 47 | 48 | printf("\r%d: %s", $nonce, $hash); 49 | 50 | if (substr($hash, 0, $condlength) === $condition) { 51 | $found = true; 52 | break; 53 | } 54 | } 55 | 56 | print("\n\n"); 57 | 58 | if ($found) { 59 | $this->nonce = $nonce; 60 | $this->hash = $hash; 61 | } 62 | 63 | return $found; 64 | } 65 | 66 | public function validate() 67 | { 68 | $condition = '0000'; 69 | $condlength = strlen($condition); 70 | 71 | $data = $this->prepareData($this->nonce); 72 | 73 | return substr(hash('sha256', $data), 0, $condlength) === $condition; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /blockchain.php: -------------------------------------------------------------------------------- 1 | _db = new CuteDB(); 17 | 18 | if (!$this->_db->open(Blockchain::dbFile)) { 19 | exit("Failed to create/open blockchian database"); 20 | } 21 | 22 | $this->_lastHash = $this->_db->get(Blockchain::lastHashField); 23 | if (!$this->_lastHash) { 24 | $block = new Block('', 'Genesis Block'); 25 | $hash = $block->getBlockHash(); 26 | $this->_db->set($hash, serialize($block)); 27 | $this->_db->set(Blockchain::lastHashField, $hash); 28 | $this->_lastHash = $hash; 29 | } 30 | } 31 | 32 | public function addBlock($data) 33 | { 34 | $newBlock = new Block($this->_lastHash, $data); 35 | 36 | $hash = $newBlock->getBlockHash(); 37 | 38 | $this->_db->set($hash, serialize($newBlock)); 39 | $this->_db->set(Blockchain::lastHashField, $hash); 40 | 41 | $this->_lastHash = $hash; 42 | } 43 | 44 | public function printBlockchain() 45 | { 46 | $lastHash = $this->_lastHash; 47 | 48 | while (true) { 49 | $block = $this->_db->get($lastHash); 50 | if (!$block) { 51 | break; 52 | } 53 | 54 | $block = unserialize($block); 55 | 56 | printf("PrevHash: %s\n", $block->prevHash); 57 | printf("Hash: %s\n", $block->hash); 58 | printf("Data: %s\n", $block->data); 59 | printf("Nonce: %s\n\n\n", $block->nonce); 60 | 61 | $lastHash = $block->prevHash; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CuteDB.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | class CuteDB 10 | { 11 | const CUTEDB_ENTRIES = 1048576; 12 | const CUTEDB_VERSION = 1; 13 | const CUTEDB_HEADER_SIZE = 12; 14 | 15 | private $_idxfile = null; 16 | private $_datfile = null; 17 | 18 | private function initDB() 19 | { 20 | /** 21 | * DB index file header: 22 | * 4 bytes for "CUTE" 23 | * 4 bytes for version 24 | * 4 bytes for hash bucket entries 25 | */ 26 | $header = pack('C4L2', 67, 85, 84, 69, 27 | CuteDB::CUTEDB_VERSION, 28 | CuteDB::CUTEDB_ENTRIES); 29 | 30 | if (fwrite($this->_idxfile, $header) != CuteDB::CUTEDB_HEADER_SIZE) { 31 | return false; 32 | } 33 | 34 | $block = pack('L128', 35 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 43 | 44 | $blockSize = strlen($block); 45 | $bucketSize = CuteDB::CUTEDB_ENTRIES * 4; 46 | 47 | for ($i = 0; $i < $bucketSize; $i += $blockSize) { 48 | if (fwrite($this->_idxfile, $block) != $blockSize) { 49 | return false; 50 | } 51 | } 52 | 53 | return true; 54 | } 55 | 56 | private function validateDB() 57 | { 58 | $header = fread($this->_idxfile, CuteDB::CUTEDB_HEADER_SIZE); 59 | 60 | if (!$header || strlen($header) != CuteDB::CUTEDB_HEADER_SIZE) { 61 | return false; 62 | } 63 | 64 | $sign = unpack('C4', substr($header, 0, 4)); 65 | 66 | if ($sign[1] != 67 || $sign[2] != 85 || 67 | $sign[3] != 84 || $sign[4] != 69) 68 | { 69 | return false; 70 | } 71 | 72 | $sign = unpack('L2', substr($header, 4)); 73 | 74 | if ($sign[1] != CuteDB::CUTEDB_VERSION || 75 | $sign[2] != CuteDB::CUTEDB_ENTRIES) 76 | { 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | 83 | public function open($dbname) 84 | { 85 | if ($this->_idxfile) { 86 | fclose($this->_idxfile); 87 | } 88 | 89 | if ($this->_datfile) { 90 | fclose($this->_datfile); 91 | } 92 | 93 | $idxFileName = sprintf('%s.idx', $dbname); 94 | $datFileName = sprintf('%s.dat', $dbname); 95 | 96 | $init = false; 97 | 98 | if (!file_exists($idxFileName)) { 99 | $init = true; 100 | $mode = 'w+b'; 101 | } else { 102 | $mode = 'r+b'; 103 | } 104 | 105 | $this->_idxfile = fopen($idxFileName, $mode); 106 | $this->_datfile = fopen($datFileName, $mode); 107 | 108 | if (!$this->_idxfile || !$this->_datfile) { 109 | if ($this->_idxfile) { 110 | fclose($this->_idxfile); 111 | } 112 | if ($this->_datfile) { 113 | fclose($this->_datfile); 114 | } 115 | return false; 116 | } 117 | 118 | if ($init) { 119 | return $this->initDB(); 120 | } 121 | 122 | return $this->validateDB(); 123 | } 124 | 125 | private function getIndexOffset($key) 126 | { 127 | $hash = crc32($key); 128 | if ($hash < 0) { 129 | $hash = -$hash; 130 | } 131 | 132 | $index = $hash % CuteDB::CUTEDB_ENTRIES; 133 | 134 | return CuteDB::CUTEDB_HEADER_SIZE + $index * 4; 135 | } 136 | 137 | public function set($key, $value) 138 | { 139 | $indexoffset = $this->getIndexOffset($key); 140 | 141 | if (fseek($this->_idxfile, $indexoffset, SEEK_SET) == -1) { 142 | return false; 143 | } 144 | 145 | $item = fread($this->_idxfile, 4); 146 | if (strlen($item) != 4) { 147 | return false; 148 | } 149 | 150 | $headoffset = unpack('L', $item)[1]; 151 | $curroffset = 0; 152 | 153 | $update = false; 154 | $offset = $headoffset; 155 | 156 | while ($offset) { 157 | 158 | if (fseek($this->_idxfile, $offset, SEEK_SET) == -1) { 159 | return false; 160 | } 161 | 162 | $curroffset = $offset; 163 | 164 | $item = fread($this->_idxfile, 128); 165 | if (strlen($item) != 128) { 166 | return false; 167 | } 168 | 169 | $offset = unpack('L', substr($item, 0, 4))[1]; 170 | $datoff = unpack('L', substr($item, 4, 4))[1]; 171 | $datlen = unpack('L', substr($item, 8, 4))[1]; 172 | $delete = unpack('C', substr($item, 12, 1))[1]; 173 | 174 | $cmpkey = rtrim(substr($item, 13), "\0"); 175 | 176 | if ($cmpkey == $key) { 177 | $update = true; 178 | break; 179 | } 180 | } 181 | 182 | if (!$update || $datlen < strlen($value)) { 183 | if (fseek($this->_datfile, 0, SEEK_END) == -1) { 184 | return false; 185 | } 186 | $datoff = ftell($this->_datfile); 187 | } else { 188 | if (fseek($this->_datfile, $datoff, SEEK_SET) == -1) { 189 | return false; 190 | } 191 | } 192 | 193 | $datlen = strlen($value); 194 | 195 | if (fwrite($this->_datfile, $value) != $datlen) { 196 | return false; 197 | } 198 | 199 | if ($update) { 200 | $keyItem = pack('L3C', $offset, $datoff, $datlen, 0); 201 | } else { 202 | $keyItem = pack('L3C', $headoffset, $datoff, $datlen, 0); 203 | } 204 | 205 | $keyItem .= $key; 206 | if (strlen($keyItem) > 128) { 207 | return false; 208 | } 209 | 210 | if (strlen($keyItem) < 128) { 211 | $zero = pack('x'); 212 | for ($i = 128 - strlen($keyItem); $i > 0; $i--) { 213 | $keyItem .= $zero; 214 | } 215 | } 216 | 217 | if (!$update) { 218 | if (fseek($this->_idxfile, 0, SEEK_END) == -1) { 219 | return false; 220 | } 221 | 222 | $prevoffset = ftell($this->_idxfile); 223 | 224 | if (fwrite($this->_idxfile, $keyItem) != 128) { 225 | return false; 226 | } 227 | 228 | if (fseek($this->_idxfile, $indexoffset, SEEK_SET) == -1) { 229 | return false; 230 | } 231 | 232 | if (fwrite($this->_idxfile, pack('L', $prevoffset)) != 4) { 233 | return false; 234 | } 235 | 236 | } else { 237 | if (fseek($this->_idxfile, $curroffset, SEEK_SET) == -1) { 238 | return false; 239 | } 240 | 241 | if (fwrite($this->_idxfile, $keyItem) != 128) { 242 | return false; 243 | } 244 | } 245 | 246 | return true; 247 | } 248 | 249 | public function get($key) 250 | { 251 | $indexoffset = $this->getIndexOffset($key); 252 | 253 | if (fseek($this->_idxfile, $indexoffset, SEEK_SET) == -1) { 254 | return false; 255 | } 256 | 257 | $item = fread($this->_idxfile, 4); 258 | if (strlen($item) != 4) { 259 | return false; 260 | } 261 | 262 | $found = false; 263 | $datlen = 0; 264 | $datoff = 0; 265 | $delete = 0; 266 | $offset = unpack('L', $item)[1]; 267 | 268 | while ($offset) { 269 | 270 | if (fseek($this->_idxfile, $offset, SEEK_SET) == -1) { 271 | return false; 272 | } 273 | 274 | $item = fread($this->_idxfile, 128); 275 | if (strlen($item) != 128) { 276 | return false; 277 | } 278 | 279 | $offset = unpack('L', substr($item, 0, 4))[1]; 280 | $datoff = unpack('L', substr($item, 4, 4))[1]; 281 | $datlen = unpack('L', substr($item, 8, 4))[1]; 282 | $delete = unpack('C', substr($item, 12, 1))[1]; 283 | 284 | $cmpkey = rtrim(substr($item, 13), "\0"); 285 | 286 | if ($cmpkey == $key) { 287 | $found = true; 288 | break; 289 | } 290 | } 291 | 292 | if (!$found || $delete) { 293 | return false; 294 | } 295 | 296 | if (fseek($this->_datfile, $datoff, SEEK_SET) == -1) { 297 | return false; 298 | } 299 | 300 | return fread($this->_datfile, $datlen); 301 | } 302 | 303 | public function delete($key) 304 | { 305 | $indexoffset = $this->getIndexOffset($key); 306 | 307 | if (fseek($this->_idxfile, $indexoffset, SEEK_SET) == -1) { 308 | return false; 309 | } 310 | 311 | $item = fread($this->_idxfile, 4); 312 | if (strlen($item) != 4) { 313 | return false; 314 | } 315 | 316 | $headoffset = unpack('L', $item)[1]; 317 | $curroffset = 0; 318 | 319 | $found = false; 320 | $offset = $headoffset; 321 | 322 | while ($offset) { 323 | 324 | if (fseek($this->_idxfile, $offset, SEEK_SET) == -1) { 325 | return false; 326 | } 327 | 328 | $curroffset = $offset; 329 | 330 | $item = fread($this->_idxfile, 128); 331 | if (strlen($item) != 128) { 332 | return false; 333 | } 334 | 335 | $offset = unpack('L', substr($item, 0, 4))[1]; 336 | $datoff = unpack('L', substr($item, 4, 4))[1]; 337 | $datlen = unpack('L', substr($item, 8, 4))[1]; 338 | $delete = unpack('C', substr($item, 12, 1))[1]; 339 | 340 | $cmpkey = rtrim(substr($item, 13), "\0"); 341 | 342 | if ($cmpkey == $key) { 343 | $found = true; 344 | break; 345 | } 346 | } 347 | 348 | if (!$found || $delete) { 349 | return false; 350 | } 351 | 352 | $keyItem = pack('L3C', $offset, $datoff, $datlen, 1); 353 | 354 | $keyItem .= $key; 355 | if (strlen($keyItem) > 128) { 356 | return false; 357 | } 358 | 359 | if (strlen($keyItem) < 128) { 360 | $zero = pack('x'); 361 | for ($i = 128 - strlen($keyItem); $i > 0; $i--) { 362 | $keyItem .= $zero; 363 | } 364 | } 365 | 366 | if (fseek($this->_idxfile, $curroffset, SEEK_SET) == -1) { 367 | return false; 368 | } 369 | 370 | if (fwrite($this->_idxfile, $keyItem) != 128) { 371 | return false; 372 | } 373 | 374 | return true; 375 | } 376 | } 377 | --------------------------------------------------------------------------------