package bsv
import (
"encoding/base64"
"encoding/json"
"os"
"testing"
)
func TestMessageSignVerify(t *testing.T) {
wif := "cPBn5A4ikZvBTQ8D7NnvHZYCAxzDZ5Z2TSGW2LkyPiLxqYaJPBW4"
address := "n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx"
priv, err := PrivateKeyFromWIF(wif)
if err != nil {
t.Fatal(err)
}
msg, err := NewMessage("hello, world")
if err != nil {
t.Fatal(err)
}
sig, err := msg.Sign(priv)
if err != nil {
t.Fatal(err)
}
if sig == "" {
t.Fatal("empty signature")
}
ok, err := msg.Verify(address, sig)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("signature should verify")
}
knownSig := "H/DIn8uA1scAuKLlCx+/9LnAcJtwQQ0PmcPrJUq90aboLv3fH5fFvY+vmbfOSFEtGarznYli6ShPr9RXwY9UrIY="
ok2, err := msg.Verify(address, knownSig)
if err != nil {
t.Fatal(err)
}
if !ok2 {
t.Fatal("known signature should verify")
}
badAddr := "mmRcrB5fTwgxaFJmVLNtaG8SV454y1E3kC"
ok3, err := msg.Verify(badAddr, knownSig)
if err != nil {
t.Fatal(err)
}
if ok3 {
t.Fatal("signature must not verify against wrong address")
}
}
func TestMessageBufferData(t *testing.T) {
wif := "cPBn5A4ikZvBTQ8D7NnvHZYCAxzDZ5Z2TSGW2LkyPiLxqYaJPBW4"
address := "n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx"
priv, _ := PrivateKeyFromWIF(wif)
bufData, _ := base64.StdEncoding.DecodeString("H/DIn8uA1scAuKLlCx+/9LnAcJtwQQ0PmcPrJUq90aboLv3fH5fFvY+vmbfOSFEtGarznYli6ShPr9RXwY9UrIY=")
msg, _ := NewMessage(bufData)
sig, err := msg.Sign(priv)
if err != nil {
t.Fatal(err)
}
ok, err := msg.Verify(address, sig)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("buffer message signature should verify")
}
}
type bip39Vector struct {
English string `json:"english"`
Japanese string `json:"japanese"`
Chinese string `json:"chinese"`
Entropy string `json:"entropy"`
Seed string `json:"seed"`
Passphase string `json:"passphase"`
}
func TestBIP39Vectors(t *testing.T) {
data, err := os.ReadFile("testdata/fixtures.json")
if err != nil {
t.Skip("mnemonic fixtures not available: " + err.Error())
}
var payload map[string][][]string
if err := json.Unmarshal(data, &payload); err != nil {
t.Fatal(err)
}
vectors := payload["english"]
count := 0
for i, v := range vectors {
if len(v) < 4 {
continue
}
passphrase := v[0]
entropyHex := v[1]
mnemonicStr := v[2]
seedHex := v[3]
t.Run("bip39_"+itoa(i), func(t *testing.T) {
if !MnemonicIsValid(mnemonicStr, MnemonicWords["ENGLISH"]) {
t.Fatalf("english mnemonic %q should be valid", mnemonicStr)
}
entropy := fromHex(entropyHex)
got, err := entropy2Mnemonic(entropy, MnemonicWords["ENGLISH"])
if err != nil {
t.Fatalf("entropy2mnemonic: %v", err)
}
if got != mnemonicStr {
t.Fatalf("mnemonic = %q, want %q", got, mnemonicStr)
}
m, err := MnemonicFromString(mnemonicStr, MnemonicWords["ENGLISH"])
if err != nil {
t.Fatalf("parse: %v", err)
}
seed := m.ToSeed(passphrase)
if toHex(seed) != seedHex {
t.Fatalf("seed = %s, want %s", toHex(seed), seedHex)
}
})
count++
}
if count == 0 {
t.Fatal("no mnemonic vectors processed")
}
t.Logf("passed %d BIP39 vectors", count)
}
func TestMnemonicBasics(t *testing.T) {
if len(MnemonicWords["ENGLISH"]) != 2048 {
t.Fatal("english wordlist length != 2048")
}
if MnemonicWords["ENGLISH"][0] != "abandon" {
t.Fatal("english wordlist[0] != abandon")
}
if len(MnemonicWords["SPANISH"]) != 2048 {
t.Fatal("spanish wordlist length != 2048")
}
if len(MnemonicWords["JAPANESE"]) != 2048 {
t.Fatal("japanese wordlist length != 2048")
}
if len(MnemonicWords["CHINESE"]) != 2048 {
t.Fatal("chinese wordlist length != 2048")
}
m1, err := MnemonicFromRandom(nil)
if err != nil {
t.Fatal(err)
}
m2, err := MnemonicFromRandom(nil)
if err != nil {
t.Fatal(err)
}
if m1.ToString() == m2.ToString() {
t.Fatal("two random mnemonics should differ")
}
if !MnemonicIsValid(m1.ToString(), MnemonicWords["ENGLISH"]) {
t.Fatal("generated mnemonic should be valid")
}
m3, err := MnemonicFromString("lab rescue lunch elbow recall phrase perfect donkey biology guess moment husband", nil)
if err != nil {
t.Fatal(err)
}
if !MnemonicIsValid(m3.ToString(), nil) {
t.Fatal("known mnemonic should be valid")
}
hd, err := m3.ToHDPrivateKey("", nil)
if err != nil {
t.Fatal(err)
}
if hd.Xprivkey == "" {
t.Fatal("empty xprv")
}
}
func TestMnemonicKnownInvalid(t *testing.T) {
if MnemonicIsValid("pilots foster august tomorrow kit daughter unknown awesome model town village master", MnemonicWords["ENGLISH"]) {
t.Fatal("should not be a valid english mnemonic")
}
if MnemonicIsValid("monster foster august tomorrow kit daughter unknown awesome model town village pilot", MnemonicWords["ENGLISH"]) {
t.Fatal("invalid checksum should fail")
}
}
func TestECIESRoundtrip(t *testing.T) {
alice, err := PrivateKeyFromRandom(nil)
if err != nil {
t.Fatal(err)
}
bob, err := PrivateKeyFromRandom(nil)
if err != nil {
t.Fatal(err)
}
message := "top secret message"
enc := NewECIES(nil)
enc.SetPrivateKey(alice)
enc.SetPublicKey(bob.ToPublicKey())
ciphertext, err := enc.Encrypt(message, nil)
if err != nil {
t.Fatal(err)
}
dec := NewECIES(nil)
dec.SetPrivateKey(bob)
plaintext, err := dec.Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if string(plaintext) != message {
t.Fatalf("decrypted = %q, want %q", plaintext, message)
}
}
func TestECIESNoKeyShortTag(t *testing.T) {
priv, _ := PrivateKeyFromRandom(nil)
pub := priv.ToPublicKey()
msg := []byte("hello ecies")
enc := NewECIES(map[string]interface{}{"noKey": true, "shortTag": true})
enc.SetPrivateKey(priv)
enc.SetPublicKey(pub)
ciphertext, err := enc.Encrypt(msg, nil)
if err != nil {
t.Fatal(err)
}
dec := NewECIES(map[string]interface{}{"noKey": true, "shortTag": true})
dec.SetPrivateKey(priv)
dec.SetPublicKey(pub)
plaintext, err := dec.Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if string(plaintext) != string(msg) {
t.Fatalf("decrypted = %q, want %q", plaintext, msg)
}
}
func TestECIESCrossCompatibility(t *testing.T) {
alice, err := PrivateKeyFromWIF("cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY")
if err != nil {
t.Fatal(err)
}
bob, err := PrivateKeyFromWIF("cQ3nCBQB9RsFSyjNQM15NQLVpXXMtWh9PUyeFz5KxLJCHsuRH2Su")
if err != nil {
t.Fatal(err)
}
message := "cross platform ecies test"
enc := NewECIES(nil)
enc.SetPrivateKey(alice)
enc.SetPublicKey(bob.ToPublicKey())
ciphertext, err := enc.Encrypt(message, nil)
if err != nil {
t.Fatal(err)
}
expected := "0223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5e0772f28aca12fba5f7dd33913b08377c12b3b355cad20da51166d64a7828bf3a9d78a31bf1a68ed57e8ceb3d10cad761bee0c5f2c47f5a032b5162200c0c9db7f88ef11ea4e44dd87657bf5bad45c572"
if toHex(ciphertext) != expected {
t.Fatalf("ciphertext mismatch:\n got %s\nwant %s", toHex(ciphertext), expected)
}
dec := NewECIES(nil)
dec.SetPrivateKey(bob)
plaintext, err := dec.Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if string(plaintext) != message {
t.Fatalf("decrypted = %q, want %q", plaintext, message)
}
}
func TestMessageCrossCompatibility(t *testing.T) {
alice, err := PrivateKeyFromWIF("cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY")
if err != nil {
t.Fatal(err)
}
msg, err := NewMessage("cross platform ecies test")
if err != nil {
t.Fatal(err)
}
sig, err := msg.Sign(alice)
if err != nil {
t.Fatal(err)
}
expected := "IIPlC4SJCA+o1X1k2A1oZOCD/WaNe2uvemQ8numVHi6mPm6ii5srcZ/4oMJUY1sNAwHuVT7xVGO8iRlA3BLXXlw="
if sig != expected {
t.Fatalf("signature mismatch:\n got %s\nwant %s", sig, expected)
}
ok, err := msg.Verify("1DUbYShrWmyhHd8qujZb7PxQ1sdLNWK98S", sig)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("signature should verify")
}
}
func TestBlockHeaderKnown(t *testing.T) {
hex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"
h, err := BlockHeaderFromString(hex)
if err != nil {
t.Fatal(err)
}
if h.Hash() != "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" {
t.Fatalf("genesis hash = %s", h.Hash())
}
if h.ToString() != hex {
t.Fatalf("header roundtrip mismatch")
}
}
func TestMerkleBlockFromData(t *testing.T) {
headerHex := "0100000000000000000000000000000000000000000000000000000000000000000000004a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b29ab5f49ffff001d1dac2b7c"
header, err := BlockHeaderFromString(headerHex)
if err != nil {
t.Fatal(err)
}
txid := "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
mb := &MerkleBlock{
Header: header,
NumTransactions: 1,
Hashes: []string{txid},
Flags: []byte{0x03},
}
if !mb.ValidMerkleTree() {
t.Fatal("single-tx merkleblock should be valid")
}
buf := mb.ToBuffer()
mb2, err := MerkleBlockFromBuffer(buf)
if err != nil {
t.Fatal(err)
}
if mb2.Header.Hash() != header.Hash() || mb2.NumTransactions != 1 {
t.Fatal("merkleblock roundtrip mismatch")
}
}
func TestMerkleBlockBIP37(t *testing.T) {
data, err := os.ReadFile("testdata/merkleblocks.json")
if err != nil {
t.Skip("testdata/merkleblocks.json not available: " + err.Error())
}
var jsonVectors []map[string]interface{}
if err := json.Unmarshal(data, &jsonVectors); err != nil {
t.Fatalf("parse JSON: %v", err)
}
if len(jsonVectors) < 4 {
t.Fatalf("expected at least 4 merkleblock vectors, got %d", len(jsonVectors))
}
for i, j := range jsonVectors {
t.Run("valid_"+itoa(i), func(t *testing.T) {
mb, err := merkleBlockFromJSONObject(j)
if err != nil {
t.Fatalf("parse: %v", err)
}
if !mb.ValidMerkleTree() {
t.Fatalf("merkleblock %d should be valid", i)
}
})
}
t.Run("filtered", func(t *testing.T) {
mb, err := merkleBlockFromJSONObject(jsonVectors[3])
if err != nil {
t.Fatal(err)
}
filtered, err := mb.FilteredTxsHash()
if err != nil {
t.Fatal(err)
}
want := "6f64fd5aa9dd01f74c03656d376625cf80328d83d9afebe60cc68b8f0e245bd9"
if len(filtered) != 1 || filtered[0] != want {
t.Fatalf("filtered = %v, want [%s]", filtered, want)
}
has, err := mb.HasTransaction(want)
if err != nil {
t.Fatal(err)
}
if !has {
t.Fatal("merkleblock should contain the filtered tx")
}
has2, err := mb.HasTransaction("0000000000000000000000000000000000000000000000000000000000000000")
if err != nil {
t.Fatal(err)
}
if has2 {
t.Fatal("merkleblock should not contain a random txid")
}
})
t.Run("too_many_hashes", func(t *testing.T) {
mb, err := merkleBlockFromJSONObject(jsonVectors[0])
if err != nil {
t.Fatal(err)
}
mb.Hashes = []string{}
for i := 0; i < 100; i++ {
mb.Hashes = append(mb.Hashes, "0000000000000000000000000000000000000000000000000000000000000000")
}
if mb.ValidMerkleTree() {
t.Fatal("too many hashes should be invalid")
}
})
t.Run("too_few_flags", func(t *testing.T) {
mb, err := merkleBlockFromJSONObject(jsonVectors[0])
if err != nil {
t.Fatal(err)
}
mb.Flags = []byte{0x00}
if mb.ValidMerkleTree() {
t.Fatal("too few flags should be invalid")
}
})
}
func merkleBlockFromJSONObject(obj map[string]interface{}) (*MerkleBlock, error) {
headerObj, ok := obj["header"].(map[string]interface{})
if !ok {
return nil, newError("InvalidArgument", "missing header")
}
header, err := NewBlockHeader(headerObj)
if err != nil {
return nil, err
}
mb := &MerkleBlock{Header: header, Hashes: []string{}, Flags: []byte{}}
if v, ok := obj["numTransactions"].(float64); ok {
mb.NumTransactions = uint32(v)
}
if v, ok := obj["hashes"].([]interface{}); ok {
for _, h := range v {
if s, ok := h.(string); ok {
mb.Hashes = append(mb.Hashes, s)
}
}
}
if v, ok := obj["flags"].([]interface{}); ok {
for _, f := range v {
if n, ok := f.(float64); ok {
mb.Flags = append(mb.Flags, byte(n))
}
}
}
return mb, nil
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func TestBlockFromRealData(t *testing.T) {
raw, err := readFileBytes("testdata/blk86756-testnet.dat")
if err != nil {
t.Skip("blk86756-testnet.dat not available: " + err.Error())
}
jsonData, err := readFileBytes("testdata/blk86756-testnet.json")
if err != nil {
t.Skip("blk86756-testnet.json not available")
}
var expected struct {
Header struct {
Hash string `json:"hash"`
} `json:"header"`
Transactions []struct {
Hash string `json:"hash"`
} `json:"transactions"`
}
if err := jsonUnmarshal(jsonData, &expected); err != nil {
t.Fatal(err)
}
block, err := BlockFromRawBlock(raw)
if err != nil {
t.Fatal(err)
}
if block.Header.Hash() != expected.Header.Hash {
t.Fatalf("block hash = %s, want %s", block.Header.Hash(), expected.Header.Hash)
}
if len(block.Transactions) != len(expected.Transactions) {
t.Fatalf("tx count = %d, want %d", len(block.Transactions), len(expected.Transactions))
}
if block.Transactions[0].GetID() != expected.Transactions[0].Hash {
t.Fatalf("first txid = %s, want %s", block.Transactions[0].GetID(), expected.Transactions[0].Hash)
}
last := block.Transactions[len(block.Transactions)-1].GetID()
if last != expected.Transactions[len(expected.Transactions)-1].Hash {
t.Fatalf("last txid = %s, want %s", last, expected.Transactions[len(expected.Transactions)-1].Hash)
}
t.Logf("block %s with %d transactions parsed and verified", expected.Header.Hash, len(block.Transactions))
}
func readFileBytes(path string) ([]byte, error) {
return os.ReadFile(path)
}