Bu yazıda, Bitcoin'de hafif istemcilere olan ihtiyacı ve "kompakt blok filtrelerin" bu ihtiyaca neden "Bloom filtrelerinden" daha iyi hizmet ettiğini kısaca açıklayacağım. Daha sonra, bir test ağında böyle bir filtre oluşturmayı adım adım izleyerek, yoğun blok filtrenin nasıl çalıştığını derinlemesine açıklayacağım.
Blok filtresinin anlamı
Bir bitcoin hafif istemcisi (bitcoin hafif yazılımı), amacı bitcoin blok zincirini depolamak değil, bir bitcoin cüzdanını desteklemek olan bir yazılımdır. Bu, işlemleri ağa yayınlayabilmesi gerektiği anlamına gelir, ancak en önemlisi, bu cüzdanla ilgili zincirden yeni işlemler bulabilmelidir. Bu tür ilgili işlemlerin iki türü vardır: bu cüzdana para göndermek (bu cüzdanın adresi için yeni bir çıktı oluşturmak) ve bu cüzdan tarafından kontrol edilen UTXO'lardan birini harcamak.
Bloom filtrelerinin nesi var?
BIP 158'den önce, hafif istemciler için en yaygın yaklaşım, BIP 371'de açıklanan Bloom filtresiydi. Bir Bloom filtresinin modeli, ilgilendiğiniz tüm veri nesnelerini (örneğin, harcanan ve oluşturulan UTXO'ların komut dosyası ortak anahtarları) almanız, bunları birer birer birden çok kez hashlemeniz ve her sonucu In "çiçeklenme filtresi" adı verilen bir bitmap. Bu filtre, ilgilendiğiniz şeyi temsil eder. Ardından, bu filtreyi güvendiğiniz bir Bitcoin düğümüne gönderirsiniz ve onlardan size bu filtreyle eşleşen her şeyi göndermelerini istersiniz.
Sorun şu ki, bu işlem çok özel değil, çünkü filtreyi kabul eden Bitcoin düğümüne hala bazı bilgiler sızdırıyorsunuz. İlgilendiğiniz fırsatları ve hiç ilgilenmediğiniz fırsatları bilebilirler, ayrıca size filtrelerle eşleşen şeyleri gönderemezler. Bu nedenle, hafif müşteriler için ideal değildir. Ancak aynı zamanda hafif istemcilere hizmet veren Bitcoin düğümleri için ideal değildir. Her filtre gönderdiğinizde, ilgili bloğu diskten yüklemeleri ve ardından filtrenizle eşleşen herhangi bir işlem olup olmadığını belirlemeleri gerekir. Sahte filtreler göndermeye devam edin ve onları bombalayabilirsiniz - temelde bir DOS saldırısı. Filtre oluşturmak çok az enerji gerektirir, ancak böyle bir talebe yanıt vermek çok fazla enerji gerektirir.
Yoğun blok filtresi
Tamam, o zaman ihtiyacımız olan özellikler:
Daha güçlü gizlilik
İstemci-sunucu yük asimetrisi daha azdır. Yani, sunucu tarafında yapılacak daha az iş olmalıdır.
Daha az güven. Hafif istemcilerin, sunucunun ilgili işlemleri döndürmemesi konusunda endişelenmesine gerek yoktur.
Sunucu (tam düğüm), yoğun bir blok filtresi kullanarak, bu bloktaki tüm veri nesneleri dahil olmak üzere her blok için deterministik bir filtre oluşturacaktır. Bu filtrenin yalnızca bir kez hesaplanması ve sonsuza kadar saklanması gerekir. Hafif bir istemci belirli bir blok için bir filtre isterse, burada bir asimetri yoktur - çünkü sunucunun istekte bulunan istemciden daha fazla iş yapmasına gerek yoktur. Light client, tutarlı olduklarından emin olmak için tüm bloğu indirmek üzere birden fazla bilgi kaynağı da seçebilir; ayrıca, light client, sunucu tarafından sağlanan filtrenin doğru olup olmadığını kontrol etmek için her zaman tüm bloğu indirebilir.Doğru (ilgili blokla eşleşir) içerik). Diğer bir faydası da bu şekilde daha özel olmasıdır. Hafif istemcinin artık sunucuya istediği verinin parmak izini göndermesine gerek yoktur. Hafif bir müşterinin etkinliğini analiz etmek de daha zor hale gelir. Light client sunucudan böyle bir filtreyi aldıktan sonra filtrenin istediği veriyi içerip içermediğini kontrol eder ve eğer öyleyse light client tüm bloğu ister. Hafif istemcilere bu şekilde hizmet verebilmek için, tam düğümlerin bu filtreleri sürdürmesi gerektiği ve hafif istemcilerin de birkaç filtreyi sürdürmek isteyebileceği, bu nedenle filtreleri mümkün olduğunca hafif tutun Miktar çok önemlidir ( bu nedenle "yoğun blok filtresi" olarak adlandırılır).
Çok iyi! Şimdi nadir şeylere giriyoruz. Böyle bir filtre nasıl oluşturulur? Nasıl görünüyor?
İhtiyaçlarımız nelerdir?
Filtreye belirli bir nesnenin parmak izini koymak istiyoruz, böylece müşteriler bir bloğun kendileriyle ilgili bir şey içerip içermediğini kontrol etmek istediklerinde, tüm nesneleri sıralayabilir ve bir filtrenin bu nesnelerle eşleşip eşleşmeyeceğini kontrol edebilirler.
Bu filtrenin olabildiğince küçük olmasını istiyoruz.
Özünde istediğimiz, bloktan çok daha küçük bir hacme sahip bir bloğun bazı bilgilerini özetleyebilecek bir şey.
Filtrede bulunan temel bilgiler şunlardır: her işlemin her girişinin komut dosyası ortak anahtarı (yani, harcanan her UTXO'nun komut dosyası genel anahtarı) ve her işlemin her çıkışının komut dosyası genel anahtarı. Temel olarak şöyle:
nesneler = {spk1, spk2, spk3, spk4, ..., spkN} // N komut dosyası ortak anahtarlarının listesi
Teknik olarak burada durabiliriz - tam da böyle bir komut dosyası ortak anahtarları listesi de filtremiz olarak işlev görebilir. Bu, hafif istemciler tarafından ihtiyaç duyulan bilgileri içeren bir bloğun yoğunlaştırılmış bir sürümüdür. Böyle bir liste ile hafif bir müşteri, bir blokta ilgili bir işlem olup olmadığını %100 doğrulayabilir. Ancak böyle bir liste çok büyük. Bu nedenle, sonraki adımlarımızın tümü bu listeyi olabildiğince kompakt hale getirmek içindir. İşlerin ilginçleştiği yer burasıdır.
İlk olarak, her nesneyi belirli bir aralıkta bir sayıya dönüştürebilir ve her nesne numarasını bu aralık içinde eşit olarak dağıtabiliriz. Diyelim ki 10 nesnemiz var (N = 10) ve sonra her nesneyi bir sayıya dönüştüren bir tür fonksiyonumuz var. Diyelim ki [0, 10] aralığını seçtik (çünkü 10 nesnemiz var). Şimdi, "hash to number" fonksiyonumuz her nesneyi girdi olarak alacak ve sırasıyla [0, 10] boyutunda bir sayı üretecektir. Sayılar bu aralıkta eşit olarak dağıtılır. Bu, sıralamadan sonra şöyle bir grafik elde edeceğimiz anlamına gelir (çok, çok ideal bir durumda):
Her şeyden önce, bu harika çünkü her bir nesnenin parmak izinin boyutunu önemli ölçüde azalttık. Artık her nesne sadece bir sayıdır. Böylece, yeni filtremiz şöyle görünür:
sayılar := {1,2,3,4,5,6,7,8,9,10}
Hafif bir istemci, aradığı bir nesnenin filtreyle eşleşip eşleşmediğini bilmek umuduyla böyle bir filtre indirir. Sadece ilgilendikleri nesneyi alırlar ve sonucun bu filtrede olup olmadığını görmek için aynı "karma sayı" işlevini çalıştırırlar. Sorun nerede? Sorun şu ki, filtredeki sayılar zaten o boşluktaki her olasılığı kapsıyor! Bu, hafif istemcinin hangi nesneleri önemsediği önemli değil, ortaya çıkan sayısal sonucun bu filtreyle eşleşmesi gerektiği anlamına gelir. Yani bu filtrenin "yanlış pozitiflik oranı" 1'dir (Çevirmenin Notu: Buradaki "yanlış pozitif", bloğun içermeyebileceği anlamına gelir. Filtrenin elde ettiği sonuç evet'tir). Bu pek iyi değil. Ayrıca, filtreyi oluşturmak için verileri sıkıştırma sürecinde çok fazla bilgi kaybettiğimizi de gösteriyor. Daha yüksek bir yanlış pozitif oranına ihtiyacımız var. Ardından, yanlış pozitif oranın 5 olmasını istediğimizi varsayalım. Ardından, bloktaki nesneleri [0, 50] aralığındaki sayılarla eşit şekilde eşleştirebiliriz:
Böyle daha iyi görünüyor. Eğer bir müşteriysem ve böyle bir filtre indirirsem, önemsediğim bir nesnenin ilgili blokta olup olmadığını filtre aracılığıyla kontrol etmem gerekir ve filtrenin blokta değil de evet deme olasılığı (yanlış pozitif) 0'a düşer. 1/5. Harika, şimdi 10 nesneyi 0 ile 50 arasındaki sayılara eşliyoruz. Bu yeni sayı listesi bizim filtremizdir. Yine, her an durabiliriz, ancak onları daha da sıkıştırabiliriz!
Artık [0, 50] aralığında eşit olarak dağıtılan sıralı bir sayı listemiz var. Bu listede 10 numara olduğunu biliyoruz. Bu, bu sıralı sayı listesindeki iki bitişik sayı arasındaki farkın büyük olasılıkla 5 olduğu sonucuna varabiliriz anlamına gelir. Genel olarak, N nesnemiz varsa ve yanlış pozitif M oranı istiyorsak, o zaman alanın boyutu N * M olmalıdır. Yani, bu sayıların boyutu 0 ile N * M arasında olmalıdır, ancak (sıralamadan sonra) kontrol edilen iki bitişik sayının enterpolasyon olasılığı M'dir. Ve M, N * M alanındaki sayıdan daha küçük saklanmalıdır. Böylece, her bir sayıyı saklamak yerine, iki bitişik sayı arasındaki farkı saklayabiliriz. Yukarıdaki örnekte bu, [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50] yerine [0, 5, 5, 5, 5] depoladığımız anlamına gelir. , 5, 5, 5, 5, 5], buradan ilk listeyi yeniden oluşturmak önemsizdir. Küçültürseniz, 50 sayısını depolamak, 5 sayısını depolamaktan açıkça daha fazla yer kaplar. Ama neden orada dursun? Biraz daha sıkıştıralım!
Bir sonraki adım, "Golomb-Rice kodlamasını" kullanmaktır. Bu kodlama yöntemi, tablodaki her sayının belirli bir sayıya çok yakın olduğu bir listeyi iyi kodlayabilir. Ve listemiz tam da bu kadar! Listemizdeki sayıların her birinin 5'e yakın olması muhtemeldir (veya M olan yanlış pozitif oranımıza yakındır), bu nedenle her sayının M ile bölümünü alın (her sayıyı 5'e bölün ve kalanı almayın), büyük olasılıkla 0 (sayı 5'ten biraz küçükse) veya 1 (sayı 5'ten biraz büyükse) olacaktır. Bölümün 2, 3 vb. olması da mümkündür, ancak olasılık çok daha küçüktür. Harika! Dolayısıyla bundan faydalanabiliriz: daha büyük, ancak daha az olası bölümleri kodlamak için daha fazla bitcoin kullanırken, daha küçük bölümleri kodlamak için mümkün olduğunca az bit kullanacağız. Ardından, her zaman [0, M - 1] (bu durumda [0, 4]) aralığında olacak olan kalanı da kodlamamız gerekir (çünkü tüm listeyi tam olarak yeniden oluşturabilmek istiyoruz). Kodlayıcı için aşağıdaki eşlemeleri kullanıyoruz:
Yukarıdaki eşlemenin anlaşılması kolaydır: 1'lerin sayısı kodlamak istediğimiz bölümü, 0 ise bölüm kodlamasının sonunu gösterir. Bu nedenle, listemizdeki her sayı için, bölümü kodlamak için yukarıdaki tabloyu kullanabilir ve ardından, M-1'in maksimum değerini temsil edebilen bir bittruck kullanarak kalanı ikili sayıya dönüştürebiliriz. Burada kalan 4'tür ve sadece 3 bit gereklidir. Örneğimiz için olası kalanları ve bunların kodlamalarını gösteren bir tablo:
Yani, ideal olarak [0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] listemiz şu şekilde kodlanacaktır:
Daha gerçekçi bir duruma geçmeden önce, bu filtre ile orijinal listemizi kurtarabilir miyiz bir bakalım.
Şimdi elimizde "0000100001000010000100001000010000100001000010000" var. Golomb-Rice kodunu biliyoruz ve M'nin 5 olduğunu biliyoruz (çünkü bu, bu filtre yapısını kullanan herkes tarafından bilinen, herkesin bildiği bir bilgi olacaktır). M'nin 5 olduğunu bildiğimiz için, kalanı kodlamak için 3 bit olacağını biliyoruz. Böylece, bu filtreyi aşağıdaki "bölüm-kalan" dizi listesine dönüştürebiliriz:
Pekala canım, bloklardan nasıl böyle bir filtre oluşturabileceğimize bakalım.
Burada kullanılan kodun tamamı bu github deposunda bulunabilir. Burada sadece bazı sözde kod parçacıkları göstereceğim. Bu kodun çekirdeği, bitcoin istemcileri tarafından bitcoind'i ve ilgili blokları çağırmak için kullanılabilen, buildFilter adlı bir işlevdir. İşlev şöyle görünür:
func buildFilter(bc *bitcoind.Bitcoind, blok bitcoind.Block) ([]byte, error) {
// 1, bloktan bir filtreye eklemek istediğimiz tüm nesneleri toplar
// 2 Bu nesneleri sayılara dönüştürün ve sıralayın
// 3 Sıralı numara listesindeki iki bitişik sayı arasındaki farkı bulun
// 4 Bu farklılıkları kodlamak için Golomb-Rice kodlamasını kullanın
}
Bu nedenle, ilk adım, filtreye eklemek istediğimiz bloktaki tüm nesneleri toplamaktır. BIP'den başlayarak, bu nesnelerin tüm kullanılmış betik ortak anahtarlarını ve her çıktı betiği genel anahtarını içerdiğini biliyoruz. BIP ayrıca bazı ek kurallar da belirler: madeni para tabanı işlemine girişleri atlarız (hiç girdisi olmadığı için mantıklı değildir) ve tüm OP_RETURN çıktılarını geçeriz. Ayrıca verileri tekilleştiriyoruz. İki özdeş betik ortak anahtarı varsa, bunu filtreye yalnızca bir kez ekleriz.
// filtreye eklemek istediğimiz nesnelerin listesi
// Tüm harcanan betik ortak anahtarlarını ve her çıktı betiği ortak anahtarını dahil edin
// Yinelenen komut dosyası ortak anahtarlarını kaldıran bir "harita" kullanıyoruz
nesneler := make(harita [string] yapı{})
// bloktaki her işlemi kolaylaştır
i için, tx := aralık bloğu.Tx {
// Her çıktı için betik ortak anahtarı ekleyin
// nesne listemize
_ için, txOut := aralık tx.Vout {
PubKey := txOut.PubKey
eğer len(PubKey) == 0 {
devam etmek
}
// Çıktı OP_RETURN (0x6a) ise atla
eğer spk [0] == 0x6a {
devam etmek
}
nesneler [skpStr] = yapı{}{}
}
// coinbase işleminin girişini eklemeyin
eğer ben == 0 {
devam etmek
}
// Her girdi için, komut dosyasının genel anahtarını alın
_ için, txIn := aralık tx.Vin {
öncekiTx, hata := bc.GetRawTransaction(txIn.Txid)
eğer hata != sıfır {
dönüş sıfır, hata
}
PubKey := öncekiTx.Vout[txIn.Vout].PubKey
eğer len(PubKey) == 0 {
devam etmek
}
nesneler [spkStr] = yapı{}{}
}
}
Tamam, şimdi tüm nesneler toplandı. Şimdi, N değişkenini nesne grafiğinin uzunluğu olarak tanımlıyoruz. Burada, N 85'tir.
Bir sonraki adım, nesnemizi bir aralıkta düzgün dağılmış sayılara dönüştürmektir. Yine, aralık, ne kadar yüksek bir yanlış pozitif oranı istediğinize bağlıdır. BIP158, M sabitini 784931 olarak tanımlar. Bu, yanlış pozitif olasılığının 784931'de 1 olduğu anlamına gelir. Tıpkı daha önce yaptığımız gibi, M çarpı N yanlış pozitif oranını alıp tüm sayıların dağılım aralığını elde ederiz. Bu aralığı F, yani F = M * N olarak tanımlarız. Burada F = 66719135. Nesneleri sayılara eşleyen işlevlerin ayrıntılarına girmeyeceğim (ayrıntıları yukarıda bağlantısı verilen github deposunda bulabilirsiniz). Bilmeniz gereken tek şey, girdi olarak nesneyi, F sabitini (nesnenin eşlenmesi gereken kapsamı tanımlar) ve bir anahtarı (blok karması) almasıdır. Tüm sayılara sahip olduğumuzda, listeyi artan düzende sıralarız ve ardından, sıralanan sayılar listesindeki iki bitişik sayı arasındaki farkı tutan farklar adı verilen yeni bir liste oluştururuz.
sayılar := make([]uint64, 0, N)
// Tüm nesneleri yineleyin ve bunları [0, F] aralığında eşit olarak dağıtılan sayılara dönüştürün
// ve bu numaraları sayı listesi olarak sakla
o için := menzil nesneleri {
// Verilen anahtarı, maksimum sayıyı (F) ve nesne baytlarını (o) kullanarak,
// nesneyi 0 ile F arasında bir sayıya dönüştür.
v := convertToNumber(b, F, key)
sayılar = ekle(sayılar, v)
}
// sayı listesini sırala
sort.Slice(sayılar, func(i, j int) bool { dönüş sayıları [i] < sayılar [j] })
// Sayı listesini farklılıklar listesine dönüştürün
farklar := make([]uint64, N)
i için, num := aralık sayıları {
eğer ben == 0 {
farklılıklar [i] = sayı
devam etmek
}
farklılıklar [i] = sayı - sayılar[i-1]
}
Harika! İşte sayıların ve farklılıkların bir listesini gösteren bir resim.
Uzaklaştırdıkça, 85 sayı alan boyunca eşit olarak dağılır! Yani farklar listesindeki değerler de çok küçük.
Son adım, Golomb-Rice kodlamasını kullanarak farklılıklar listesini kodlamaktır. Önceki açıklamadan, her bir farkı en olası değere bölmemiz ve ardından bölümü ve kalanı kodlamamız gerektiğini hatırlayın. Bir önceki örneğimizde en olası değerin M olduğunu ve kalanın [0, M] arasında olacağını söylemiştim. Ancak BIP158, son filtre boyutunu gerçekten en aza indiren parametre olmayan 2'yi bulduğumuz için bunu yapmaz. Yani M kullanmak yerine yeni bir P sabiti tanımlıyoruz ve Golomb-Rice parametresi olarak 2^P kullanıyoruz. P, 19 olarak tanımlanır. Bu, bölümü ve kalanı elde etmek için her bir farkı 2^19'a böldüğümüz ve kalanın 19 bit ile ikili olarak kodlandığı anlamına gelir.
filtre := bstream.NewBStreamWriter(0)
// Farklar listesindeki her değer için bölümü ve kalanı 2^P'ye bölerek elde edin.
_, d için := aralık farkları {
q := matematik.Kat(float64(d)/math.Exp2(float64(P))))
r := d - uint64(math.Exp2(float64(P))*q)
// Kodlayıcı
i için := 0; ben < int(q); ben++ {
filter.WriteBit(doğru)
}
filter.WriteBit(yanlış)
filter.WriteBits(r, P)
}
İlk iki bayt dışında bitcoind'de aldığımız filtrenin tamamen aynısı! İlk 2 baytta neden bir fark var? Çünkü BIP, N değerinin CompactSize formatında kodlanması ve alıcı tarafından çözülebilmesi için filtrenin önünde görünmesi gerektiğini söylüyor. Bu tarafından yapılır:
fd := filtre.Bytes()
arabellek baytları.Buffer
buffer.Grow(wire.IntSerializeSize(uint64(N)) + len(fd))
hata = wire.WriteInt(&buffer, 0, uint64(N))
eğer hata != sıfır {
dönüş sıfır, hata
}
_, hata = tampon.Yaz(fd)
eğer hata != sıfır {
dönüş sıfır, hata
}
Şimdi filtreyi tekrar yazdırırsak, bunun tam olarak bitcoind'in sonucuyla aynı olduğunu görürüz:
Ancak kişisel anlayışım, filtreye N eklemeye gerek olmadığı yönünde. P'nin değerini biliyorsanız, N'nin değerini de bulabilirsiniz. Bakalım ilk filtrenin çıktısını kullanarak orijinal sayı listesini geri yükleyebilir miyiz:
b := bstream.NewBStreamReader(filtre)
(
sayılar []uint64
öncekiNum uint64
)
için {
// Bayt dizisinden bölümü okuyun. Karşılaşılan ilk 0, bölümün sonunu gösterir
// 0 ile karşılaşmadan önce, 1 sayısı bölümü temsil eder
q uint64
c, hata := b.ReadBit()
eğer hata != sıfır {
dönüş hatası
}
için {
s++
c, hata = b.ReadBit()
eğer hatalar.Is(err, io.EOF) {
kırmak
} else if err != nil {
dönüş hatası
}
}
// Sonraki P bitleri kalanı kodlar
r, hata := b.ReadBits(P)
eğer hatalar.Is(err, io.EOF) {
kırmak
} else if err != nil {
dönüş hatası
}
n := q*uint64(math.Exp2(float64(P)))) + r
sayı := n + öncekiNum
sayılar = ekle(sayılar, sayı)
öncekiNum = sayı
}
fmt.Println(sayılar)
Yukarıdaki işlem aynı sayı listesini üretir, bu yüzden onu N'yi bilmeden yeniden oluşturabiliriz. Bu yüzden, filtreye N eklemenin mantığının ne olduğundan emin değilim. N'nin neden dahil edilmesi gerektiğini biliyorsanız, lütfen bana bildirin!
Okuduğunuz için teşekkürler!
View Original
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
BIP 158 Yoğun Blok Filtre Açıklamalı
kaydeden Elle Mouton
Bu yazıda, Bitcoin'de hafif istemcilere olan ihtiyacı ve "kompakt blok filtrelerin" bu ihtiyaca neden "Bloom filtrelerinden" daha iyi hizmet ettiğini kısaca açıklayacağım. Daha sonra, bir test ağında böyle bir filtre oluşturmayı adım adım izleyerek, yoğun blok filtrenin nasıl çalıştığını derinlemesine açıklayacağım.
Blok filtresinin anlamı
Bir bitcoin hafif istemcisi (bitcoin hafif yazılımı), amacı bitcoin blok zincirini depolamak değil, bir bitcoin cüzdanını desteklemek olan bir yazılımdır. Bu, işlemleri ağa yayınlayabilmesi gerektiği anlamına gelir, ancak en önemlisi, bu cüzdanla ilgili zincirden yeni işlemler bulabilmelidir. Bu tür ilgili işlemlerin iki türü vardır: bu cüzdana para göndermek (bu cüzdanın adresi için yeni bir çıktı oluşturmak) ve bu cüzdan tarafından kontrol edilen UTXO'lardan birini harcamak.
Bloom filtrelerinin nesi var?
BIP 158'den önce, hafif istemciler için en yaygın yaklaşım, BIP 371'de açıklanan Bloom filtresiydi. Bir Bloom filtresinin modeli, ilgilendiğiniz tüm veri nesnelerini (örneğin, harcanan ve oluşturulan UTXO'ların komut dosyası ortak anahtarları) almanız, bunları birer birer birden çok kez hashlemeniz ve her sonucu In "çiçeklenme filtresi" adı verilen bir bitmap. Bu filtre, ilgilendiğiniz şeyi temsil eder. Ardından, bu filtreyi güvendiğiniz bir Bitcoin düğümüne gönderirsiniz ve onlardan size bu filtreyle eşleşen her şeyi göndermelerini istersiniz.
Sorun şu ki, bu işlem çok özel değil, çünkü filtreyi kabul eden Bitcoin düğümüne hala bazı bilgiler sızdırıyorsunuz. İlgilendiğiniz fırsatları ve hiç ilgilenmediğiniz fırsatları bilebilirler, ayrıca size filtrelerle eşleşen şeyleri gönderemezler. Bu nedenle, hafif müşteriler için ideal değildir. Ancak aynı zamanda hafif istemcilere hizmet veren Bitcoin düğümleri için ideal değildir. Her filtre gönderdiğinizde, ilgili bloğu diskten yüklemeleri ve ardından filtrenizle eşleşen herhangi bir işlem olup olmadığını belirlemeleri gerekir. Sahte filtreler göndermeye devam edin ve onları bombalayabilirsiniz - temelde bir DOS saldırısı. Filtre oluşturmak çok az enerji gerektirir, ancak böyle bir talebe yanıt vermek çok fazla enerji gerektirir.
Yoğun blok filtresi
Tamam, o zaman ihtiyacımız olan özellikler:
Sunucu (tam düğüm), yoğun bir blok filtresi kullanarak, bu bloktaki tüm veri nesneleri dahil olmak üzere her blok için deterministik bir filtre oluşturacaktır. Bu filtrenin yalnızca bir kez hesaplanması ve sonsuza kadar saklanması gerekir. Hafif bir istemci belirli bir blok için bir filtre isterse, burada bir asimetri yoktur - çünkü sunucunun istekte bulunan istemciden daha fazla iş yapmasına gerek yoktur. Light client, tutarlı olduklarından emin olmak için tüm bloğu indirmek üzere birden fazla bilgi kaynağı da seçebilir; ayrıca, light client, sunucu tarafından sağlanan filtrenin doğru olup olmadığını kontrol etmek için her zaman tüm bloğu indirebilir.Doğru (ilgili blokla eşleşir) içerik). Diğer bir faydası da bu şekilde daha özel olmasıdır. Hafif istemcinin artık sunucuya istediği verinin parmak izini göndermesine gerek yoktur. Hafif bir müşterinin etkinliğini analiz etmek de daha zor hale gelir. Light client sunucudan böyle bir filtreyi aldıktan sonra filtrenin istediği veriyi içerip içermediğini kontrol eder ve eğer öyleyse light client tüm bloğu ister. Hafif istemcilere bu şekilde hizmet verebilmek için, tam düğümlerin bu filtreleri sürdürmesi gerektiği ve hafif istemcilerin de birkaç filtreyi sürdürmek isteyebileceği, bu nedenle filtreleri mümkün olduğunca hafif tutun Miktar çok önemlidir ( bu nedenle "yoğun blok filtresi" olarak adlandırılır).
Çok iyi! Şimdi nadir şeylere giriyoruz. Böyle bir filtre nasıl oluşturulur? Nasıl görünüyor?
İhtiyaçlarımız nelerdir?
Filtrede bulunan temel bilgiler şunlardır: her işlemin her girişinin komut dosyası ortak anahtarı (yani, harcanan her UTXO'nun komut dosyası genel anahtarı) ve her işlemin her çıkışının komut dosyası genel anahtarı. Temel olarak şöyle:
nesneler = {spk1, spk2, spk3, spk4, ..., spkN} // N komut dosyası ortak anahtarlarının listesi
Teknik olarak burada durabiliriz - tam da böyle bir komut dosyası ortak anahtarları listesi de filtremiz olarak işlev görebilir. Bu, hafif istemciler tarafından ihtiyaç duyulan bilgileri içeren bir bloğun yoğunlaştırılmış bir sürümüdür. Böyle bir liste ile hafif bir müşteri, bir blokta ilgili bir işlem olup olmadığını %100 doğrulayabilir. Ancak böyle bir liste çok büyük. Bu nedenle, sonraki adımlarımızın tümü bu listeyi olabildiğince kompakt hale getirmek içindir. İşlerin ilginçleştiği yer burasıdır.
İlk olarak, her nesneyi belirli bir aralıkta bir sayıya dönüştürebilir ve her nesne numarasını bu aralık içinde eşit olarak dağıtabiliriz. Diyelim ki 10 nesnemiz var (N = 10) ve sonra her nesneyi bir sayıya dönüştüren bir tür fonksiyonumuz var. Diyelim ki [0, 10] aralığını seçtik (çünkü 10 nesnemiz var). Şimdi, "hash to number" fonksiyonumuz her nesneyi girdi olarak alacak ve sırasıyla [0, 10] boyutunda bir sayı üretecektir. Sayılar bu aralıkta eşit olarak dağıtılır. Bu, sıralamadan sonra şöyle bir grafik elde edeceğimiz anlamına gelir (çok, çok ideal bir durumda):
sayılar := {1,2,3,4,5,6,7,8,9,10}
Hafif bir istemci, aradığı bir nesnenin filtreyle eşleşip eşleşmediğini bilmek umuduyla böyle bir filtre indirir. Sadece ilgilendikleri nesneyi alırlar ve sonucun bu filtrede olup olmadığını görmek için aynı "karma sayı" işlevini çalıştırırlar. Sorun nerede? Sorun şu ki, filtredeki sayılar zaten o boşluktaki her olasılığı kapsıyor! Bu, hafif istemcinin hangi nesneleri önemsediği önemli değil, ortaya çıkan sayısal sonucun bu filtreyle eşleşmesi gerektiği anlamına gelir. Yani bu filtrenin "yanlış pozitiflik oranı" 1'dir (Çevirmenin Notu: Buradaki "yanlış pozitif", bloğun içermeyebileceği anlamına gelir. Filtrenin elde ettiği sonuç evet'tir). Bu pek iyi değil. Ayrıca, filtreyi oluşturmak için verileri sıkıştırma sürecinde çok fazla bilgi kaybettiğimizi de gösteriyor. Daha yüksek bir yanlış pozitif oranına ihtiyacımız var. Ardından, yanlış pozitif oranın 5 olmasını istediğimizi varsayalım. Ardından, bloktaki nesneleri [0, 50] aralığındaki sayılarla eşit şekilde eşleştirebiliriz:
Artık [0, 50] aralığında eşit olarak dağıtılan sıralı bir sayı listemiz var. Bu listede 10 numara olduğunu biliyoruz. Bu, bu sıralı sayı listesindeki iki bitişik sayı arasındaki farkın büyük olasılıkla 5 olduğu sonucuna varabiliriz anlamına gelir. Genel olarak, N nesnemiz varsa ve yanlış pozitif M oranı istiyorsak, o zaman alanın boyutu N * M olmalıdır. Yani, bu sayıların boyutu 0 ile N * M arasında olmalıdır, ancak (sıralamadan sonra) kontrol edilen iki bitişik sayının enterpolasyon olasılığı M'dir. Ve M, N * M alanındaki sayıdan daha küçük saklanmalıdır. Böylece, her bir sayıyı saklamak yerine, iki bitişik sayı arasındaki farkı saklayabiliriz. Yukarıdaki örnekte bu, [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50] yerine [0, 5, 5, 5, 5] depoladığımız anlamına gelir. , 5, 5, 5, 5, 5], buradan ilk listeyi yeniden oluşturmak önemsizdir. Küçültürseniz, 50 sayısını depolamak, 5 sayısını depolamaktan açıkça daha fazla yer kaplar. Ama neden orada dursun? Biraz daha sıkıştıralım!
Bir sonraki adım, "Golomb-Rice kodlamasını" kullanmaktır. Bu kodlama yöntemi, tablodaki her sayının belirli bir sayıya çok yakın olduğu bir listeyi iyi kodlayabilir. Ve listemiz tam da bu kadar! Listemizdeki sayıların her birinin 5'e yakın olması muhtemeldir (veya M olan yanlış pozitif oranımıza yakındır), bu nedenle her sayının M ile bölümünü alın (her sayıyı 5'e bölün ve kalanı almayın), büyük olasılıkla 0 (sayı 5'ten biraz küçükse) veya 1 (sayı 5'ten biraz büyükse) olacaktır. Bölümün 2, 3 vb. olması da mümkündür, ancak olasılık çok daha küçüktür. Harika! Dolayısıyla bundan faydalanabiliriz: daha büyük, ancak daha az olası bölümleri kodlamak için daha fazla bitcoin kullanırken, daha küçük bölümleri kodlamak için mümkün olduğunca az bit kullanacağız. Ardından, her zaman [0, M - 1] (bu durumda [0, 4]) aralığında olacak olan kalanı da kodlamamız gerekir (çünkü tüm listeyi tam olarak yeniden oluşturabilmek istiyoruz). Kodlayıcı için aşağıdaki eşlemeleri kullanıyoruz:
0000 10000 10000 10000 10000 10000 10000 10000 10000 10000
Daha gerçekçi bir duruma geçmeden önce, bu filtre ile orijinal listemizi kurtarabilir miyiz bir bakalım.
Şimdi elimizde "0000100001000010000100001000010000100001000010000" var. Golomb-Rice kodunu biliyoruz ve M'nin 5 olduğunu biliyoruz (çünkü bu, bu filtre yapısını kullanan herkes tarafından bilinen, herkesin bildiği bir bilgi olacaktır). M'nin 5 olduğunu bildiğimiz için, kalanı kodlamak için 3 bit olacağını biliyoruz. Böylece, bu filtreyi aşağıdaki "bölüm-kalan" dizi listesine dönüştürebiliriz:
[(0, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), ( 1, 0), (1, 0)]
Bölümün M(5) ile bölünerek elde edildiğini bilerek, şunu kurtarabiliriz:
[0, 5, 5, 5, 5, 5, 5, 5, 5, 5]
Ayrıca, bu listenin iki bitişik sayı arasındaki farkı temsil ettiğini biliyoruz, böylece orijinal listeyi geri yükleyebiliriz:
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
Daha gerçekçi bir örnek
Şimdi gerçek bir Bitcoin testnet bloğu için bir filtre oluşturmaya çalışacağız. 2101914 bloğunu kullanacağım. Filtresinin nasıl göründüğüne bakalım:
$ bitcoin-cli getblockhash 2101914 000000000000002c06f9afaf2b2b066d4f814ff60cfbc4df55840975a00e035c $ bitcoin-cli getblockfilter 000000000000002c06f9afaf2b2b066d4f814ff60cfbc4df55840975a00e035c { "filtre": "5571d126b85aa79c9de56d55995aa292de0484b830680a735793a8c2260113148421279906f800c3b8c94ff37681fb1fd230482518c52df57437864 023833f2f801639692646ddcd7976ae4f2e2a1ef58c79b3aed6a705415255e362581692831374a5e5e70d5501cdc0a52095206a15cd2eb98ac980c22466e6945 a65a5b0b0c5b32aa1e0cda2545da2c4345e049b614fcad80b9dc9c903788163822f4361bbb8755b79c276b1cf7952148de1e5ee0a92f6d70c4f522aa6877558f6 2b34b56ade12fa2e61023abf3e570937bf379722bc1b0dc06ffa1c5835bb651b9346a270", "başlık": "8d0cd8353342930209ac7027be150e679bbc7c65cc62bb8392968e43a6ea3bfe" }
Pekala canım, bloklardan nasıl böyle bir filtre oluşturabileceğimize bakalım.
Burada kullanılan kodun tamamı bu github deposunda bulunabilir. Burada sadece bazı sözde kod parçacıkları göstereceğim. Bu kodun çekirdeği, bitcoin istemcileri tarafından bitcoind'i ve ilgili blokları çağırmak için kullanılabilen, buildFilter adlı bir işlevdir. İşlev şöyle görünür:
func buildFilter(bc *bitcoind.Bitcoind, blok bitcoind.Block) ([]byte, error) { // 1, bloktan bir filtreye eklemek istediğimiz tüm nesneleri toplar // 2 Bu nesneleri sayılara dönüştürün ve sıralayın // 3 Sıralı numara listesindeki iki bitişik sayı arasındaki farkı bulun // 4 Bu farklılıkları kodlamak için Golomb-Rice kodlamasını kullanın }
Bu nedenle, ilk adım, filtreye eklemek istediğimiz bloktaki tüm nesneleri toplamaktır. BIP'den başlayarak, bu nesnelerin tüm kullanılmış betik ortak anahtarlarını ve her çıktı betiği genel anahtarını içerdiğini biliyoruz. BIP ayrıca bazı ek kurallar da belirler: madeni para tabanı işlemine girişleri atlarız (hiç girdisi olmadığı için mantıklı değildir) ve tüm OP_RETURN çıktılarını geçeriz. Ayrıca verileri tekilleştiriyoruz. İki özdeş betik ortak anahtarı varsa, bunu filtreye yalnızca bir kez ekleriz.
// filtreye eklemek istediğimiz nesnelerin listesi // Tüm harcanan betik ortak anahtarlarını ve her çıktı betiği ortak anahtarını dahil edin // Yinelenen komut dosyası ortak anahtarlarını kaldıran bir "harita" kullanıyoruz nesneler := make(harita [string] yapı{}) // bloktaki her işlemi kolaylaştır i için, tx := aralık bloğu.Tx { // Her çıktı için betik ortak anahtarı ekleyin // nesne listemize _ için, txOut := aralık tx.Vout { PubKey := txOut.PubKey eğer len(PubKey) == 0 { devam etmek } // Çıktı OP_RETURN (0x6a) ise atla eğer spk [0] == 0x6a { devam etmek } nesneler [skpStr] = yapı{}{} } // coinbase işleminin girişini eklemeyin eğer ben == 0 { devam etmek } // Her girdi için, komut dosyasının genel anahtarını alın _ için, txIn := aralık tx.Vin { öncekiTx, hata := bc.GetRawTransaction(txIn.Txid) eğer hata != sıfır { dönüş sıfır, hata } PubKey := öncekiTx.Vout[txIn.Vout].PubKey eğer len(PubKey) == 0 { devam etmek } nesneler [spkStr] = yapı{}{} } }
Tamam, şimdi tüm nesneler toplandı. Şimdi, N değişkenini nesne grafiğinin uzunluğu olarak tanımlıyoruz. Burada, N 85'tir.
Bir sonraki adım, nesnemizi bir aralıkta düzgün dağılmış sayılara dönüştürmektir. Yine, aralık, ne kadar yüksek bir yanlış pozitif oranı istediğinize bağlıdır. BIP158, M sabitini 784931 olarak tanımlar. Bu, yanlış pozitif olasılığının 784931'de 1 olduğu anlamına gelir. Tıpkı daha önce yaptığımız gibi, M çarpı N yanlış pozitif oranını alıp tüm sayıların dağılım aralığını elde ederiz. Bu aralığı F, yani F = M * N olarak tanımlarız. Burada F = 66719135. Nesneleri sayılara eşleyen işlevlerin ayrıntılarına girmeyeceğim (ayrıntıları yukarıda bağlantısı verilen github deposunda bulabilirsiniz). Bilmeniz gereken tek şey, girdi olarak nesneyi, F sabitini (nesnenin eşlenmesi gereken kapsamı tanımlar) ve bir anahtarı (blok karması) almasıdır. Tüm sayılara sahip olduğumuzda, listeyi artan düzende sıralarız ve ardından, sıralanan sayılar listesindeki iki bitişik sayı arasındaki farkı tutan farklar adı verilen yeni bir liste oluştururuz.
sayılar := make([]uint64, 0, N) // Tüm nesneleri yineleyin ve bunları [0, F] aralığında eşit olarak dağıtılan sayılara dönüştürün // ve bu numaraları sayı listesi olarak sakla o için := menzil nesneleri { // Verilen anahtarı, maksimum sayıyı (F) ve nesne baytlarını (o) kullanarak, // nesneyi 0 ile F arasında bir sayıya dönüştür. v := convertToNumber(b, F, key) sayılar = ekle(sayılar, v) } // sayı listesini sırala sort.Slice(sayılar, func(i, j int) bool { dönüş sayıları [i] < sayılar [j] }) // Sayı listesini farklılıklar listesine dönüştürün farklar := make([]uint64, N) i için, num := aralık sayıları { eğer ben == 0 { farklılıklar [i] = sayı devam etmek } farklılıklar [i] = sayı - sayılar[i-1] }
Harika! İşte sayıların ve farklılıkların bir listesini gösteren bir resim.
Son adım, Golomb-Rice kodlamasını kullanarak farklılıklar listesini kodlamaktır. Önceki açıklamadan, her bir farkı en olası değere bölmemiz ve ardından bölümü ve kalanı kodlamamız gerektiğini hatırlayın. Bir önceki örneğimizde en olası değerin M olduğunu ve kalanın [0, M] arasında olacağını söylemiştim. Ancak BIP158, son filtre boyutunu gerçekten en aza indiren parametre olmayan 2'yi bulduğumuz için bunu yapmaz. Yani M kullanmak yerine yeni bir P sabiti tanımlıyoruz ve Golomb-Rice parametresi olarak 2^P kullanıyoruz. P, 19 olarak tanımlanır. Bu, bölümü ve kalanı elde etmek için her bir farkı 2^19'a böldüğümüz ve kalanın 19 bit ile ikili olarak kodlandığı anlamına gelir.
filtre := bstream.NewBStreamWriter(0) // Farklar listesindeki her değer için bölümü ve kalanı 2^P'ye bölerek elde edin. _, d için := aralık farkları { q := matematik.Kat(float64(d)/math.Exp2(float64(P)))) r := d - uint64(math.Exp2(float64(P))*q) // Kodlayıcı i için := 0; ben < int(q); ben++ { filter.WriteBit(doğru) } filter.WriteBit(yanlış) filter.WriteBits(r, P) }
Harika! Şimdi filtrenin çıktısını alabiliriz:
71d126b85aa79c9de56d55995aa292de0484b830680a735793a8c2260113148421279906f800c3b8c94ff37681fb1fd230482518c52df57437864023833f 2f 801639692646ddcd7976ae4f2e2a1ef58c79b3aed6a705415255e362581692831374a5e5e70d5501cdc0a52095206a15cd2eb98ac980c22466e6945a65a5b 0b0c 5b32aa1e0cda2545da2c4345e049b614fcad80b9dc9c903788163822f4361bbb8755b79c276b1cf7952148de1e5ee0a92f6d70c4f522aa6877558f62b34b 56ade12fa2e 61023abf3e570937bf379722bc1b0dc06ffa1c5835bb651b9346a270
İlk iki bayt dışında bitcoind'de aldığımız filtrenin tamamen aynısı! İlk 2 baytta neden bir fark var? Çünkü BIP, N değerinin CompactSize formatında kodlanması ve alıcı tarafından çözülebilmesi için filtrenin önünde görünmesi gerektiğini söylüyor. Bu tarafından yapılır:
fd := filtre.Bytes() arabellek baytları.Buffer buffer.Grow(wire.IntSerializeSize(uint64(N)) + len(fd)) hata = wire.WriteInt(&buffer, 0, uint64(N)) eğer hata != sıfır { dönüş sıfır, hata } _, hata = tampon.Yaz(fd) eğer hata != sıfır { dönüş sıfır, hata }
Şimdi filtreyi tekrar yazdırırsak, bunun tam olarak bitcoind'in sonucuyla aynı olduğunu görürüz:
5571d126b85aa79c9de56d55995aa292de0484b830680a735793a8c2260113148421279906f800c3b8c94ff37681fb1fd230482518c52df5743786402383 3f 2f801639692646ddcd7976ae4f2e2a1ef58c79b3aed6a705415255e362581692831374a5e5e70d5501cdc0a52095206a15cd2eb98ac980c22466e6945a65a 5b0b 0c5b32aa1e0cda2545da2c4345e049b614fcad80b9dc9c903788163822f4361bbb8755b79c276b1cf7952148de1e5ee0a92f6d70c4f522aa6877558f62b3 4b56ade12fa 2e61023abf3e570937bf379722bc1b0dc06ffa1c5835bb651b9346a270
başarı!
Ancak kişisel anlayışım, filtreye N eklemeye gerek olmadığı yönünde. P'nin değerini biliyorsanız, N'nin değerini de bulabilirsiniz. Bakalım ilk filtrenin çıktısını kullanarak orijinal sayı listesini geri yükleyebilir miyiz:
b := bstream.NewBStreamReader(filtre) ( sayılar []uint64 öncekiNum uint64 ) için { // Bayt dizisinden bölümü okuyun. Karşılaşılan ilk 0, bölümün sonunu gösterir // 0 ile karşılaşmadan önce, 1 sayısı bölümü temsil eder q uint64 c, hata := b.ReadBit() eğer hata != sıfır { dönüş hatası } için { s++ c, hata = b.ReadBit() eğer hatalar.Is(err, io.EOF) { kırmak } else if err != nil { dönüş hatası } } // Sonraki P bitleri kalanı kodlar r, hata := b.ReadBits(P) eğer hatalar.Is(err, io.EOF) { kırmak } else if err != nil { dönüş hatası } n := q*uint64(math.Exp2(float64(P)))) + r sayı := n + öncekiNum sayılar = ekle(sayılar, sayı) öncekiNum = sayı } fmt.Println(sayılar)
Yukarıdaki işlem aynı sayı listesini üretir, bu yüzden onu N'yi bilmeden yeniden oluşturabiliriz. Bu yüzden, filtreye N eklemenin mantığının ne olduğundan emin değilim. N'nin neden dahil edilmesi gerektiğini biliyorsanız, lütfen bana bildirin!
Okuduğunuz için teşekkürler!