;;;; gbw-signer.scm: Gales Bitcoin Wallet offline key generation and transaction signing ;;; J. Welsh, October 2017 - March 2020 ;;; Knobs (define *install-path* "/package/gbw-signer-3") (define *rng-port* (delay (open-input-file "/dev/urandom"))) ;; Minimum value for which to generate change outputs, as lubricant for sabotaged network that stifles transactions with small outputs regardless of fee. Per commits: ;; 8de9bb53af32f7f6b09c06f831f2c0a7b4e95303 ( http://archive.is/tE6nK ) ;; 6a4c196dd64da2fd33dc7ae77a8cdd3e4cf0eff1 ( http://archive.is/rALd9 ) (define *dust-threshold* 546) ;;; Libraries (define (resource path) (string-append *install-path* "/library/" path)) ;; Public EC precomputations ("make-cache" to generate) (define *cache-path* (resource "secp256k1.cache")) (load (resource "pkg.scm")) (define bignum (load-pkg (resource "bignum.scm"))) (import bignum decdigit->fix bn->hex hex->bn bytes->bn bn->fix fix->bn bn-zero? bn-even? bn-divrem rand-bn) (define ecdsa (load-pkg (resource "ecdsa.scm") bignum)) (import ecdsa secp256k1) (define bit-ops (load-pkg (resource "bit-ops.scm"))) (import bit-ops hex->bytes bytes->hex) (define hashes (load-pkg (resource "hashes.scm") bit-ops)) (import hashes ripemd160 sha256) ;;; Helpers ;; Poor man's debugger (define (trace . args) (write args) (newline) (flush-output-port)) (define *null-byte* (integer->char 0)) (define (left-pad s len char) (string-append (make-string (- len (string-length s)) char) s)) (define (pad-hex-256be s) (left-pad s 64 #\0)) ;; 256 bits = 64 nibbles (define (pad-bytes-256be s) (left-pad s 32 *null-byte*)) (define (string-head s n) (substring s 0 n)) (define (string-tail s n) (substring s n (string-length s))) (define (string-reverse s) (let ((r (make-string (string-length s))) (last (- (string-length s) 1))) (do ((i 0 (+ i 1))) ((> i last) r) (string-set! r i (string-ref s (- last i)))))) (define (string-find s start chars) (let loop ((i start)) (cond ((= i (string-length s)) #f) ((memv (string-ref s i) chars) i) (else (loop (+ i 1)))))) (define (string-split s chars) (let loop ((i 0)) (let ((cut (string-find s i chars))) (cond ((not cut) (if (= i (string-length s)) '() (list (string-tail s i)))) ((= i cut) (loop (+ i 1))) (else (cons (substring s i cut) (loop (+ cut 1)))))))) (define (bn->bytes n) ;; hacky... (hex->bytes (bn->hex n))) (define (integer->bytes n) ;; hacky... (hex->bytes (number->string n 16))) (define (bn-pack-le nbytes) (let ((nibbles (* nbytes 2))) (lambda (n) (string-reverse (hex->bytes (left-pad (bn->hex n) nibbles #\0)))))) (define (string->bytevec s) (let* ((len (string-length s)) (v (make-vector len))) (do ((i 0 (+ i 1))) ((= i len) v) (vector-set! v i (char->integer (string-ref s i)))))) (define (byte-string->bn s) (bytes->bn (string->bytevec s))) (define (byte-string . bytes) (list->string (map integer->char bytes))) (define (display-line x) (display x) (newline) (flush-output-port)) (define (read-line port) (let loop ((acc '())) (let ((c (read-char port))) (cond ((eof-object? c) (if (null? acc) c (list->string (reverse acc)))) ((char=? c #\newline) (list->string (reverse acc))) (else (loop (cons c acc))))))) (define (read-all port) (do ((c (read-char port) (read-char port)) (acc '() (cons c acc))) ((eof-object? c) (list->string (reverse acc))))) ;; gscm so far has a subprocess extension but not a full POSIX or condition system. (define (file-exists? path) (call-with-values (lambda () (open-subprocess "/usr/bin/test" "test" "-e" path)) (lambda (pid i o) (close-output-port o) (read-all i) (close-input-port i) (zero? (wait-subprocess pid))))) (define (read-priv-key-hex port) (let ((k (read-line port))) (if (not (= (string-length k) 64)) (error "ill-formed key")) k)) (define (get-priv-key-by-address addr) (hex->bn (call-with-input-file (string-append "keys/" addr) read-priv-key-hex))) (define (have-key? addr) (file-exists? (string-append "keys/" addr))) (define (sha256d x) (sha256 (sha256 x))) (define (hash160 x) (ripemd160 (sha256 x))) (define (shuffle l) (define v (list->vector l)) ;; Knuth/Fisher-Yates shuffle in place (do ((i (- (vector-length v) 1) (- i 1))) ((negative? i)) (let ((j (bn->fix ((rand-bn (fix->bn (+ i 1))) (force *rng-port*)))) (t (vector-ref v i))) (vector-set! v i (vector-ref v j)) (vector-set! v j t))) (vector->list v)) (define *cache* (delay ((secp256k1 'read-cache) *cache-path*))) (define (pop-arg) (if (null? *args*) (error "too few arguments")) (let ((a (car *args*))) (set! *args* (cdr *args*)) a)) ;;; Base58 [TRB/base58.h] (define *bn58* (fix->bn 58)) (define (b58digit i) ;; alphanumeric - 0OIl (string-ref "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" i)) (define b58digit->integer (let ((inverse (make-vector 256 #f))) (do ((i 0 (+ i 1))) ((= i 58)) (vector-set! inverse (char->integer (b58digit i)) i)) (lambda (d) (or (vector-ref inverse (char->integer d)) (error "invalid base58 digit:" d))))) (define (encode-base58 data) (let loop ((n (byte-string->bn data)) (acc '())) (if (not (bn-zero? n)) (bn-divrem n *bn58* (lambda (q r) (loop q (cons (bn->fix r) acc)))) ;; Leading zero bytes pass through one-to-one (do ((i 0 (+ i 1)) (acc acc (cons 0 acc))) ((or (= i (string-length data)) (not (zero? (char->integer (string-ref data i))))) (list->string (map b58digit acc))))))) (define (decode-base58 data) (do ((digits (map b58digit->integer (string->list data)) (cdr digits)) (zeros 0 (+ 1 zeros))) ((or (null? digits) (not (zero? (car digits)))) (if (null? digits) (make-string zeros *null-byte*) (do ((acc (car digits) (+ (* acc 58) (car digits))) (digits (cdr digits) (cdr digits))) ((null? digits) (string-append (make-string zeros *null-byte*) (integer->bytes acc)))))))) ;; Well-known Base58Check payload type tags (define *b58-version-p2pkh* 0) (define *b58-version-p2sh* 5) (define *b58-version-secret* 128) (define *b58-version-testnet-p2pkh* 111) (define *b58-version-testnet-p2sh* 196) (define *b58-version-testnet-secret* 239) (define (encode-base58check version data) (let ((data (string-append (byte-string version) data))) (encode-base58 (string-append data (string-head (sha256d data) 4))))) (define (decode-base58check data) (let* ((data (decode-base58 data)) (cut (- (string-length data) 4))) (if (negative? cut) (error "decode-base58check: checksum too short")) (let ((payload (string-head data cut)) (check (string-tail data cut))) (if (not (string=? check (string-head (sha256d payload) 4))) (error "decode-base58check: bad checksum")) payload))) (define (point->address p form) (encode-base58check *b58-version-p2pkh* (hash160 (encode-point p form)))) ;;; OpenSSL binary encodings ;; EC point (public key) [1] ;; Leading byte encodes form: ;; (0) the point at infinity ;; (2 x) compressed, even y ;; (3 x) compressed, odd y ;; (4 x y) uncompressed ;; (6 x y) hybrid, even y ;; (7 x y) hybrid, odd y (define (encode-point p form) (if (eq? p 'inf) (byte-string 0) (let ((x (car p)) (y (cdr p)) (pack (lambda (n) (pad-bytes-256be (bn->bytes n))))) (case form ((compressed) (string-append (byte-string (if (bn-even? y) 2 3)) (pack x))) ((uncompressed) (string-append (byte-string 4) (pack x) (pack y))) ((hybrid) ;; Hybrid form seems entirely pointless to me, but is included for completeness. (string-append (byte-string (if (bn-even? y) 6 7)) (pack x) (pack y))) (else (error "encode-point: bad form:" form)))))) ;; DERP encoding: a subset of the Distinguished Encoding Rules of Abstract Syntax Notation One (define (derp-encapsulate tag constructed? contents) ;; Class assumed to be 0 (universal) ;; Tag assumed to be 0-30 ;; Length assumed to be 0-127 (string-append (byte-string (if constructed? (+ tag 32) tag) (string-length contents)) contents)) (define (derp-contents s) (string-tail s 2)) (define (encode-derp-integer n) ;; INTEGER is encoded base 256, two's complement, big endian, with the minimal number of octets, except zero which is a single zero octet (let* ((b (bn->bytes n)) (pad? (or (zero? (string-length b)) (> (char->integer (string-ref b 0)) 127)))) (derp-encapsulate 2 #f (if pad? (string-append (byte-string 0) b) b)))) (define (decode-derp-integer s start) (byte-string->bn (derp-contents s))) (define (encode-derp-sequence . items) ;; SEQUENCE is encoded as the concatenation of the respective encoded items (derp-encapsulate 16 #t (apply string-append items))) (define (encode-derp-sig r s) ;; [SSL/crypto/ecdsa/ecs_asn1.c], [SSL/crypto/asn1/x_bignum.c] (encode-derp-sequence (encode-derp-integer r) (encode-derp-integer s))) ;;; Bitcoin transactions (define (alist fields) (lambda vals (map list fields vals))) (define (alist-get l field . default) (let ((l (assq field l))) (if l (cadr l) (if (null? default) (error "field not found" field) (car default))))) (define (alist-extend l field val) (cons (list field val) l)) (define make-input (alist '(txid index value sig-script))) (define make-output (alist '(value address))) (define make-prevout (alist '(txid index value address))) (define (prevout->unsigned-input p) (alist-extend p 'sig-script '())) (define *tx-version* (byte-string 1 0 0 0)) (define *lock-time* (byte-string 0 0 0 0)) (define (sign-transaction prev-outputs new-outputs) ;; [TRB/script.cpp] SignatureHash, SignSignature, Solver ;; [TRB/key.h] CKey::Sign (force *cache*) (let ((unsig-ins (map prevout->unsigned-input prev-outputs)) (encoded-prefix (string-append *tx-version* (encode-var-int (length prev-outputs)))) (encoded-suffix (string-append (encode-outputs new-outputs) *lock-time*))) (do ((unsig-ins unsig-ins (cdr unsig-ins)) (prev-outputs prev-outputs (cdr prev-outputs)) (enc-unsig-ins (map encode-input unsig-ins) (cdr enc-unsig-ins)) (enc-unsig-ins-head '() `(,@enc-unsig-ins-head ,(car enc-unsig-ins))) (signed-ins '() (let* ((prev-addr (alist-get (car prev-outputs) 'address)) (m (apply string-append `(,encoded-prefix ,@enc-unsig-ins-head ,(encode-input (alist-extend (car unsig-ins) 'sig-script (output-script prev-addr))) ,@(cdr enc-unsig-ins) ,encoded-suffix ,*sighash-all/u32*))) (hash (string->bytevec (sha256d m))) (priv (get-priv-key-by-address prev-addr)) (pub ((secp256k1 'priv->pub) priv)) (sig-script ((secp256k1 'sign) hash priv (force *rng-port*) (lambda (r s) (input-script r s pub 'uncompressed))))) (cons (alist-extend (car unsig-ins) 'sig-script sig-script) signed-ins)))) ((null? unsig-ins) (apply string-append `(,encoded-prefix ,@(map encode-input (reverse signed-ins)) ,encoded-suffix)))))) (define (estimate-tx-size prev-outputs new-outputs) (let ((n-ins (length prev-outputs))) (+ 8 ;; version, locktime (string-length (encode-var-int n-ins)) (* 181 n-ins) ;; txid, index, r, s, x, y, overhead (string-length (encode-outputs new-outputs))))) ;;; Script, per [TRB/script.h] (define (enumerate start items . more) (let loop ((i start) (l items) (more more) (acc '())) (if (null? l) (if (null? more) (reverse acc) (loop (car more) (cadr more) (cddr more) acc)) (loop (+ i 1) (cdr l) more (cons (list (car l) i) acc))))) (define *script-ops* (enumerate 0 '(op_0) ;; aka false ;; 1-75 indicate length of following string to push 76 '(op_pushdata1 op_pushdata2 op_pushdata4 op_1negate op_reserved op_1 op_2 op_3 op_4 op_5 op_6 op_7 op_8 op_9 op_10 op_11 op_12 op_13 op_14 op_15 op_16 ;; control op_nop op_ver op_if op_notif op_verif op_vernotif op_else op_endif op_verify op_return ;; stack ops op_toaltstack op_fromaltstack op_2drop op_2dup op_3dup op_2over op_2rot op_2swap op_ifdup op_depth op_drop op_dup op_nip op_over op_pick op_roll op_rot op_swap op_tuck ;; splice ops op_cat op_substr op_left op_right op_size ;; bit logic op_invert op_and op_or op_xor op_equal op_equalverify op_reserved1 op_reserved2 ;; numeric op_1add op_1sub op_2mul op_2div op_negate op_abs op_not op_0notequal op_add op_sub op_mul op_div op_mod op_lshift op_rshift op_booland op_boolor op_numequal op_numequalverify op_numnotequal op_lessthan op_greaterthan op_lessthanorequal op_greaterthanorequal op_min op_max op_within ;; crypto op_ripemd160 op_sha1 op_sha256 op_hash160 op_hash256 op_codeseparator op_checksig op_checksigverify op_checkmultisig op_checkmultisigverify ;; expansion op_nop1 op_nop2 op_nop3 op_nop4 op_nop5 op_nop6 op_nop7 op_nop8 op_nop9 op_nop10) 253 ;; template matching params '(op_pubkeyhash op_pubkey op_invalidopcode))) ;; Human-readable, bijective script decoding (define (encode-script s) (define (encode-op name) (byte-string (cadr (or (assoc name *script-ops*) (error "bad opcode name" name))))) (let loop ((s s) (acc '())) (if (null? s) (apply string-append (reverse acc)) (let ((op (car s))) (if (integer? op) (loop (cddr s) (cons (hex->bytes (cadr s)) (cons (byte-string op) acc))) (let ((acc (cons (encode-op op) acc))) (if (memq op '(op_pushdata1 op_pushdata2 op_pushdata4)) (let* ((data (hex->bytes (cadr s))) (len (pack-ule (cond ((eq? op 'op_pushdata1) 1) ((eq? op 'op_pushdata2) 2) (else 4)) (string-length data)))) (loop (cddr s) (cons data (cons len acc)))) (loop (cdr s) acc)))))))) (define (script-push bytes) (let ((len (string-length bytes))) (list (cond ((< len 76) len) ((< len (expt 2 8)) op_pushdata1) ((< len (expt 2 16)) op_pushdata2) ((< len (expt 2 32)) op_pushdata4) (else (error "script-push overflow"))) (bytes->hex bytes)))) (define (output-script address) (let ((address (decode-base58check address))) (if (not (= (string-length address) 21)) (error "bad address length")) (let ((version (char->integer (string-ref address 0))) (hash (string-tail address 1))) (cond ((or (= version *b58-version-p2pkh*) (= version *b58-version-testnet-p2pkh*)) `(op_dup op_hash160 ,@(script-push hash) op_equalverify op_checksig)) (else (error "bad address type")))))) (define *sighash-all/u32* (byte-string 1 0 0 0)) (define *sighash-all/u8* (byte-string 1)) (define (input-script r s pubkey form) ;; [TRB/script.cpp] Solver (5-arg form) and related (append (script-push (string-append (encode-derp-sig r s) *sighash-all/u8*)) (script-push (encode-point pubkey form)))) ;;; Wire protocol (define (pack-ule nbytes n) (do ((k nbytes (- k 1)) (n n (quotient n 256)) (bytes '() (cons (integer->char (remainder n 256)) bytes))) ((zero? k) (if (zero? n) (list->string (reverse bytes)) (error "pack-ule: overflow"))))) ;; Encode an unsigned integer under 2^64 in Bitcoin's variable-length format (define encode-var-int (let ((pack (lambda (tag nbytes) (let ((tag (byte-string tag))) (lambda (n) (string-append tag (pack-ule nbytes n))))))) (let ((c2 (expt 2 16)) (c3 (expt 2 32)) (c4 (expt 2 64)) (pack2 (pack 253 2)) (pack4 (pack 254 4)) (pack8 (pack 255 8))) (lambda (n) (cond ((< n 253) (byte-string n)) ((< n c2) (pack2 n)) ((< n c3) (pack4 n)) ((< n c4) (pack8 n)) (else (error "encode-var-int overflow"))))))) (define (encode-var-str s) (string-append (encode-var-int (string-length s)) s)) (define *input-seq-final* (byte-string 255 255 255 255)) (define (encode-input i) ;; [TRB/main.h] CTxIn (string-append (alist-get i 'txid) (pack-ule 4 (alist-get i 'index)) (encode-var-str (encode-script (alist-get i 'sig-script))) *input-seq-final*)) (define (encode-output o) ;; [TRB/main.h] CTxOut (string-append (pack-ule 8 (alist-get o 'value)) (encode-var-str (encode-script (output-script (alist-get o 'address)))))) (define (encode-outputs outs) (apply string-append (encode-var-int (length outs)) (map encode-output outs))) ;;; "Human" encodings (define *satoshi/coin* (expt 10 8)) ;; Quadratic algorithm! (define (dec->integer s) (do ((i 0 (+ i 1)) (acc 0 (+ (* acc 10) (decdigit->fix (string-ref s i))))) ((= i (string-length s)) acc))) ;; Losslessly convert BTC value from unsigned decimal string to satoshi integer (define (coin->integer s) (let ((cut (string-find s 0 '(#\.)))) (let ((ipart (if cut (string-head s cut) s)) (fpart (if cut (string-tail s (+ cut 1)) ""))) (let ((flen (string-length fpart))) (if (= 0 (string-length ipart) flen) (error "not a number:" s)) (if (> flen 8) (error "excess coin precision:" s)) (+ (* (dec->integer ipart) *satoshi/coin*) (* (dec->integer fpart) (expt 10 (- 8 flen)))))))) (define (integer->coin i) (if (negative? i) (string-append "-" (integer->coin (- i))) (string-append (number->string (quotient i *satoshi/coin*)) "." (left-pad (number->string (remainder i *satoshi/coin*)) 8 #\0)))) (define *tab* (integer->char 9)) (define *whitespace* (list #\space *tab*)) (define (read-prevout port) (let ((line (read-line port))) (if (eof-object? line) line (let ((parts (string-split line *whitespace*))) (if (< (length parts) 4) (error "missing field(s) in output:" line)) (apply (lambda (address value txid index . comment) (make-prevout (string-reverse (hex->bytes txid)) (dec->integer index) (coin->integer value) address)) parts))))) (define (write-new-outputs txid outputs port) (define hex-txid (bytes->hex (string-reverse txid))) (do ((index 0 (+ index 1)) (outputs outputs (cdr outputs))) ((null? outputs)) (let* ((o (car outputs)) (addr (alist-get o 'address))) (if (have-key? addr) (begin (for-each (lambda (x) (display x port) (write-char #\space port)) (list addr (integer->coin (alist-get o 'value)) hex-txid index)) (display "#unconfirmed" port) (newline port)))))) (define (update-prevouts saved-lines txid new-outputs) (call-with-output-file "outputs" (lambda (port) (write-new-outputs txid new-outputs port) (display saved-lines port)))) (define (emit-rawtx rtx) (with-output-to-file "transactions" (lambda () (display-line (bytes->hex rtx))) 'append)) ;;; Commands (define (make-cache) ((secp256k1 'write-cache) *cache-path*)) (define (gen-key) (force *cache*) (let* ((priv ((secp256k1 'gen-priv-key) (force *rng-port*))) (addr (point->address ((secp256k1 'priv->pub) priv) 'uncompressed))) (with-output-to-file (string-append "keys/" addr) (lambda () (display-line (pad-hex-256be (bn->hex priv))))) (display-line addr))) (define (send) (define (get-value o) (alist-get o 'value)) (define outputs (do ((acc '() (let* ((addr (pop-arg)) (val (coin->integer (pop-arg)))) (cons (make-output val addr) acc)))) ((null? *args*) (reverse acc)))) (define output-subtotal (delay (apply + (map get-value outputs)))) (define change-addr (call-with-input-file "change" read-line)) (define outputs+change-dummy (delay (cons (make-output 1 change-addr) outputs))) (define fee/kb (coin->integer (call-with-input-file "fee" read-line))) (define prevouts-port (open-input-file "outputs")) (define (add-input prevouts) (define p (read-prevout prevouts-port)) (if (eof-object? p) (error "insufficient funds")) (let* ((prevouts (cons p prevouts)) (prevout-total (apply + (map get-value prevouts))) (size (estimate-tx-size prevouts (force outputs+change-dummy))) ;; fee computation per wallet.cpp CWallet::CreateTransaction (fee (* fee/kb (+ 1 (quotient size 1000)))) (change-val (- prevout-total (force output-subtotal) fee))) (if (negative? change-val) (add-input prevouts) (let* ((outputs (shuffle (if (< change-val *dust-threshold*) outputs (cons (make-output change-val change-addr) outputs)))) (rawtx (sign-transaction (reverse prevouts) outputs))) (emit-rawtx rawtx) (update-prevouts (read-all prevouts-port) (sha256d rawtx) outputs) (close-input-port prevouts-port))))) (add-input '())) (define (priv2addr) (force *cache*) (display-line (point->address ((secp256k1 'priv->pub) (hex->bn (read-priv-key-hex (current-input-port)))) 'uncompressed))) (define (priv2wif) (display-line (encode-base58check *b58-version-secret* (hex->bytes (read-priv-key-hex (current-input-port)))))) (define (import-wif) (force *cache*) (let ((line (read-line (current-input-port)))) (if (not (eof-object? line)) (let* ((data (decode-base58check line)) (form (case (string-length data) ((33) 'uncompressed) ((34) (if (= (char->integer (string-ref data 33)) 1) 'compressed (error "bad WIF compression tag"))) (else (error "bad WIF key length"))))) (let ((version (char->integer (string-ref data 0))) (key-bytes (substring data 1 33))) (if (not (or (= version *b58-version-secret*) (= version *b58-version-testnet-secret*))) (error "bad WIF version byte")) (let ((addr (point->address ((secp256k1 'priv->pub) (byte-string->bn key-bytes)) form))) (with-output-to-file (string-append "keys/" addr) (lambda () (display-line (bytes->hex (if (eq? form 'uncompressed) key-bytes (string-append key-bytes (byte-string 1))))))) (display-line addr) (import-wif))))))) (define (test) (define (test-equal a b) (display-line (if (equal? a b) 'pass `(fail ,a != ,b)))) ;; [TRB/test/base58_tests.cpp] (display-line "Base58 encode and decode:") (for-each (lambda (r) (let* ((bytes (hex->bytes (car r))) (enc (encode-base58 bytes)) (dec (decode-base58 enc))) (test-equal enc (cadr r)) (test-equal dec bytes))) '(("" "") ("61" "2g") ("626262" "a3gV") ("636363" "aPEr") ("73696d706c792061206c6f6e6720737472696e67" "2cFupjhnEsSn59qHXstmK2ffpLv2") ("00eb15231dfceb60925886b67d065299925915aeb172c06647" "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L") ("516b6fcd0f" "ABnLTmg") ("bf4f89001e670274dd" "3SEo3LWLoPntC") ("572e4794" "3EFU7m") ("ecac89cad93923c02321" "EJDM8drfXA6uyA") ("10c8511e" "Rt5zm") ("00000000000000000000" "1111111111"))) (force *cache*) ;; [PRB/test/key_tests.cpp] (display-line "Key and address generation:") (for-each (lambda (r) (let* ((key-bytes (string-tail (decode-base58check (car r)) 1)) (addr (point->address ((secp256k1 'priv->pub) (byte-string->bn key-bytes)) 'uncompressed))) (test-equal addr (cadr r)))) '(("5HxWvvfubhXpYYpS3tJkw6fq9jE9j18THftkZjHHfmFiWtmAbrj" "1QFqqMUD55ZV3PJEJZtaKCsQmjLT6JkjvJ") ("5KC4ejrDjv152FGwP386VD1i2NYc5KkfSMyv1nGy1VGDxGHqVY3" "1F5y5E5FMc5YzdJtB9hLaUe43GDxEKXENJ")))) (define (help) (display-line "Usage: gbw-signer COMMAND [ARGS]") (newline) (display-line "Available commands:") (for-each (lambda (c) (newline) (display (car c)) (display " - ") (display-line (caddr c))) commands)) ;;; CLI dispatch (define commands `(("make-cache" ,make-cache "Generate the required cache file of public elliptic curve precomputations (power-of-2 powers of the generator point).") ("gen-key" ,gen-key "Generate a new key/address, storing the hex private key to a file in the 'keys' directory named by the address.") ("send" ,send "Sign a transaction paying the pairwise (address, value) arguments, spending previous outputs described in the 'outputs' table in order, with change going to the address contained in the 'change' file and fee in BTC/kB indicated in the 'fee' file. Rewrite 'outputs' to replace the spent ones with any new self-outputs (those paying addresses found in the 'keys' directory, such as change). Append the hex raw transaction to the 'transactions' file.") ("priv2addr" ,priv2addr "Print address computed from a hex private key fed to standard input.") ("priv2wif" ,priv2wif "Print WIF encoding of a hex private key fed to standard input.") ("import-wif" ,import-wif "Import WIF private key fed to standard input into the 'keys' directory by converting to hex and computing the address, including any compression tag (though such tag is currently unsupported by other commands).") ("test" ,test "Run the (rather limited) test suite.") ("help" ,help "Show this usage."))) (if (not (null? *args*)) ;; allows loading at REPL (let* ((prog-name (pop-arg)) (cmd (if (null? *args*) "help" (pop-arg))) (entry (assoc cmd commands))) (if entry ((cadr entry)) (error "gbw-signer: command not found:" cmd)))) ;;; Notes ; [1] While apparently simple, point encoding is one of the more dangerous parts of the code, as any incompatibility could result in seemingly valid addresses that turn out to be unspendable after you've already buried your treasure there. (Likewise the hash functions.) Verifying requires quite the code walk: ; ; [TRB/key.h] CKey::GetPubKey calls ; [SSL/crypto/ec/ec_asn1.c] i2o_ECPublicKey (which, filename notwithstanding, does not involve ASN.1), which calls ; [SSL/crypto/ec/ec_oct.c] EC_POINT_point2oct, which calls ; [SSL/crypto/ec/ecp_oct.c] ec_GFp_simple_point2oct, in which is found the encoding used here, assuming (group->meth->flags & EC_FLAGS_DEFAULT_OCT) and (group->meth->field_type == NID_X9_62_prime_field). "group" is the group field of the EC_KEY passed to i2o_ECPublicKey, initialized in ; [TRB/key.h] CKey::CKey by calling ; [SSL/crypto/ec/ec_key.c] EC_KEY_new_by_curve_name(NID_secp256k1), which initializes group by calling ; [SSL/crypto/ec/ec_curve.c] EC_GROUP_new_by_curve_name on the same NID, which calls ; [id.] ec_group_new_from_data on the member of the constant curve_list with matching NID. Since the curve_list item in question has a data field of &_EC_SECG_PRIME_256K1.h (an EC_CURVE_DATA constant having field_type of NID_X9_62_prime_field) and meth field of 0, the branch is taken that calls ; [SSL/crypto/ec/ec_cvt.c] EC_GROUP_new_curve_GFp, which initializes meth to either ; [SSL/crypto/ec/ecp_nist.c] EC_GFp_nist_method() or ; [SSL/crypto/ec/ecp_mont.c] EC_GFp_mont_method(), by calling ; [SSL/crypto/ec/ec_lib.c] EC_GROUP_new. In both cases the result is a constant EC_METHOD having the above required flags and field_type, assuming OPENSSL_FIPS is not defined. ; ; Even this does not constitute a proof, due to the possibility of mutation by seemingly-unrelated functions. ; ; An implication of this encoding is that even though [TRB] only emits uncompressed form, and no implementation has ever to my knowledge used hybrid form, there are three P2PKH addresses spendable by any given private key, and a consensus-compatible verifier must support all forms. ;;; References ; [TRB] The Real Bitcoin, version 0.5.4-RELEASE. http://thebitcoin.foundation/ ; [SSL] OpenSSL, version 1.0.1g. ; [PRB] Power Ranger Bitcoin, version 0.11.