PHP ile Blockchain (Blokzinciri) yazıyoruz. Bölüm 4: İşlemler 1 (Transactions)
Bu yazı ingilizceden çevrilmiştir.
PHP ile Blockchain:
Giriş
İşlemler blockchain’in kalbidir ve blockchain’in tek amacı işlemleri güvenli ve emin şekilde saklamaktır, böylece oluşturulduktan sonra kimse işlemleri değiştiremesin. Bu yazıda blockchain yapımıza işlemleri uygulayacağız. Ancak konu çok uzun olduğu için 2 kısma bölerek yazacağım. Bugün işlemlerin genel mekanizmasını yazalım. İkinci kısımda detayları işleriz.
Bu bölümde epey bir kod düzenlemesi olduğu için kaynağa göz atın:
Aslında kaşık yok
Daha önce ödeme ile ilgili bir uygulama geliştirdiyseniz muhtemelen veritabanında accounts
ve transactions
tabloları tutmuşsunuzdur. accounts
tablosu kullanıcılar ile ilgili bilgileri tutar, örneğin kişisel verileri ve hesap bilgisi gibi, transactions
tablosu ise harcama bilgilerini veya transfer bilgilerini tutar. Bitcoin blockchain’inde ise ödeme işlemleri bambaşka bir şekilde gerçekleşiyor. Şöyleki:
- Hesap (account) yok
- Balans yok
- Sabit adres yok
- Para yok
- Gönderici ve alıcı yok
Bitcoin blockchain’i herkese açık bir veritabanı olduğu için cüzdan sahipleri hakkında hiçbir kişisel veri tutmaz. Paralar kişilerin hesabında toplanmaz. İşlemler paraları birinin hesabından diğerine transfer etmez. Kişilerin toplam parasını tutan bir veri sütünü yok. Sadece işlemler var. Peki içinde ne mi var dersiniz?
Bitcoin Transaction
Bir işlem girdi ve çıktıların birleşimidir:
class Transaction
{
public $id;
/** @var array|TXInput[]
public $vIn; /** @var array|TXOutput[]
public $vOut;
}
Yeni işlemin girdileri önceki işlemlerin çıktılarını işaret eder (Ancak daha sonra değineceğimiz bir istisnai durum var). Çıktılar aslında coin değerini tutan yerdir. Aşağıdaki diagramda işlemlerin birbirine bağını görebilirsiniz.

Dikkat etmeniz gerekenler:
- Bazı çıktılar (output) herhangi bir girdiye (input) işaret etmiyor.
- Bir işlem içinde birden fazla işlemden gelen girdiler olabilir.
- Her girdi sadece bir çıktıdan işaret alır.
Makale boyunca “para”, “coin”, “harcama”, “gönderme”, “hesap” gibi terimleri kullanacağız ancak Bitcoin’de böyle kavramlar yok. İşlemler içindeki değeri bir kod ile kilitler ve sadece kilitleyen kişi tarafından açılabilir.
İşlem çıktıları
Hadi çıktılar ile başlayalım:
class TXOutput
{
public $value;
public $scriptPubKey;
}
Coin değerini saklayan aslında çıktılardır (çıktı nesnesindeki $value). Saklama derken çıktılar $sciptPubKey’de bulunan değer ile kilitlenir. Bitcoin içinde kullanılan scripting dili Script ile kilitleme ve açma mantığı tanımlanır. Script dili aslında oldukça ilkel (Hackleme ve kötü amaçlı kullanımı önlemek için kasıtlı olarak ilkel tutulmuştur) ama biz bunu detaylı şekilde ele almayalım. Detaylı şekilde öğrenmek istiyorsanız buradan inceleyin.
Bitcoin’de değer alanı, BTC’nin sayısı değil, satoshi sayısını saklar. Bir satoshi, bitcoin’in yüz milyonda biridir (0.00000001 BTC). Bu da Bitcoin’deki en küçük birimdir (kuruş gibi).
Henüz adres kısmını eklemediğimiz için şimdilik scripting kısmını gözardı edebiliriz. $scriptPubKey
herhangi bir veri tutabilir (kullanıcı uygun gördüğü bir adres).
Bu arada böyle bir scripting dilini barındırdığı için aslında Bitcoin akıllı sözleşmeler (smart contracts) için de kullanılabilir.
Çıktılarla ilgili önemli bir not da onların bölünemez olmaları. Şöyleki bir çıktı birden fazla girdiye işaret edemez. Çıktıdaki parayı kısmi şekilde kullanamazsınız. Eğer bir girdiye işaret ediyorsa, tuttuğu değerin tümünü harcadığı anlamına gelir, eğer çıktıdaki tutar istenenden fazla ise iade çıktısı ile gönderen kişinin adresine geri yüklenir. Aslında gerçek dünyadaki gibi düşünebilirsiniz; 1 liralık ödemeniz var, 5 lira verirseniz, 4 lira iade alırsınız.
İşlem girdileri
Girdiler de şöyle:
class TXInput
{
public $txId;
public $vOut;
public $scriptSig;
}
Yukarıda bahsettiğimiz gibi, her girdi bir önceki çıktıdan gelir: $txId değeri önceki işlemin id’sini tutar, $vOut ise o işlemdeki çıktının sırasını tutar. $scriptSig değeri ise çıktının $scriptPubKey değeri için veri tutar. Eğer bu veri doğru ise çıktı kilidi açılır ve yeni çıktılar oluşturulabilir. Eğer doğru değilse çıktıya referans olamaz. Herhangi biri başkasının coin’lerini harcayamaz garantisini veren de bu mekanizmadır.
Yine de adres kısmını henüz eklemediğimiz için $scriptSig değerine herhangi bir veri yazabiliriz. Sonraki makalede açık anahtar ve imza kontrolunu uygulayacağız.
Hadi özetleyelim. Çıktılar “coin”lerin saklandığı yerdir. Her bir çıktı bir kilit scripti ile tutulur. Her işlem en az bir girdi ve bir çıktı içermesi gerek. Her girdi bir önceki işlemin çıktısını referans alır ve kilit açma scriptini barındırır ($scriptSig değeri).
Peki en üstte çıktı mı var yoksa girdi mi var?
Yumurta
Bitcoin’de yumurta tavuktan önce gelir. Aslında girdi-çıktı mantığı bir nevi yumurta mı tavuktan, tavuk mu yumurtadan durumunu andırır: girdiler çıktıları üretir, çıktılar girdileri mümkün kılar. Bitcoinde ise çıktı girdiden önce gelir.
Bir bloğu kazımaya başladığınızda bir coinbase işlemi ekliyorsunuz. Coinbase işlemi önceki bir işlem çıktısına ihtiyaç duymayan özel bir işlem türüdür. Olmayan “coin”i üretir. Tavuksuz yumurtadır. İşte buna madencilerin kazandıkları ödül denir.
Bildiğiniz gibi blockchain başlangıcında bir genesis bloğu vardı. İşte bu blok ilk çıktıyı üretir. Daha önce bir işlem olmadığı için önceki çıktıya da ihtiyaç duymaz.
Hadi bir coinbase işlemi oluşturalım:
Coinbase işleminin sadece bir girdisi vardır. Bizim uygulamamızda girdimizin $txId
değeri boş (null) ve $vOut
değeri -1'dir. Ayrıca coinbase işlemi $scriptSig
değerinde script tutmaz, aksine herhangi bir değer taşıyabilir.
Bitcoinde ilk coinbase işlemini oluştururken Satoshi Nakamoto şöyle bir mesaj koymuş: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”. Buradan kendiniz de görebilirsiniz. Bu da 3 Ocak 2009 tarihindeki The Times manşetidir.
SUBSIDY
değeri ise madencilikten kazanılan tutardır. Bitcoin’de bu değer sabit değildir ve toplam blok sayısına göre üretilir. Genesis bloğunu kazıdıktan sonra 50 BTC değerinde coinbase üretilmiştir ve her 210000 blokta bu değer 2'ye bölünür. Bizim uygulamamızda ise biz bu değeri sabit tutacağız ( en azından şimdilik 😉).
Blockchain’de İşlemleri Saklamak
Bundan sonra her blok en az bir işlem içerecek ve işlemsiz blok kazınamayacak. Bu da Block
nesnesindeki $data
değerini kaldırmamız ve yerine işlemleri tutmamız anlamına gelir.
Block
nesnesindeki create metodunu ve Genesis bloğunu
oluşturduğumuz fonksiyonları da buna göre güncelleyelim.
function create(array $transactions, string $prevBlockHash)
{
$block = (new Block(time(), $transactions, $prevBlockHash));
return (new ProofOfWork($block))->run();
}
Yeni blockchain oluşturduğumuz yeri de güncelleyelim:
public function __construct(string $address)
{
if (!$this->lastHash) {
echo "Creating Genesis Block..." . PHP_EOL;
$coinbase = Transaction::newCoinbaseTX($address, self::GENESIS_COINBASE_DATA);
$genesis = Block::create([$coinbase], ''); $query = $this->db->prepare('INSERT INTO blocks (hash, block) VALUES (?, ?)');
$query->execute([$genesis->hash, json_encode($genesis)]); $this->lastHash = $genesis->hash;
}
}
Gördüğünüz gibi fonksiyon, kazancı alacak kişinin adresini alıyor.
Proof-of-Work
Proof-of-Work algoritması da blockchain’in tutarlığını ve güvenliğini garantilemek için işlemleri içermelidir. Haliyle ProofOfWork->prepare metodunu güncellememiz gerek:
private function prepare(int $nonce)
{
return $this->block->prevBlockHash .
$this->block->hashTransactions() .
dechex($this->block->timestamp) .
dechex($this->block->targetZeros) .
dechex($nonce);
}
$this->block->data
ile $this->block->hashTransactions()
metodunu değişelim:
public function hashTransactions()
{
$txHashes = '';
foreach ($this->transactions as $transaction) {
$txHashes .= $transaction->id;
}
return hash('sha256', $txHashes);
}
Yine, verilerin benzersiz temsilini bulmak için hash algoritmasını kullanıyoruz. Bir bloktaki tüm işlemlerin tek bir hash’e sahip olmasını istiyoruz. Bunun için tüm hash’leri tek satıra indirgedikten sonra hash’ini alıyoruz.
Bitcoin daha ayrıntılı bir teknik kullanıyor: bloktaki işlemleri bir Merkle ağacı olarak birleştiyor ve kök hash üretiyor, bu kök hash’i de Proof-Of-Work içinde kullanıyor. Bu yaklaşım herhangi bir işlemin bir bloğun içinde olup olmadığını hızlıca kontrol etmek için tüm işlemleri indirmeden sadece kök hash’e bakması yeterli.
Hadi herşey yolunda olduğundan emin olalım:

Güzel! İlk madencilik kazancımızı elde ettik. Peki balansı nasıl kontrol ederiz?
Harcanmamış İşlem Çıktıları
Tüm harcanmamış işlem çıktılarını (Unspent Transaction Outputs — UTXO) bulmamız gerek. Harcanmamışın anlamı işlemlerin çıktıları herhangi bir girdiye işaret etmeyenlerdir. Yukarıdaki diagramda çıktılar şöyle:
- tx0, output 1;
- tx1, output 0;
- tx3, output 0;
- tx4, output 0.
Tabiki balansı kontrol ederken tüm çıktılara ihtiyacımız yok, sadece elimizdeki anahtar ile açabildiğimiz çıktılara ihtiyacımız var (henüz anahtar kısmını uygulamadığımız için burada kullanıcının belirttiği adresi kullanacağız). Hadi İlk başta girdilerin ve çıktıların açma-kapama metodlarını yazalım.
// TXInput.php
public function canUnlockOutputWith(string $unlockingData)
{
return $this->scriptSig === $unlockingData;
}// TXOutput.php
public function canBeUnlockedWith(string $unlockingData)
{
$this->scriptPubKey === $unlockingData;
}
Burada aslında script değeri ile $unlockingData
değerini karşılaştırıyoruz. Bu metodlar sonraki makalelerde açık anahtar bazlı adres kısmını uyguladığımızda daha da gelişecektir.
Sonraki adım — harcanmamış çıktı barındıran işlemleri bulma — biraz meşakkatli:
Her işlem bir blok içinde olduğundan, tüm blockchain’deki blokları kontrol etmek zorundayız. Çıktılar ile başlıyoruz:
if ($output->canBeUnlockedWith($address)) {
$unspentTXs[] = $transaction;
}
Eğer bir çıktı aradığımız adres ile kilitlendi ise ihtiyacımız olan işlem anlamına gelir. Ama onu almadan önce, daha önce bir girdiye referans olmuş mu diye kontrol etmemiz gerek:
if (isset($spentTXOs[$transaction->id])) {
foreach ($spentTXOs[$transaction->id] as $outputId) {
if ($outputKey === $outputId) {
continue 2;
}
}
}
Herhangi bir girdiye referans olan çıktıları es geçiyoruz (tuttukları coinler başka çıktılara taşınmış olduğu için onları sayamayız). Çıktıları kontrol ettikten sonra verilen adres ile kilidi açılabilen tüm girdileri topluyoruz (bu coinbase işlemine uygulanmaz çünkü onun girdisi yok.):
if ($transaction->isCoinbase() === false) {
foreach ($transaction->vIn as $input) {
if ($input->canUnlockOutputWith($address)) {
$spentTXOs[$input->txId][] = $input->vOut;
}
}
}
Yazdığımız fonksiyon harcanmamış çıktısı olan işlem listesini dönüyor. Verilen adresin balansını hesaplamamız için bu işlemleri aldıktan sonra sadece harcanmamış çıktıları dönen bir fonksiyona daha ihtiyacımız var:
public function findUTXO(string $address)
{
$UTXOs = [];
$unspentTransactions = $this->findUnspentTransactions($address);
foreach ($unspentTransactions as $transaction) {
foreach ($transaction->vOut as $output) {
if ($output->canBeUnlockedWith($address)) {
$UTXOs[] = $output;
}
}
}
return $UTXOs;
}
Bu kadar! Şimdi de getbalance komutumuzu ekleyebiliriz:
function getBalance(string $address)
{
$balance = 0;
foreach ($this->blockchain->findUTXO($address) as $output) {
$balance += $output->value;
}
echo "Balance of {$address}: {$balance}" . PHP_EOL;
}
Verilen adres ile kilitlenen tüm harcanmamış işlem çıktılarındaki değer
satırını topladığımızda adresin balansını verir.
Hadi genesis bloğunu kazıdıktan sonraki balansımızı kontrol edelim:

Bu bizim yukarıda kazıdığımız ilk paramız!
Para Gönderme
Şimdi de başkasına para göndermek istiyoruz. Bunun için yeni bir işlem yaratmamız gerek, bloğa eklememiz gerek ve bloğu kazımamız gerek. Şu ana dek sadece coinbase işlemini (özel bir işlem türü) uygulayabildik. Şimdi de genel işleme ihtiyacımız var:
Yeni çıktılar üretmeden önce harcanmamış çıktıları bulup yeterli miktarda para içerdiğini kontrol etmemiz gerek. $this->findSpendableOutputs
metodu da bu işi yapar. Sonrasında bulunan her çıktı için buna işaret eden bir girdi üretilir. Sonrasında da 2 çıktı üretilir:
- İlki parayı alacak olan kişinin adresi ile kilitlenmiş olacak. Bu da aslında paranın başka adrese transfer olmasıdır.
- Bu da gönderen kişinin adresi ile kilitlenmiş olacak, para üstüdür. Ancak harcanmamış çıktılar gönderilecek miktardan fazla ise üretilir. Hatırlayalım: çıktılar bölünebilir değiller.
$this->findSpendableOutputs
metodu daha önce yazdığımız $this->findUnspentTransactions
metoduna bağlıdır:
Fonksiyon tüm harcanmamış işlemler üzerinde gezer ve tutarlarını toplar. Toplam tutar transfer etmek istediğimiz tutar ile eşit veya tutardan fazla olduğunda döngüden çıkar. İhtiyacımızdan fazlasını almak istemiyoruz. Toplam tutar ve çıktıların sırasını işlem hash’lerine göre gruplayıp geri gönderir.
Şimdi de Blockchain->mineBlock
metodunu güncelleyelim:
public function mineBlock(array $transactions)
{
$block = Block::create($transactions, $this->lastHash);
$query = $this->db->prepare('INSERT INTO blocks (hash, block) VALUES (?, ?)');
$query->execute([$block->hash, json_encode($block)]);
$this->lastHash = $block->hash;
}
Ve son olarak da send
metodumuzu uygulayalım:
private function send(string $from, string $to, int $amount)
{
$this->blockchain = Blockchain::getInstance($from);
$transaction = $this->blockchain->newUTXOTransaction($from, $to, $amount);
$this->blockchain->mineBlock([$transaction]);
echo "Success" . PHP_EOL;
}
Para göndermek yeni bir işlem üretme ve bunu blockchain’e kazıma işlemi ile ekleme anlamına geliyor. Ama Bitcoinde bu hızlı bir şekilde olmuyor (bizim yaptığımız gibi değil). Aksine tüm yeni işlemleri hafıza havuzuna (memory pool — mempool) alıyor. Yeni bir blok kazımaya hazır olan bir madenci mempool’daki tüm işlemleri alır (max 1MB boyut şartı ile) ve yeni bir aday blok üretir. İşlemlerin onaylı sayılması için blok içinde kazınması ve blokchain’e eklenmesi gerekiyor.
Hadi para gönderme uygulamamız çalıştığına emin olalım:

Güzel. Şimdi de başka bir kişiye daha para gönderelim ve sonrasında hesabında olmayan paradan fazlasını göndermeye çalışalım.

Sonuç
Oh be! Kolay olmadı ama artık işlemlerimiz var! Ama hala Bitcoin benzeri bir kripto para blockchain’indeki temel özelliklerden yoksun:
- Adresler. Henüz gerçek özel anahtarlı adreslere sahip değiliz.
- Kazanç. Şu anda blok kazımak hiç de kârlı değil.
- UTXO listesi. Balansı öğrenmek için tüm blokchain’i gezmek zorundayız. Bu da çok fazla blok olduğunda uzun sürebilir. Ayrıca işlemleri de onaylamak istediğimizde epey bir zaman alacak. UTXO listesi de bu tarz sorunları ortadan kaldırmak ve işlemlerle ilgili operasyonları hızlandırmak amacı ile ortaya çıkmıştır.
- Mempool. İşlemlerin bloklara eklenmeden önce tutulduğu yerdir. Bizim uygulamamızda bir blok içinde sadece 1 tane işlem var ve bu da oldukça verimsizdir.