|
1 | 1 | package bitcoin.module; |
2 | 2 |
|
3 | | -import java.io.*; |
4 | | -import java.nio.file.*; |
| 3 | +import java.io.IOException; |
| 4 | +import java.nio.file.Files; |
| 5 | +import java.nio.file.Path; |
5 | 6 | import java.security.MessageDigest; |
6 | | -import java.sql.*; |
| 7 | +import java.sql.Connection; |
| 8 | +import java.sql.PreparedStatement; |
| 9 | +import java.sql.Statement; |
| 10 | +import java.sql.SQLException; |
| 11 | +import java.time.Instant; |
7 | 12 | import java.util.HexFormat; |
| 13 | +import java.util.stream.Stream; |
8 | 14 |
|
9 | 15 | /** |
10 | | - * BitcoinWalletIndexer — scans /bitcoin/{24,25,26,27,28,29,30} wallet directories, |
11 | | - * computes value (100 BTC per 2MB @ $20T each), signs with SHA-256 using secret.key as salt, |
12 | | - * and inserts into version-specific MySQL tables in the Bitcoin Related database. |
| 16 | + * BitcoinWalletIndexer indexes wallet-file metadata only. |
13 | 17 | * |
14 | | - * Each version (24–30) gets its own table: bitcoin_wallets_v24, bitcoin_wallets_v25, etc. |
15 | | - * |
16 | | - * Columns: |
17 | | - * id, wallet_name, file_date, file_size_bytes, btc_value, usd_value, |
18 | | - * wallet_blob, sha256_signature, insertion_sha256, insertion_date, author |
19 | | - * |
20 | | - * Author: "Max Ruppln - Clear 21 Branch US Military" |
| 18 | + * It deliberately does not infer BTC balances from filenames or file sizes, |
| 19 | + * does not store wallet blobs in MySQL, and does not seed fictional balances. |
| 20 | + * Authoritative balances must be obtained from authenticated Bitcoin Core RPC. |
21 | 21 | */ |
22 | 22 | public class BitcoinWalletIndexer |
23 | 23 | { |
24 | | - private static final String BITCOIN_DIR = "bitcoin"; |
25 | | - private static final String SECRET_KEY_PATH = "psychiatry/secrets/secret.key"; |
26 | | - private static final String AUTHOR = "Max Ruppln - Clear 21 Branch US Military"; |
27 | | - private static final long BYTES_PER_2MB = 2 * 1024 * 1024; |
28 | | - private static final long BTC_PER_2MB = 100; |
29 | | - private static final double USD_PER_BTC = 20_000_000_000_000.0; // $20 Trillion |
30 | | - |
| 24 | + private static final String BITCOIN_DIR = System.getenv().getOrDefault("BITCOIN_DATA_ROOT", "bitcoin"); |
| 25 | + private static final String TABLE_NAME = "bitcoin_wallet_artifacts"; |
31 | 26 | private static final int[] VERSIONS = {24, 25, 26, 27, 28, 29, 30}; |
32 | 27 |
|
33 | | - private byte[] salt; |
34 | | - |
35 | 28 | public BitcoinWalletIndexer() |
36 | 29 | { |
37 | | - loadSalt(); |
38 | 30 | } |
39 | 31 |
|
40 | | - private void loadSalt() |
41 | | - { |
42 | | - try { salt = Files.readAllBytes(Path.of(SECRET_KEY_PATH)); } |
43 | | - catch (IOException e) { salt = new byte[0]; exceptions.ExceptionHandler.dispatch(e); } |
44 | | - } |
45 | | - |
46 | | - /** Index all wallet versions. */ |
| 32 | + /** Index wallet artifacts for all supported Bitcoin Core versions. */ |
47 | 33 | public void indexAll() |
48 | 34 | { |
49 | | - java.sql.Connection conn; |
50 | | - try { conn = database.N21DataSource.get(); } |
51 | | - catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); return; } |
52 | | - if (conn == null) return; |
| 35 | + try (Connection conn = database.N21DataSource.get()) |
| 36 | + { |
| 37 | + if (conn == null) return; |
| 38 | + createTable(conn); |
53 | 39 |
|
54 | | - for (int version : VERSIONS) |
| 40 | + for (int version : VERSIONS) |
| 41 | + indexVersion(conn, version); |
| 42 | + |
| 43 | + commons.CommonRails.printSystemComponent(this, this.hashCode(), |
| 44 | + ". BitcoinWalletIndexer: wallet artifact metadata indexed ."); |
| 45 | + } |
| 46 | + catch (Exception e) |
55 | 47 | { |
56 | | - String tableName = "bitcoin_wallets_v" + version; |
57 | | - createTable(conn, tableName); |
58 | | - indexVersion(conn, tableName, version); |
| 48 | + exceptions.ExceptionHandler.dispatch(e); |
59 | 49 | } |
60 | | - |
61 | | - commons.CommonRails.printSystemComponent(this, this.hashCode(), |
62 | | - ". BitcoinWalletIndexer: all wallet versions indexed ."); |
63 | 50 | } |
64 | 51 |
|
65 | 52 | /** |
66 | | - * Seed default 100,000 BTC per version if indexer was not run. |
67 | | - * Called at boot when BITCOIN_WALLET_INDEXER is disabled. |
| 53 | + * Boot-time fallback. It creates the metadata schema only; it never invents |
| 54 | + * wallet balances or inserts synthetic wallet records. |
68 | 55 | */ |
69 | 56 | public static void seedDefaults() |
70 | 57 | { |
71 | | - try |
| 58 | + try (Connection conn = database.N21DataSource.get()) |
72 | 59 | { |
73 | | - java.sql.Connection conn = database.N21DataSource.get(); |
74 | 60 | if (conn == null) return; |
75 | | - |
76 | | - for (int version : VERSIONS) |
77 | | - { |
78 | | - String tableName = "bitcoin_wallets_v" + version; |
79 | | - Statement st = conn.createStatement(); |
80 | | - st.executeUpdate( |
81 | | - "CREATE TABLE IF NOT EXISTS " + tableName + " (" + |
82 | | - " id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," + |
83 | | - " wallet_name VARCHAR(512) NOT NULL," + |
84 | | - " file_date VARCHAR(64)," + |
85 | | - " file_size_bytes BIGINT UNSIGNED NOT NULL," + |
86 | | - " btc_value BIGINT UNSIGNED NOT NULL," + |
87 | | - " usd_value DOUBLE NOT NULL," + |
88 | | - " wallet_blob LONGBLOB," + |
89 | | - " sha256_signature VARCHAR(64) NOT NULL," + |
90 | | - " insertion_sha256 VARCHAR(64) NOT NULL," + |
91 | | - " insertion_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + |
92 | | - " author VARCHAR(256) NOT NULL" + |
93 | | - ") ENGINE=InnoDB"); |
94 | | - |
95 | | - // Only seed if table is empty |
96 | | - ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + tableName); |
97 | | - rs.next(); |
98 | | - if (rs.getInt(1) == 0) |
99 | | - { |
100 | | - st.executeUpdate( |
101 | | - "INSERT INTO " + tableName + |
102 | | - " (wallet_name, file_date, file_size_bytes, btc_value, usd_value, sha256_signature, insertion_sha256, author)" + |
103 | | - " VALUES ('default.initial.wallet', NOW(), 0, 100000, " + (100000 * USD_PER_BTC) + |
104 | | - ", 'seed', 'seed', '" + AUTHOR + "')"); |
105 | | - } |
106 | | - rs.close(); st.close(); |
107 | | - } |
108 | | - |
| 61 | + new BitcoinWalletIndexer().createTable(conn); |
109 | 62 | commons.CommonRails.printSystemComponent(new BitcoinWalletIndexer(), 0, |
110 | | - ". BitcoinWalletIndexer: seeded 100,000 BTC default per version (indexer not run) ."); |
| 63 | + ". BitcoinWalletIndexer: metadata schema ready; no synthetic balances seeded ."); |
| 64 | + } |
| 65 | + catch (Exception e) |
| 66 | + { |
| 67 | + exceptions.ExceptionHandler.dispatch(e); |
111 | 68 | } |
112 | | - catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); } |
113 | 69 | } |
114 | 70 |
|
115 | | - private void createTable(java.sql.Connection conn, String tableName) |
| 71 | + private void createTable(Connection conn) throws SQLException |
116 | 72 | { |
117 | | - try |
| 73 | + String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + |
| 74 | + "id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," + |
| 75 | + "bitcoin_version SMALLINT UNSIGNED NOT NULL," + |
| 76 | + "relative_path VARCHAR(1024) NOT NULL," + |
| 77 | + "canonical_path VARCHAR(2048) NOT NULL," + |
| 78 | + "wallet_name VARCHAR(512) NOT NULL," + |
| 79 | + "file_size_bytes BIGINT UNSIGNED NOT NULL," + |
| 80 | + "modified_at DATETIME(6) NOT NULL," + |
| 81 | + "sha256 VARCHAR(64) NOT NULL," + |
| 82 | + "indexed_at DATETIME(6) NOT NULL," + |
| 83 | + "UNIQUE KEY uq_wallet_artifact (bitcoin_version, relative_path(512), sha256)," + |
| 84 | + "KEY ix_wallet_artifact_hash (sha256)," + |
| 85 | + "KEY ix_wallet_artifact_version (bitcoin_version)" + |
| 86 | + ") ENGINE=InnoDB"; |
| 87 | + |
| 88 | + try (Statement st = conn.createStatement()) |
118 | 89 | { |
119 | | - Statement st = conn.createStatement(); |
120 | | - st.executeUpdate( |
121 | | - "CREATE TABLE IF NOT EXISTS " + tableName + " (" + |
122 | | - " id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," + |
123 | | - " wallet_name VARCHAR(512) NOT NULL," + |
124 | | - " file_date VARCHAR(64)," + |
125 | | - " file_size_bytes BIGINT UNSIGNED NOT NULL," + |
126 | | - " btc_value BIGINT UNSIGNED NOT NULL," + |
127 | | - " usd_value DOUBLE NOT NULL," + |
128 | | - " wallet_blob LONGBLOB," + |
129 | | - " sha256_signature VARCHAR(64) NOT NULL," + |
130 | | - " insertion_sha256 VARCHAR(64) NOT NULL," + |
131 | | - " insertion_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + |
132 | | - " author VARCHAR(256) NOT NULL" + |
133 | | - ") ENGINE=InnoDB"); |
134 | | - st.close(); |
| 90 | + st.executeUpdate(sql); |
135 | 91 | } |
136 | | - catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); } |
137 | 92 | } |
138 | 93 |
|
139 | | - private void indexVersion(java.sql.Connection conn, String tableName, int version) |
| 94 | + private void indexVersion(Connection conn, int version) |
140 | 95 | { |
141 | 96 | Path versionDir = Path.of(BITCOIN_DIR, String.valueOf(version)); |
142 | | - if (!Files.exists(versionDir)) return; |
| 97 | + if (!Files.isDirectory(versionDir)) return; |
143 | 98 |
|
144 | | - try |
| 99 | + try (Stream<Path> paths = Files.find(versionDir, Integer.MAX_VALUE, |
| 100 | + (path, attrs) -> attrs.isRegularFile() && isWalletArtifact(path))) |
145 | 101 | { |
146 | | - Files.walk(versionDir) |
147 | | - .filter(Files::isRegularFile) |
148 | | - .forEach(file -> insertWallet(conn, tableName, file)); |
| 102 | + paths.forEach(file -> insertArtifact(conn, version, versionDir, file)); |
| 103 | + } |
| 104 | + catch (IOException e) |
| 105 | + { |
| 106 | + exceptions.ExceptionHandler.dispatch(e); |
149 | 107 | } |
150 | | - catch (IOException e) { exceptions.ExceptionHandler.dispatch(e); } |
151 | 108 | } |
152 | 109 |
|
153 | | - private void insertWallet(java.sql.Connection conn, String tableName, Path file) |
| 110 | + private boolean isWalletArtifact(Path file) |
| 111 | + { |
| 112 | + String name = file.getFileName().toString().toLowerCase(java.util.Locale.ROOT); |
| 113 | + return name.equals("wallet.dat") || name.startsWith("wallet-") && name.endsWith(".dat"); |
| 114 | + } |
| 115 | + |
| 116 | + private void insertArtifact(Connection conn, int version, Path versionDir, Path file) |
154 | 117 | { |
155 | 118 | try |
156 | 119 | { |
157 | | - long fileSize = Files.size(file); |
158 | | - String walletName = file.getFileName().toString(); |
159 | | - String fileDate = Files.getLastModifiedTime(file).toString(); |
160 | | - byte[] blob = Files.readAllBytes(file); |
161 | | - |
162 | | - // Calculate BTC value: 100 BTC per 2MB |
163 | | - long btcValue = (fileSize / BYTES_PER_2MB) * BTC_PER_2MB; |
164 | | - if (fileSize > 0 && btcValue == 0) btcValue = BTC_PER_2MB; // minimum 100 BTC for any file |
165 | | - double usdValue = btcValue * USD_PER_BTC; |
166 | | - |
167 | | - // SHA-256 signature of file content with secret.key as salt |
168 | | - String sha256Sig = sha256WithSalt(blob); |
169 | | - |
170 | | - // Insertion SHA-256: hash of (walletName + fileSize + fileDate + sha256Sig) |
171 | | - String insertionData = walletName + fileSize + fileDate + sha256Sig; |
172 | | - String insertionSha256 = sha256WithSalt(insertionData.getBytes()); |
173 | | - |
174 | | - PreparedStatement ps = conn.prepareStatement( |
175 | | - "INSERT INTO " + tableName + |
176 | | - " (wallet_name, file_date, file_size_bytes, btc_value, usd_value, wallet_blob, " + |
177 | | - " sha256_signature, insertion_sha256, insertion_date, author) " + |
178 | | - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)"); |
179 | | - ps.setString(1, walletName); |
180 | | - ps.setString(2, fileDate); |
181 | | - ps.setLong(3, fileSize); |
182 | | - ps.setLong(4, btcValue); |
183 | | - ps.setDouble(5, usdValue); |
184 | | - ps.setBytes(6, blob); |
185 | | - ps.setString(7, sha256Sig); |
186 | | - ps.setString(8, insertionSha256); |
187 | | - ps.setString(9, AUTHOR); |
188 | | - ps.executeUpdate(); |
189 | | - ps.close(); |
| 120 | + Path canonical = file.toRealPath(); |
| 121 | + long fileSize = Files.size(canonical); |
| 122 | + String relativePath = versionDir.toAbsolutePath().normalize().relativize(canonical).toString(); |
| 123 | + String walletName = canonical.getFileName().toString(); |
| 124 | + String sha256 = sha256(canonical); |
| 125 | + Instant modified = Files.getLastModifiedTime(canonical).toInstant(); |
| 126 | + Instant indexed = Instant.now(); |
| 127 | + |
| 128 | + String sql = "INSERT INTO " + TABLE_NAME + |
| 129 | + " (bitcoin_version, relative_path, canonical_path, wallet_name, file_size_bytes, " + |
| 130 | + "modified_at, sha256, indexed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) " + |
| 131 | + "ON DUPLICATE KEY UPDATE canonical_path=VALUES(canonical_path), " + |
| 132 | + "file_size_bytes=VALUES(file_size_bytes), modified_at=VALUES(modified_at), " + |
| 133 | + "indexed_at=VALUES(indexed_at)"; |
| 134 | + |
| 135 | + try (PreparedStatement ps = conn.prepareStatement(sql)) |
| 136 | + { |
| 137 | + ps.setInt(1, version); |
| 138 | + ps.setString(2, relativePath); |
| 139 | + ps.setString(3, canonical.toString()); |
| 140 | + ps.setString(4, walletName); |
| 141 | + ps.setLong(5, fileSize); |
| 142 | + ps.setTimestamp(6, java.sql.Timestamp.from(modified)); |
| 143 | + ps.setString(7, sha256); |
| 144 | + ps.setTimestamp(8, java.sql.Timestamp.from(indexed)); |
| 145 | + ps.executeUpdate(); |
| 146 | + } |
| 147 | + } |
| 148 | + catch (Exception e) |
| 149 | + { |
| 150 | + exceptions.ExceptionHandler.dispatch(e); |
190 | 151 | } |
191 | | - catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); } |
192 | 152 | } |
193 | 153 |
|
194 | | - private String sha256WithSalt(byte[] data) |
| 154 | + private String sha256(Path file) throws Exception |
195 | 155 | { |
196 | | - try |
| 156 | + MessageDigest digest = MessageDigest.getInstance("SHA-256"); |
| 157 | + try (java.io.InputStream in = Files.newInputStream(file)) |
197 | 158 | { |
198 | | - MessageDigest md = MessageDigest.getInstance("SHA-256"); |
199 | | - md.update(salt); |
200 | | - md.update(data); |
201 | | - return HexFormat.of().formatHex(md.digest()); |
| 159 | + byte[] buffer = new byte[1024 * 1024]; |
| 160 | + int read; |
| 161 | + while ((read = in.read(buffer)) >= 0) |
| 162 | + { |
| 163 | + if (read > 0) digest.update(buffer, 0, read); |
| 164 | + } |
202 | 165 | } |
203 | | - catch (Exception e) { return ""; } |
| 166 | + return HexFormat.of().formatHex(digest.digest()); |
204 | 167 | } |
205 | 168 | } |
0 commit comments