diff --git a/etc/english.json b/etc/english.json new file mode 100644 index 00000000..5bc5c639 --- /dev/null +++ b/etc/english.json @@ -0,0 +1,2050 @@ +[ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo" +] diff --git a/lib/bcoin.js b/lib/bcoin.js index 6ef8958e..c80cb2e4 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -1,7 +1,7 @@ var bcoin = exports; var elliptic = require('elliptic'); -bcoin.ecdsa = elliptic.ecdsa(elliptic.nist.secp256k1); +bcoin.ecdsa = elliptic.ec('secp256k1'); bcoin.utils = require('./bcoin/utils'); bcoin.bloom = require('./bcoin/bloom'); bcoin.protocol = require('./bcoin/protocol'); @@ -13,3 +13,6 @@ bcoin.chain = require('./bcoin/chain'); bcoin.wallet = require('./bcoin/wallet'); bcoin.peer = require('./bcoin/peer'); bcoin.pool = require('./bcoin/pool'); +bcoin.hd = require('./bcoin/hd'); + +bcoin.protocol.network.set(process.env.BCOIN_NETWORK || 'main'); diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index cce5b0f9..308f6925 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -3,7 +3,7 @@ var EventEmitter = require('events').EventEmitter; var bcoin = require('../bcoin'); var constants = bcoin.protocol.constants; -var preload = bcoin.protocol.preload; +var network = bcoin.protocol.network; var utils = bcoin.utils; var assert = utils.assert; @@ -26,6 +26,7 @@ function Chain(options) { }; this.orphan = { map: {}, + bmap: {}, count: 0 }; this.index = { @@ -37,6 +38,20 @@ function Chain(options) { }; this.request = new utils.RequestCache(); + var preload = network.preload; + + // Start from the genesis block + // if we're a full node. + if (this.options.fullNode) { + preload = { + v: preload.v, + type: preload.type, + hashes: preload.hashes.slice(0, 1), + ts: preload.ts.slice(0, 1), + heights: preload.heights.slice(0, 1) + }; + } + this.fromJSON(preload); // Last TS after preload, needed for fill percent @@ -53,25 +68,35 @@ function compareTs(a, b) { } Chain.prototype._init = function _init() { + var self = this; + if (!this.storage) return; + utils.nextTick(function() { + self.emit('debug', 'Chain is loading.'); + }); + this.loading = true; - var self = this; + var s = this.storage.createReadStream({ start: this.prefix, end: this.prefix + 'z' }); + s.on('data', function(data) { var hash = data.key.slice(self.prefix.length); self._addIndex(hash, data.value.ts, data.value.height); }); + s.on('error', function(err) { self.emit('error', err); }); + s.on('end', function() { self.loading = false; self.emit('load'); + self.emit('debug', 'Chain successfully loaded.'); }); }; @@ -222,6 +247,7 @@ Chain.prototype.add = function add(block) { if (!this._probeIndex(hash, block.ts) && !prevProbe) { this.orphan.count++; this.orphan.map[prev] = block; + this.orphan.bmap[block.hash('hex')] = block; var range = this._getRange(hash, block.ts, true); var hashes = this.index.hashes.slice(range.start, range.end + 1); @@ -247,6 +273,7 @@ Chain.prototype.add = function add(block) { // We have orphan child for this block - add it to chain block = this.orphan.map[hash]; delete this.orphan.map[hash]; + delete this.orphan.bmap[block.hash('hex')]; this.orphan.count--; } while (true); @@ -388,6 +415,65 @@ Chain.prototype.getLast = function getLast(cb) { return cb(this.index.hashes[this.index.hashes.length - 1]); }; +Chain.prototype.getStartHeight = function getStartHeight() { + if (!this.options.fullNode) { + if (this.options.startHeight != null) { + return this.options.startHeight; + } + return 0; + } + return this.index.heights[this.index.heights.length - 1]; +}; + +Chain.prototype.locatorHashes = function(index) { + var chain = this.index.hashes; + var hashes = []; + var top = chain.length - 1; + var step = 1; + var i; + + if (typeof index === 'string') { + for (i = top; i >= 0; i--) { + if (chain[i] === index) { + top = i; + break; + } + } + } else if (typeof index === 'number') { + top = index; + } + + i = top; + for (;;) { + hashes.push(chain[i]); + i = i - step; + if (i <= 0) { + hashes.push(chain[0]); + break; + } + if (hashes.length >= 10) + step *= 2; + } + + return hashes; +}; + +Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { + var self = this; + + if (typeof hash !== 'string') { + hash = hash.hash('hex'); + } + + var orphans = this.orphan.bmap; + + var orphanRoot = hash; + while (orphans[orphanRoot.prevBlock]) + orphanRoot = orphans[orphanRoot.prevBlock]; + + return orphanRoot; +}; + Chain.prototype.toJSON = function toJSON() { var keep = 1000; @@ -432,6 +518,7 @@ Chain.prototype.toJSON = function toJSON() { return { v: 1, type: 'chain', + network: network.type, hashes: first.hashes.concat(last.hashes), ts: first.ts.concat(last.ts), heights: first.heights.concat(last.heights) @@ -441,16 +528,18 @@ Chain.prototype.toJSON = function toJSON() { Chain.prototype.fromJSON = function fromJSON(json) { assert.equal(json.v, 1); assert.equal(json.type, 'chain'); + if (json.network) + assert.equal(json.network, network.type); this.index.hashes = json.hashes.slice(); this.index.ts = json.ts.slice(); this.index.heights = json.heights.slice(); if (this.index.bloom) this.index.bloom.reset(); else - this.index.bloom = new bcoin.bloom(28 * 1024 * 1024, 16, 0xdeadbee0); + this.index.bloom = new bcoin.bloom(28 * 1024 * 1024, 16, 0xdeadbeef); if (this.index.hashes.length === 0) - this.add(new bcoin.block(constants.genesis, 'block')); + this.add(new bcoin.block(network.genesis, 'block')); for (var i = 0; i < this.index.hashes.length; i++) { this.index.bloom.add(this.index.hashes[i], 'hex'); diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js new file mode 100644 index 00000000..4297e55e --- /dev/null +++ b/lib/bcoin/hd.js @@ -0,0 +1,686 @@ +/** + * hd.js - hierarchical determistic seeds and keys (BIP32, BIP39) + * https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki + * https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki + */ + +/** + * Code adapted from bitcore-lib: + * https://github.com/bitpay/bitcore-lib/blob/master/lib/hdprivatekey.js + * https://github.com/bitpay/bitcore-lib/blob/master/lib/hdpublickey.js + * https://github.com/ryanxcharles/fullnode/blob/master/lib/bip32.js + * + * Copyright (c) 2013-2015 BitPay, Inc. + * + * Parts of this software are based on Bitcoin Core + * Copyright (c) 2009-2015 The Bitcoin Core developers + * + * Parts of this software are based on fullnode + * Copyright (c) 2014 Ryan X. Charles + * Copyright (c) 2014 reddit, Inc. + * + * Parts of this software are based on BitcoinJS + * Copyright (c) 2011 Stefan Thomas + * + * Parts of this software are based on BitcoinJ + * Copyright (c) 2011 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +var hd = exports; + +/** + * Modules + */ + +var bcoin = require('../bcoin'); +var hash = require('hash.js'); +var bn = require('bn.js'); +var elliptic = require('elliptic'); +var utils = bcoin.utils; +var assert = utils.assert; +var network = bcoin.protocol.network; + +var EventEmitter = require('events').EventEmitter; + +var english = require('../../etc/english.json'); + +var ec = elliptic.curves.secp256k1; + +/** + * HD Seeds + */ + +function HDSeed(options) { + if (!(this instanceof HDSeed)) + return new HDSeed(options); + + this.bits = options.bits || 128; + this.entropy = options.entropy || HDSeed._entropy(this.bits / 8); + this.mnemonic = options.mnemonic || HDSeed._mnemonic(this.entropy); + if (options.passphrase !== undefined) { + this.seed = this.createSeed(options.passphrase); + } +} + +HDSeed.create = function(options) { + var obj = new HDSeed(options); + return obj.seed || obj; +}; + +HDSeed.prototype.createSeed = function(passphrase) { + this.passphrase = passphrase || ''; + return pbkdf2(this.mnemonic, 'mnemonic' + passphrase, 2048, 64); +}; + +HDSeed._entropy = function(size) { + return randomBytes(size); +}; + +HDSeed._mnemonic = function(entropy) { + var bin = ''; + for (var i = 0; i < entropy.length; i++) { + bin = bin + ('00000000' + entropy[i].toString(2)).slice(-8); + } + + var mnemonic = []; + for (i = 0; i < bin.length / 11; i++) { + var wi = parseInt(bin.slice(i * 11, (i + 1) * 11), 2); + mnemonic.push(english[wi]); + } + + return mnemonic.join(' '); +}; + +/** + * HD Keys + */ + +var HARDENED = 0x80000000; +var MAX_INDEX = 2 * HARDENED; +var MIN_ENTROPY = 128 / 8; +var MAX_ENTROPY = 512 / 8; +var PARENT_FINGER_PRINT_SIZE = 4; +var PATH_ROOTS = ['m', 'M', 'm\'', 'M\'']; + +/** + * HD Private Key + */ + +function HDPriv(options) { + var data; + + if (!(this instanceof HDPriv)) + return new HDPriv(options); + + if (!options) + options = { seed: new HDSeed({ passphrase: '' }) }; + + if (typeof options === 'string' && options.indexOf('xprv') === 0) + options = { xkey: options }; + + if (options.passphrase) + options.seed = new HDSeed({ passphrase: options.passphrase }); + + if (options.seed) { + if (typeof options.seed === 'object' && !(options.seed instanceof HDSeed)) { + options.seed = new HDSeed(options.seed); + } + this.seed = options.seed; + data = this._seed(options.seed); + } else if (options.xkey) { + data = this._unbuild(options.xkey); + } else { + data = options; + } + + data = this._normalize(data, network.prefixes.xprivkey); + + this.data = data; + + this._build(data); +} + +HDPriv.prototype._normalize = function(data, version) { + var b; + + data.version = version || network.prefixes.xprivkey; + data.version = +data.version; + + data.depth = +data.depth; + + if (typeof data.parentFingerPrint === 'number') { + b = []; + utils.writeU32BE(b, data.parentFingerPrint, 0); + data.parentFingerPrint = b; + } + + data.childIndex = +data.childIndex; + + if (typeof data.chainCode === 'string') + data.chainCode = utils.toArray(data.chainCode, 'hex'); + + data.privateKey = data.privateKey || data.priv; + if (data.privateKey) { + if (data.privateKey.getPrivate) + data.privateKey = data.privateKey.getPrivate().toArray(); + else if (typeof data.privateKey === 'string') + data.privateKey = utils.toKeyArray(data.privateKey); + } + + data.publicKey = data.publicKey || data.pub; + if (data.publicKey) { + if (data.publicKey.getPublic) + data.publicKey = data.privateKey.getPublic(true, 'array'); + else if (typeof data.publicKey === 'string') + data.publicKey = utils.toKeyArray(data.publicKey); + } + + if (typeof data.checksum === 'number') { + b = []; + utils.writeU32BE(b, data.checksum, 0); + data.checksum = b; + } + + return data; +}; + +HDPriv.prototype._seed = function(seed) { + if (seed instanceof HDSeed) + seed = seed.seed; + + if (utils.isHex(seed)) + seed = utils.toArray(seed, 'hex'); + + if (seed.length < MIN_ENTROPY || seed.length > MAX_ENTROPY) + throw new Error('entropy not in range'); + + var hash = sha512hmac(seed, 'Bitcoin seed'); + + return { + // version: network.prefixes.xprivkey, + depth: 0, + parentFingerPrint: 0, + childIndex: 0, + chainCode: hash.slice(32, 64), + privateKey: hash.slice(0, 32), + checksum: null + }; +}; + +HDPriv.prototype._unbuild = function(xkey) { + var raw = utils.fromBase58(xkey); + var data = {}; + var off = 0; + + data.version = utils.readU32BE(raw, off); + off += 4; + data.depth = raw[off]; + off += 1; + data.parentFingerPrint = utils.readU32BE(raw, off); + off += 4; + data.childIndex = utils.readU32BE(raw, off); + off += 4; + data.chainCode = raw.slice(off, off + 32); + off += data.chainCode.length; + off += 1; // nul byte + data.privateKey = raw.slice(off, off + 32); + off += data.privateKey.length; + data.checksum = utils.readU32BE(raw, off); + off += 4; + + var hash = utils.dsha256(raw.slice(0, -4)); + if (data.checksum !== utils.readU32BE(hash, 0)) + throw new Error('checksum mismatch'); + + return data; +}; + +HDPriv.prototype._build = function(data) { + var sequence = []; + var off = 0; + + utils.writeU32BE(sequence, data.version, off); + off += 4; + sequence[off] = data.depth; + off += 1; + utils.copy(data.parentFingerPrint, sequence, off, true); + off += data.parentFingerPrint.length; + utils.writeU32BE(sequence, data.childIndex, off); + off += 4; + utils.copy(data.chainCode, sequence, off, true); + off += data.chainCode.length; + sequence[off] = 0; + off += 1; + utils.copy(data.privateKey, sequence, off, true); + off += data.privateKey.length; + var checksum = utils.dsha256(sequence).slice(0, 4); + utils.copy(checksum, sequence, off, true); + off += 4; + + var xprivkey = utils.toBase58(sequence); + + var pair = bcoin.ecdsa.keyPair({ priv: data.privateKey }); + var privateKey = pair.getPrivate().toArray(); + var publicKey = pair.getPublic(true, 'array'); + + var size = PARENT_FINGER_PRINT_SIZE; + var fingerPrint = utils.ripesha(publicKey).slice(0, size); + + this.version = data.version; + this.depth = data.depth; + this.parentFingerPrint = data.parentFingerPrint; + this.childIndex = data.childIndex; + this.chainCode = data.chainCode; + this.privateKey = privateKey; + // this.checksum = checksum; + + this.xprivkey = xprivkey; + this.fingerPrint = fingerPrint; + this.publicKey = publicKey; + + this.hdpub = new HDPub(this); + this.xpubkey = this.hdpub.xpubkey; + this.pair = bcoin.ecdsa.keyPair({ priv: this.privateKey }); +}; + +HDPriv.prototype.derive = function(index, hard) { + if (typeof index === 'string') + return this.deriveString(index); + + hard = index >= HARDENED ? true : hard; + if (index < HARDENED && hard === true) + index += HARDENED; + + var index_ = []; + utils.writeU32BE(index_, index, 0); + + var data = hard + ? [0].concat(this.privateKey).concat(index_) + : data = [].concat(this.publicKey).concat(index_); + + var hash = sha512hmac(data, this.chainCode); + var leftPart = new bn(hash.slice(0, 32)); + var chainCode = hash.slice(32, 64); + + var privateKey = leftPart.add(new bn(this.privateKey)).mod(ec.curve.n).toArray(); + + return new HDPriv({ + // version: this.version, + depth: this.depth + 1, + parentFingerPrint: this.fingerPrint, + childIndex: index, + chainCode: chainCode, + privateKey: privateKey + }); +}; + +// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki +HDPriv._getIndexes = function(path) { + var steps = path.split('/'); + var root = steps.shift(); + var indexes = []; + + if (~PATH_ROOTS.indexOf(path)) + return indexes; + + if (!~PATH_ROOTS.indexOf(root)) + return null; + + for (var i = 0; i < steps.length; i++) { + var step = steps[i]; + var hard = step[step.length - 1] === '\''; + + if (hard) + step = step.slice(0, -1); + + if (!step || step[0] === '-') + return null; + + var index = +step; + if (hard) + index += HARDENED; + + indexes.push(index); + } + + return indexes; +}; + +HDPriv.isValidPath = function(path, hard) { + if (typeof path === 'string') { + var indexes = HDPriv._getIndexes(path); + return indexes !== null && indexes.every(HDPriv.isValidPath); + } + + if (typeof path === 'number') { + if (path < HARDENED && hard === true) { + path += HARDENED; + } + return path >= 0 && path < MAX_INDEX; + } + + return false; +}; + +HDPriv.prototype.deriveString = function(path) { + if (!HDPriv.isValidPath(path)) + throw new Error('invalid path'); + + var indexes = HDPriv._getIndexes(path); + + return indexes.reduce(function(prev, index) { + return prev.derive(index); + }); +}; + +/** + * HD Public Key + */ + +function HDPub(options) { + if (!(this instanceof HDPub)) + return new HDPriv(options); + + var data; + + if (typeof options === 'string' && options.indexOf('xpub') === 0) + options = { xkey: options }; + + if (options.xkey) + data = this._unbuild(options.xkey); + else + data = options; + + data = this._normalize(data, network.prefixes.xpubkey); + + this.data = data; + + this._build(data); +} + +HDPub.prototype._normalize = HDPriv.prototype._normalize; + +HDPub.prototype._unbuild = function(xkey) { + var raw = utils.fromBase58(xkey); + var data = {}; + var off = 0; + + data.version = utils.readU32BE(raw, off); + off += 4; + data.depth = raw[off]; + off += 1; + data.parentFingerPrint = utils.readU32BE(raw, off); + off += 4; + data.childIndex = utils.readU32BE(raw, off); + off += 4; + data.chainCode = raw.slice(off, off + 32); + off += data.chainCode.length; + data.publicKey = raw.slice(off, off + 33); + off += data.publicKey.length; + data.checksum = utils.readU32BE(raw, off); + off += 4; + + var hash = utils.dsha256(raw.slice(0, -4)); + if (data.checksum !== utils.readU32BE(hash, 0)) + throw new Error('checksum mismatch'); + + return data; +}; + +HDPub.prototype._build = function(data) { + var sequence = []; + var off = 0; + + utils.writeU32BE(sequence, data.version, off); + off += 4; + sequence[off] = data.depth; + off += 1; + utils.copy(data.parentFingerPrint, sequence, off, true); + off += data.parentFingerPrint.length; + utils.writeU32BE(sequence, data.childIndex, off); + off += 4; + utils.copy(data.chainCode, sequence, off, true); + off += data.chainCode.length; + utils.copy(data.publicKey, sequence, off, true); + off += data.publicKey.length; + var checksum = utils.dsha256(sequence).slice(0, 4); + utils.copy(checksum, sequence, off, true); + off += 4; + + if (!data.checksum || !data.checksum.length) + data.checksum = checksum; + else if (utils.toHex(checksum) !== utils.toHex(data.checksum)) + throw new Error('checksum mismatch'); + + var xpubkey = utils.toBase58(sequence); + + var publicKey = data.publicKey; + var size = PARENT_FINGER_PRINT_SIZE; + var fingerPrint = utils.ripesha(publicKey).slice(0, size); + + this.version = data.version; + this.depth = data.depth; + this.parentFingerPrint = data.parentFingerPrint; + this.childIndex = data.childIndex; + this.chainCode = data.chainCode; + this.publicKey = publicKey; + // this.checksum = checksum; + + this.xpubkey = xpubkey; + this.fingerPrint = fingerPrint; + + this.xprivkey = data.xprivkey; + this.pair = bcoin.ecdsa.keyPair({ pub: this.publicKey }); +}; + +HDPub.prototype.derive = function(index, hard) { + if (typeof index === 'string') + return this.deriveString(index); + + if (index >= HARDENED || hard) + throw new Error('invalid index'); + + if (index < 0) + throw new Error('invalid path'); + + var index_ = []; + utils.writeU32BE(index_, index, 0); + + var data = [].concat(this.publicKey).concat(index_); + var hash = sha512hmac(data, this.chainCode); + var leftPart = new bn(hash.slice(0, 32)); + var chainCode = hash.slice(32, 64); + + var pair = bcoin.ecdsa.keyPair({ pub: this.publicKey }); + var pubkeyPoint = ec.curve.g.mul(leftPart).add(pair.pub); + var publicKey = bcoin.ecdsa.keyFromPublic(pubkeyPoint).getPublic(true, 'array'); + + return new HDPub({ + // version: network.prefixes.xpubkey, + depth: this.depth + 1, + parentFingerPrint: this.fingerPrint, + childIndex: index, + chainCode: chainCode, + publicKey: publicKey + }); +}; + +HDPub.isValidPath = function(arg) { + if (typeof arg === 'string') { + var indexes = HDPriv._getIndexes(arg); + return indexes !== null && indexes.every(HDPub.isValidPath); + } + + if (typeof arg === 'number') + return arg >= 0 && arg < HARDENED; + + return false; +}; + +HDPub.prototype.deriveString = function(path) { + if (~path.indexOf('\'')) + throw new Error('cannot derive hardened'); + else if (!HDPub.isValidPath(path)) + throw new Error('invalid path'); + + var indexes = HDPriv._getIndexes(path); + + return indexes.reduce(function(prev, index) { + return prev.derive(index); + }); +}; + +/** + * Make HD keys behave like elliptic KeyPairs + */ + +[HDPriv, HDPub].forEach(function(HD) { + HD.prototype.validate = function validate() { + return this.pair.validate.apply(this.pair, arguments); + }; + + HD.prototype.getPublic = function getPublic() { + return this.pair.getPublic.apply(this.pair, arguments); + }; + + HD.prototype.getPrivate = function getPrivate() { + return this.pair.getPublic.apply(this.pair, arguments); + }; + + HD.prototype.sign = function sign(msg) { + return this.pair.sign.apply(this.pair, arguments); + }; + + HD.prototype.verify = function verify(msg, signature) { + return this.pair.verify.apply(this.pair, arguments); + }; + + HD.prototype.inspect = function inspect() { + return this.pair.inspect.apply(this.pair, arguments); + }; + + HD.prototype.__defineGetter__('pub', function() { + return this.pair.pub; + }); + + HD.prototype.__defineGetter__('priv', function() { + return this.pair.priv; + }); +}); + +/** + * Helpers + */ + +var isBrowser = (typeof process !== 'undefined' && process.browser) + || typeof window !== 'undefined'; + +function sha512hmac(data, salt) { + if (isBrowser) { + var hmac = hash.hmac(hash.sha512, utils.toArray(salt)); + return hmac.update(utils.toArray(data)).digest(); + } + var crypto = require('crypto'); + var hmac = crypto.createHmac('sha512', new Buffer(salt)); + var h = hmac.update(new Buffer(data)).digest(); + return Array.prototype.slice.call(h); +} + +function randomBytes(size) { + if (isBrowser) { + var a = Uint8Array(size); + (window.crypto || window.msCrypto).getRandomValues(a); + var buf = new Array(size); + utils.copy(a, buf, 0); + return buf; + } + var crypto = require('crypto'); + return Array.prototype.slice.call(crypto.randomBytes(size)); +} + +/** + * PDKBF2 + * Credit to: https://github.com/stayradiated/pbkdf2-sha512 + * Copyright (c) 2014, JP Richardson Copyright (c) 2010-2011 Intalio Pte, All Rights Reserved + */ + +function pbkdf2(key, salt, iterations, dkLen) { + 'use strict'; + + var hLen = 64; + if (dkLen > (Math.pow(2, 32) - 1) * hLen) + throw Error('Requested key length too long'); + + if (typeof key !== 'string' && typeof key.length !== 'number') + throw new TypeError('key must a string or array'); + + if (typeof salt !== 'string' && typeof salt.length !== 'number') + throw new TypeError('salt must a string or array'); + + if (typeof key === 'string') + key = utils.toArray(key, null); + + if (typeof salt === 'string') + salt = utils.toArray(salt, null); + + var DK = new Array(dkLen); + var U = new Array(hLen); + var T = new Array(hLen); + var block1 = new Array(salt.length + 4); + + var l = Math.ceil(dkLen / hLen); + var r = dkLen - (l - 1) * hLen; + + utils.copy(salt.slice(0, salt.length), block1, 0); + + for (var i = 1; i <= l; i++) { + block1[salt.length + 0] = (i >> 24 & 0xff); + block1[salt.length + 1] = (i >> 16 & 0xff); + block1[salt.length + 2] = (i >> 8 & 0xff); + block1[salt.length + 3] = (i >> 0 & 0xff); + + U = sha512hmac(block1, key); + + utils.copy(U.slice(0, hLen), T, 0); + + for (var j = 1; j < iterations; j++) { + U = sha512hmac(U, key); + + for (var k = 0; k < hLen; k++) + T[k] ^= U[k]; + } + + var destPos = (i - 1) * hLen; + var len = (i === l ? r : hLen); + utils.copy(T.slice(0, len), DK, 0); + } + + return DK; +} + +/** + * Expose + */ + +hd.seed = HDSeed; +hd.priv = HDPriv; +hd.pub = HDPub; +hd.pbkdf2 = pbkdf2; diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index b5b80f5b..b06b8c22 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -89,6 +89,17 @@ Peer.prototype._init = function init() { self._error(err); }); + if (this.pool.options.fullNode) { + this.once('version', function() { + var ip = self.socket && self.socket.remoteAddress || '0.0.0.0'; + self.pool.emit('debug', + 'Sent version (%s): height=%s', + ip, this.pool.chain.getStartHeight()); + self.pool.emit('debug', 'version (%s): sending locator hashes', ip); + self.loadHeaders(self.chain.locatorHashes(), 0); + }); + } + this._ping.timer = setInterval(function() { self._write(self.framer.ping([ 0xde, 0xad, 0xbe, 0xef, @@ -98,9 +109,7 @@ Peer.prototype._init = function init() { // Send hello this._write(this.framer.version({ - height: this.options.startHeight != null - ? this.options.startHeight - : 0, + height: this.pool.chain.getStartHeight(), relay: this.options.relay })); @@ -167,18 +176,8 @@ Peer.prototype.broadcast = function broadcast(items) { }; Peer.prototype.updateWatch = function updateWatch() { - if (!this.pool.options.relay) { - if (this.ack) { - var self = this; - if (this.pool.block.lastHash) - this.loadBlocks([ self.pool.block.lastHash ], 0); - else - this.pool.chain.getLast(function(hash) { - self.loadBlocks([ hash ], 0); - }); - } + if (this.pool.options.fullNode) return; - } if (this.ack) this._write(this.framer.filterLoad(this.bloom, 'none')); @@ -292,6 +291,8 @@ Peer.prototype._onPacket = function onPacket(packet) { return this._handleVersion(payload); else if (cmd === 'inv') return this._handleInv(payload); + else if (cmd === 'headers') + return this._handleHeaders(payload); else if (cmd === 'getdata') return this._handleGetData(payload); else if (cmd === 'addr') @@ -437,15 +438,6 @@ Peer.prototype._handleInv = function handleInv(items) { }); this.emit('blocks', blocks); - if (!this.pool.options.relay) { - if (txs.length) - this.emit('txs', txs.map(function(tx) { - return tx.hash; - })); - this.getData(items); - return; - } - if (txs.length === 0) return; @@ -455,6 +447,38 @@ Peer.prototype._handleInv = function handleInv(items) { this.getData(txs); }; +Peer.prototype._handleHeaders = function handleHeaders(headers) { + var self = this; + + headers = headers.map(function(header) { + header.prevBlock = utils.toHex(header.prevBlock); + header.merkleRoot = utils.toHex(header.merkleRoot); + header.hash = utils.toHex(utils.dsha256(header._raw)); + return header; + }); + + if (this.pool.options.fullNode) { + for (var i = 0; i < headers.length; i++) { + var header = headers[i]; + var hash = header.hash; + // if (this.chain.orphan.bmap[hash]) { + if (this.chain.orphan.map[header.prevBlock]) { + this.loadHeaders(this.chain.locatorHashes(), this.chain.getOrphanRoot(hash)); + continue; + } + if (!this.chain.index.bloom.test(hash, 'hex') || i === headers.length - 1) + this.getData([{ type: 'block', hash: hash }]); + } + } + + this.emit('headers', headers); +}; + + +Peer.prototype.loadHeaders = function loadHeaders(hashes, stop) { + this._write(this.framer.getHeaders(hashes, stop)); +}; + Peer.prototype.loadBlocks = function loadBlocks(hashes, stop) { this._write(this.framer.getBlocks(hashes, stop)); }; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 52525c78..417762b0 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -7,13 +7,18 @@ var utils = bcoin.utils; var assert = utils.assert; function Pool(options) { + var self = this; + if (!(this instanceof Pool)) return new Pool(options); EventEmitter.call(this); this.options = options || {}; - this.options.relay = this.options.relay !== false; + this.options.fullNode = !!this.options.fullNode; + this.options.relay == null + ? (this.options.fullNode ? false : true) + : this.options.relay; this.storage = this.options.storage; this.destroyed = false; this.size = options.size || 32; @@ -40,7 +45,9 @@ function Pool(options) { storage: this.storage, // Since regular blocks contain transactions and full merkle // trees, it's risky to cache 2000 blocks. Let's do 100. - cacheLimit: !this.options.relay ? 100 : null + cacheLimit: this.options.fullNode ? 100 : null, + fullNode: this.options.fullNode, + startHeight: this.options.startHeight }); this.watchMap = {}; this.bloom = new bcoin.bloom(8 * 1024, @@ -85,19 +92,32 @@ function Pool(options) { this.createConnection = options.createConnection; assert(this.createConnection); - this._init(); + this.chain.on('debug', function() { + var args = Array.prototype.slice.call(arguments); + self.emit.apply(self, ['debug'].concat(args)); + }); + + if (!this.chain.loading) { + this._init(); + } else { + this.chain.once('load', function() { + self._init(); + }); + } } inherits(Pool, EventEmitter); module.exports = Pool; Pool.prototype._init = function _init() { + var self = this; + this._addLoader(); for (var i = 0; i < this.size; i++) this._addPeer(0); this._load(); - var self = this; this.chain.on('missing', function(hash, preload, parent) { + if (self.options.fullNode) return; self._request('block', hash, { force: true }); self._scheduleRequests(); self._loadRange(preload); @@ -142,6 +162,8 @@ Pool.prototype._addLoader = function _addLoader() { clearTimeout(timer); }); + if (this.options.fullNode) return; + function destroy() { // Chain is full and up-to-date if (self.chain.isFull()) { @@ -198,6 +220,8 @@ Pool.prototype.isFull = function isFull() { }; Pool.prototype._loadRange = function _loadRange(hashes, force) { + if (this.options.fullNode) return; + if (!hashes) return; @@ -220,6 +244,8 @@ Pool.prototype._loadRange = function _loadRange(hashes, force) { }; Pool.prototype._load = function _load() { + if (this.options.fullNode) return; + if (this.request.queue.length >= this.load.hwm) { this.load.hiReached = true; return false; @@ -295,19 +321,38 @@ Pool.prototype._addPeer = function _addPeer(backoff) { self._scheduleRequests(); }); - peer.on('merkleblock', function(block) { - // Reset backoff, peer seems to be responsive - backoff = 0; + if (!this.options.fullNode) { + peer.on('merkleblock', function(block) { + // Reset backoff, peer seems to be responsive + backoff = 0; - self._response(block); - self.chain.add(block); - self.emit('chain-progress', self.chain.fillPercent(), peer); - self.emit('block', block, peer); - }); - - if (!this.options.relay) { + self._response(block); + self.chain.add(block); + self.emit('chain-progress', self.chain.fillPercent(), peer); + self.emit('block', block, peer); + }); + } else { peer.on('block', function(block) { - peer.emit('merkleblock', block); + backoff = 0; + + var height = self.chain.index.hashes.length; + var hash = block.hash('hex'); + + if (!self.chain.index.bloom.test(block.prevBlock, 'hex')) { + peer.loadHeaders(self.chain.locatorHashes(), self.chain.getOrphanRoot(block)); + } + + self._response(block); + self.chain.add(block); + + // if (!self.chain.index.bloom.test(block.prevBlock, 'hex')) { + // peer.loadHeaders(self.chain.locatorHashes(), self.chain.getOrphanRoot(block)); + // } else if (self.chain.index.hashes.length === height) { + // return; + // } + + self.emit('chain-progress', self.chain.fillPercent(), peer); + self.emit('block', block, peer); }); } @@ -365,8 +410,11 @@ Pool.prototype._removePeer = function _removePeer(peer) { Pool.prototype.watch = function watch(id) { if (id instanceof bcoin.wallet) { - this.watch(id.getAddress()); - this.watch(id.getPublicKey()); + // this.watch(id.getAddress()); + this.watch(id.getFullHash()); + this.watch(id.getFullPublicKey()); + this.watch(id.getOwnHash()); + this.watch(id.getOwnPublicKey()); return; } @@ -415,8 +463,10 @@ Pool.prototype.unwatch = function unwatch(id) { Pool.prototype.addWallet = function addWallet(w, defaultTs) { if (this.wallets.indexOf(w) !== -1) return false; - this.watch(w.getHash()); - this.watch(w.getPublicKey()); + this.watch(w.getFullHash()); + this.watch(w.getFullPublicKey()); + this.watch(w.getOwnHash()); + this.watch(w.getOwnPublicKey()); var self = this; var e = new EventEmitter(); @@ -447,8 +497,10 @@ Pool.prototype.removeWallet = function removeWallet(w) { if (i == -1) return; this.wallets.splice(i, 1); - this.unwatch(w.getHash()); - this.unwatch(w.getPublicKey()); + this.unwatch(w.getFullHash()); + this.unwatch(w.getFullPublicKey()); + this.unwatch(w.getOwnHash()); + this.unwatch(w.getOwnPublicKey()); } Pool.prototype.search = function search(id, range, e) { diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 0842b379..7f9722c1 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -3,21 +3,6 @@ var utils = bcoin.utils; exports.minVersion = 70001; exports.version = 70002; -exports.magic = 0xd9b4bef9; -exports.genesis = { - version: 1, - prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 ], - merkleRoot: utils.toArray( - '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', - 'hex' - ).reverse(), - ts: 1231006505, - bits: 0x1d00ffff, - nonce: 2083236893 -}; // version - services field exports.services = { @@ -49,9 +34,9 @@ exports.opcodes = { pushdata1: 0x4c, pushdata2: 0x4d, pushdata4: 0x4e, - negate1: 0x4f, + // negate1: 0x4f, - nop: 0x61, + nop1: 0x61, if_: 0x63, notif: 0x64, else_: 0x67, @@ -129,12 +114,17 @@ exports.opcodes = { checksig: 0xac, checksigverify: 0xad, checkmultisig: 0xae, - checkmultisigverify: 0xaf + checkmultisigverify: 0xaf, + checklocktimeverify: 0xb1 }; +exports.opcodes['-1'] = 0x50 + -1; for (var i = 1; i <= 16; i++) exports.opcodes[i] = 0x50 + i; +for (var i = 0; i <= 7; i++) + exports.opcodes['nop' + (i + 3)] = 0xb2 + i; + exports.opcodesByVal = new Array(256); Object.keys(exports.opcodes).forEach(function(name) { exports.opcodesByVal[exports.opcodes[name]] = name; @@ -148,8 +138,15 @@ exports.hashType = { anyonecaypay: 0x80 }; +exports.rhashType = Object.keys(exports.hashType).reduce(function(out, type) { + out[exports.hashType[type]] = type; + return out; +}, {}); + exports.block = { maxSize: 1000000, maxSigops: 1000000 / 50, maxOrphanTx: 1000000 / 100 }; + +exports.locktimeThreshold = 500000000; // Tue Nov 5 00:53:20 1985 UTC diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index 64e5b9da..12c4d0b7 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -1,4 +1,5 @@ var bcoin = require('../../bcoin'); +var network = require('./network'); var constants = require('./constants'); var utils = bcoin.utils; var assert = utils.assert; @@ -27,7 +28,7 @@ Framer.prototype.header = function header(cmd, payload) { var h = new Array(24); // Magic value - writeU32(h, constants.magic, 0); + writeU32(h, network.magic, 0); // Command var len = writeAscii(h, cmd, 4); @@ -201,7 +202,15 @@ Framer.prototype.filterClear = function filterClear() { return this.packet('filterclear', []); }; +Framer.prototype.getHeaders = function getHeaders(hashes, stop) { + return this._getBlocks('getheaders', hashes, stop); +}; + Framer.prototype.getBlocks = function getBlocks(hashes, stop) { + return this._getBlocks('getblocks', hashes, stop); +}; + +Framer.prototype._getBlocks = function _getBlocks(cmd, hashes, stop) { var p = []; writeU32(p, constants.version, 0); var off = 4 + varint(p, hashes.length, 4); @@ -227,7 +236,7 @@ Framer.prototype.getBlocks = function getBlocks(hashes, stop) { p[off + len] = 0; assert.equal(off + len, p.length); - return this.packet('getblocks', p); + return this.packet(cmd, p); }; Framer.tx = function tx(tx) { diff --git a/lib/bcoin/protocol/index.js b/lib/bcoin/protocol/index.js index 7002c0fb..954909bd 100644 --- a/lib/bcoin/protocol/index.js +++ b/lib/bcoin/protocol/index.js @@ -3,4 +3,4 @@ var protocol = exports; protocol.constants = require('./constants'); protocol.framer = require('./framer'); protocol.parser = require('./parser'); -protocol.preload = require('./preload'); +protocol.network = require('./network'); diff --git a/lib/bcoin/protocol/network.js b/lib/bcoin/protocol/network.js new file mode 100644 index 00000000..1ac88836 --- /dev/null +++ b/lib/bcoin/protocol/network.js @@ -0,0 +1,178 @@ +var bcoin = require('../../bcoin'); +var utils = bcoin.utils; + +/** + * Network + */ + +var network = exports; + +network.set = function(type) { + var net = network[type]; + utils.merge(network, net); +}; + +/** + * Main + */ + +var main = network.main = {}; + +main.prefixes = { + pubkey: 0, + script: 5, + privkey: 128, + xpubkey: 0x0488b21e, + xprivkey: 0x0488ade4 +}; + +utils.merge(main.prefixes, { + normal: main.prefixes.pubkey, + p2pkh: main.prefixes.pubkey, + multisig: main.prefixes.pubkey, + p2sh: main.prefixes.script +}); + +main.type = 'main'; + +main.seeds = [ + 'seed.bitcoin.sipa.be', // Pieter Wuille + 'dnsseed.bluematt.me', // Matt Corallo + 'dnsseed.bitcoin.dashjr.org', // Luke Dashjr + 'seed.bitcoinstats.com', // Christian Decker + 'bitseed.xf2.org', // Jeff Garzik + 'seed.bitcoin.jonasschnelli.ch' // Jonas Schnelli +]; + +main.port = 8333; + +main.alertKey = utils.toArray('' + + '04fc9702847840aaf195de8442ebecedf5b095c' + + 'dbb9bc716bda9110971b28a49e0ead8564ff0db' + + '22209e0374782c093bb899692d524e9d6a6956e' + + '7c5ecbcd68284', + 'hex'); + +main.checkpoints = [ + { height: 11111, hash: '0000000069e244f73d78e8fd29ba2fd2ed618bd6fa2ee92559f542fdb26e7c1d' }, + { height: 33333, hash: '000000002dd5588a74784eaa7ab0507a18ad16a236e7b1ce69f00d7ddfb5d0a6' }, + { height: 74000, hash: '0000000000573993a3c9e41ce34471c079dcf5f52a0e824a81e7f953b8661a20' }, + { height: 105000, hash: '00000000000291ce28027faea320c8d2b054b2e0fe44a773f3eefb151d6bdc97' }, + { height: 134444, hash: '00000000000005b12ffd4cd315cd34ffd4a594f430ac814c91184a0d42d2b0fe' }, + { height: 168000, hash: '000000000000099e61ea72015e79632f216fe6cb33d7899acb35b75c8303b763' }, + { height: 193000, hash: '000000000000059f452a5f7340de6682a977387c17010ff6e6c3bd83ca8b1317' }, + { height: 210000, hash: '000000000000048b95347e83192f69cf0366076336c639f9b7228e9ba171342e' }, + { height: 216116, hash: '00000000000001b4f4b433e81ee46494af945cf96014816a4e2370f11b23df4e' }, + { height: 225430, hash: '00000000000001c108384350f74090433e7fcf79a606b8e797f065b130575932' }, + { height: 250000, hash: '000000000000003887df1f29024b06fc2200b55f8af8f35453d7be294df2d214' }, + { height: 279000, hash: '0000000000000001ae8c72a0b0c301f67e3afca10e819efa9041e458e9bd7e40' }, + { height: 295000, hash: '00000000000000004d9b4ef50f0f9d686fd69db2e03af35a100370c64632a983' } +]; + +main.checkpoints.tsLastCheckpoint = 1397080064; +main.checkpoints.txsLastCheckpoint = 36544669; +main.checkpoints.txsPerDay = 60000.0; + +// http://blockexplorer.com/b/0 +// http://blockexplorer.com/rawblock/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f +main.genesis = { + version: 1, + _hash: utils.toArray( + '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', + 'hex' + ).reverse(), + prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 ], + merkleRoot: utils.toArray( + '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', + 'hex' + ).reverse(), + ts: 1231006505, + bits: 0x1d00ffff, + nonce: 2083236893 +}; + +main.magic = 0xd9b4bef9; + +main.preload = require('./preload'); + +/** + * Testnet (v3) + * https://en.bitcoin.it/wiki/Testnet + */ + +var testnet = network.testnet = {}; + +testnet.type = 'testnet'; + +testnet.prefixes = { + pubkey: 111, + script: 196, + privkey: 239, + xpubkey: 0x043587cf, + xprivkey: 0x04358394 +}; + +utils.merge(testnet.prefixes, { + normal: testnet.prefixes.pubkey, + p2pkh: testnet.prefixes.pubkey, + multisig: testnet.prefixes.pubkey, + p2sh: testnet.prefixes.script +}); + +testnet.seeds = [ + 'testnet-seed.alexykot.me', + 'testnet-seed.bitcoin.petertodd.org', + 'testnet-seed.bluematt.me', + 'testnet-seed.bitcoin.schildbach.de' +]; + +testnet.port = 18333; + +testnet.alertKey = utils.toArray('' + + '04302390343f91cc401d56d68b123028bf52e5f' + + 'ca1939df127f63c6467cdf9c8e2c14b61104cf8' + + '17d0b780da337893ecc4aaff1309e536162dabb' + + 'db45200ca2b0a', + 'hex'); + +testnet.checkpoints = [ + { height: 546, hash: '000000002a936ca763904c3c35fce2f3556c559c0214345d31b1bcebf76acb70' } +]; + +testnet.checkpoints.tsLastCheckpoint = 1338180505; +testnet.checkpoints.txsLastCheckpoint = 16341; +testnet.checkpoints.txsPerDay = 300; + +// http://blockexplorer.com/testnet/b/0 +// http://blockexplorer.com/testnet/rawblock/000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943 +testnet.genesis = { + version: 1, + _hash: utils.toArray( + '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', + 'hex' + ).reverse(), + prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 ], + merkleRoot: utils.toArray( + '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', + 'hex' + ).reverse(), + ts: 1296688602, + bits: 0x1d00ffff, + nonce: 414098458 +}; + +testnet.magic = 0x0709110b; + +testnet.preload = { + 'v': 1, + 'type': 'chain', + 'hashes': [utils.toHex(testnet.genesis._hash)], + 'ts': [testnet.genesis.ts], + 'heights': [0] +}; diff --git a/lib/bcoin/protocol/parser.js b/lib/bcoin/protocol/parser.js index 7fc165ba..c13bcbd9 100644 --- a/lib/bcoin/protocol/parser.js +++ b/lib/bcoin/protocol/parser.js @@ -6,6 +6,7 @@ var bcoin = require('../../bcoin'); var utils = bcoin.utils; var assert = utils.assert; var constants = require('./constants'); +var network = require('./network'); var readU32 = utils.readU32; var readU64 = utils.readU64; @@ -71,7 +72,7 @@ Parser.prototype.parse = function parse(chunk) { Parser.prototype.parseHeader = function parseHeader(h) { var magic = readU32(h, 0); - if (magic !== constants.magic) { + if (magic !== network.magic) { return this._error('Invalid magic value: ' + magic.toString(16)); } @@ -97,6 +98,8 @@ Parser.prototype.parsePayload = function parsePayload(cmd, p) { return this.parseInvList(p); else if (cmd === 'merkleblock') return this.parseMerkleBlock(p); + else if (cmd === 'headers') + return this.parseHeaders(p); else if (cmd === 'block') return this.parseBlock(p); else if (cmd === 'tx') @@ -135,10 +138,6 @@ Parser.prototype.parseVersion = function parseVersion(p) { // Relay var relay = p.length > off ? p[off] === 1 : true; - // NOTE: Could just do this to make relay - // `false` when it's not included: - // var relay = p[off] === 1; - return { v: v, services: services, @@ -226,6 +225,43 @@ Parser.prototype.parseMerkleBlock = function parseMerkleBlock(p) { }; }; +Parser.prototype.parseHeaders = function parseHeaders(p) { + if (p.length < 81) + return this._error('Invalid headers size'); + + var result = readIntv(p, 0); + var off = result.off; + var count = result.r; + + var headers = []; + + if (p.length >= off + 81) { + for (var i = 0; i < count && off + 81 < p.length; i++) { + var header = {}; + var start = off; + header.version = readU32(p, off); + off += 4; + header.prevBlock = p.slice(off, off + 32); + off += 32; + header.merkleRoot = p.slice(off, off + 32); + off += 32; + header.ts = readU32(p, off); + off += 4; + header.bits = readU32(p, off); + off += 4; + header.nonce = readU32(p, off); + off += 4; + var r = readIntv(p, off); + header.totalTX = r.r; + off = r.off; + header._raw = p.slice(start, start + 80); + headers.push(header); + } + } + + return headers; +}; + Parser.prototype.parseBlock = function parseBlock(p) { if (p.length < 81) return this._error('Invalid block size'); diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index dd5225dc..6f9ed62d 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -1,6 +1,8 @@ var bcoin = require('../bcoin'); +var bn = require('bn.js'); var constants = bcoin.protocol.constants; var utils = bcoin.utils; +var assert = bcoin.utils.assert; var script = exports; script.decode = function decode(s) { @@ -23,8 +25,8 @@ script.decode = function decode(s) { continue; } - // Raw number - if (b >= 0x51 && b <= 0x60) { + // Raw number (-1 and 1-16) + if (b === 0x4f || (b >= 0x51 && b <= 0x60)) { opcodes.push(b - 0x50); continue; } @@ -62,6 +64,7 @@ script.encode = function encode(s) { // Push value to stack if (Array.isArray(instr)) { if (instr.length === 0) { + // OP_FALSE res.push(0); } else if (1 <= instr.length && instr.length <= 0x4b) { res = res.concat(instr.length, instr); @@ -109,113 +112,635 @@ script.subscript = function subscript(s) { return res; }; -script.execute = function execute(s, stack, tx) { +script.verify = function verify(hash, sig, pub) { + var k = bcoin.ecdsa.keyFromPublic(pub); + + // Points at Infinity make verify() throw. + // This specifically throws on wallet-test.js + // where [1] is concatted to the pubkey. + if (k.getPublic().isInfinity()) + return false; + + // Use a try catch in case there are + // any uncaught errors for bad inputs in verify(). + try { + return bcoin.ecdsa.verify(hash, sig, pub); + } catch (e) { + return false; + } +}; + +script._next = function(to, s, pc) { + var depth = 0; + while (s[pc]) { + var o = s[pc]; + if (o === 'if_' || o === 'notif') + depth++; + else if (o === 'else_') + depth--; + else if (o === 'endif') + depth--; + if (depth < 0) + break; + if (depth === 0 && o === to) + return pc; + if (o === 'else_') + depth++; + pc++; + } + return -1; +}; + +script.execute = function execute(s, stack, tx, index) { + s = s.slice(); + + if (s.length > 10000) + return false; + + var lastSep = -1; + + stack.alt = stack.alt || []; + for (var pc = 0; pc < s.length; pc++) { var o = s[pc]; + if (Array.isArray(o)) { stack.push(o); - } else if (o === 'dup') { - if (stack.length === 0) - return false; + continue; + } - stack.push(stack[stack.length - 1]); - } else if (o === 'hash160') { - if (stack.length === 0) - return false; + if (o === -1 || (o >= 1 && o <= 16)) { + stack.push([o]); + continue; + } - stack.push(utils.ripesha(stack.pop())); - } else if (o === 'eqverify' || o === 'eq') { - if (stack.length < 2) - return false; - - var res = utils.isEqual(stack.pop(), stack.pop()); - if (o === 'eqverify') { - if (!res) - return false; - } else { - stack.push(res ? [ 1 ] : []); + switch (o) { + case 'nop1': + case 'nop3': + case 'nop4': + case 'nop5': + case 'nop6': + case 'nop7': + case 'nop8': + case 'nop9': + case 'nop10': { + break; } - - } else if (o === 'checksigverify' || o === 'checksig') { - if (!tx || stack.length < 2) - return false; - - var pub = stack.pop(); - var sig = stack.pop(); - var type = sig[sig.length - 1]; - if (type !== 1) - return false; - - var res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), pub); - if (o === 'checksigverify') { - if (!res) + case 'if_': + case 'notif': { + var val = false; + if (stack.length < 1) return false; - } else { - stack.push(res ? [ 1 ] : []); + var v = stack.pop(); + val = new bn(v).cmpn(0) !== 0; + if (o === 'notif') + val = !val; + var if_ = pc; + var else_ = script._next('else_', s, pc); + var endif = script._next('endif', s, pc); + // Splice out the statement blocks we don't need + if (val) { + if (endif === -1) + return false; + if (else_ === -1) { + s.splice(endif, 1); + s.splice(if_, 1); + } else { + s.splice(else_, (endif - else_) + 1); + s.splice(if_, 1); + } + } else { + if (endif === -1) + return false; + if (else_ === -1) { + s.splice(if_, (endif - if_) + 1); + } else { + s.splice(endif, 1); + s.splice(if_, (else_ - if_) + 1); + } + } + // Subtract one since we removed the if/notif opcode + pc--; + break; } - } else if (o === 'checkmultisigverify' || o === 'checkmultisig') { - if (!tx || stack.length < 3) + case 'else_': { return false; - - var n = stack.pop(); - if (n.length !== 1 || !(1 <= n[0] && n[0] <= 3)) + } + case 'endif': { return false; - n = n[0]; - - if (stack.length < n + 1) + } + case 'verify': { + if (stack.length === 0) + return false; + if (new bn(stack[stack.length - 1]).cmpn(0) === 0) + return false; + break; + } + case 'ret': { return false; + } + case 'toaltstack': { + if (stack.length === 0) + return false; + stack.alt.push(stack.pop()); + break; + } + case 'fromaltstack': { + if (stack.alt.length === 0) + return false; + stack.push(stack.alt.pop()); + break; + } + case 'ifdup': { + if (stack.length === 0) + return false; + if (new bn(stack[stack.length - 1]).cmpn(0) !== 0) + stack.push(new bn(stack[stack.length - 1]).toArray()); + break; + } + case 'depth': { + stack.push(new bn(stack.length).toArray()); + break; + } + case 'drop': { + if (stack.length === 0) + return false; + stack.pop(); + break; + } + case 'dup': { + if (stack.length === 0) + return false; + stack.push(stack[stack.length - 1]); + break; + } + case 'nip': { + if (stack.length < 2) + return false; + stack.splice(stack.length - 2, 1); + break; + } + case 'over': { + if (stack.length < 2) + return false; + stack.push(stack[stack.length - 2]); + break; + } + case 'pick': + case 'roll': { + if (stack.length < 2) + return false; + var n = new bn(stack.pop()).toNumber(); + if (n < 0 || n >= stack.length) + return false; + var v = stack[-n - 1]; + if (o === 'roll') + stack.splice(stack.length - n - 1, 1); + stack.push(v); + break; + } + case 'rot': { + if (stack.length < 3) + return false; + var v3 = stack[stack.length - 3]; + var v2 = stack[stack.length - 2]; + var v1 = stack[stack.length - 1]; + stack[stack.length - 3] = v2; + stack[stack.length - 2] = v3; + v2 = stack[stack.length - 2]; + stack[stack.length - 2] = v1; + stack[stack.length - 1] = v2; + break; + } + case 'swap': { + if (stack.length < 2) + return false; + var v2 = stack[stack.length - 2]; + var v1 = stack[stack.length - 1]; + stack[stack.length - 2] = v1; + stack[stack.length - 1] = v2; + break; + } + case 'tuck': { + if (stack.length < 2) + return false; + stack.splice(stack.length - 2, 0, stack[stack.length - 1]); + break; + } + case 'drop2': { + if (stack.length < 2) + return false; + stack.pop(); + stack.pop(); + break; + } + case 'dup2': { + if (stack.length < 2) + return false; + var v1 = stack[stack.length - 1]; + var v2 = stack[stack.length - 2]; + stack.push(v1); + stack.push(v2); + break; + } + case 'dup3': { + if (stack.length < 3) + return false; + var v1 = stack[stack.length - 1]; + var v2 = stack[stack.length - 2]; + var v3 = stack[stack.length - 3]; + stack.push(v1); + stack.push(v2); + stack.push(v3); + break; + } + case 'over2': { + if (stack.length < 4) + return false; + var v1 = stack[stack.length - 4]; + var v2 = stack[stack.length - 3]; + stack.push(v1); + stack.push(v2); + break; + } + case 'rot2': { + if (stack.length < 6) + return false; + var v1 = stack[stack.length - 6]; + var v2 = stack[stack.length - 5]; + stack.splice(stack.length - 6, 2); + stack.push(v1); + stack.push(v2); + break; + } + case 'swap2': { + if (stack.length < 4) + return false; + var v4 = stack[stack.length - 4]; + var v3 = stack[stack.length - 3]; + var v2 = stack[stack.length - 2]; + var v1 = stack[stack.length - 1]; + stack[stack.length - 4] = v2; + stack[stack.length - 2] = v4; + stack[stack.length - 3] = v1; + stack[stack.length - 1] = v3; + break; + } + case 'size': { + if (stack.length < 1) + return false; + stack.push(new bn(stack[stack.length - 1].length || 0).toArray()); + break; + } + case 'add1': + case 'sub1': + case 'negate': + case 'abs': + case 'not': + case 'noteq0': { + if (stack.length < 1) + return false; + var n = new bn(stack.pop()); + switch (o) { + case 'add1': + n.iadd(1); + break; + case 'sub1': + n.isub(1); + break; + case 'negate': + n = n.neg(); + break; + case 'abs': + if (n.cmpn(0) < 0) + n = n.neg(); + break; + case 'not': + n = n.cmpn(0) === 0; + break; + case 'noteq0': + n = n.cmpn(0) !== 0; + break; + default: + return false; + } + stack.push(n.toArray()); + break; + } + case 'add': + case 'sub': + case 'booland': + case 'boolor': + case 'numeq': + case 'numeqverify': + case 'numneq': + case 'lt': + case 'gt': + case 'lte': + case 'gte': + case 'min': + case 'max': { + switch (o) { + case 'add': + case 'sub': + case 'booland': + case 'boolor': + case 'numeq': + case 'numeqverify': + case 'numneq': + case 'lt': + case 'gt': + case 'lte': + case 'gte': + case 'min': + case 'max': + if (stack.length < 2) + return false; + var n2 = new bn(stack.pop()); + var n1 = new bn(stack.pop()); + var n = new bn(0); + switch (o) { + case 'add': + n = n1.add(b2); + break; + case 'sub': + n = n1.sub(n2); + break; + case 'booland': + n = n1.cmpn(0) !== 0 && n2.cmpn(0) !== 0; + break; + case 'boolor': + n = n1.cmpn(0) !== 0 || n2.cmpn(0) !== 0; + break; + case 'numeq': + n = n1.cmp(n2) === 0; + break; + case 'numeqverify': + n = n1.cmp(n2) === 0; + break; + case 'numneq': + n = n1.cmp(n2) !== 0; + break; + case 'lt': + n = n1.cmp(n2) < 0; + break; + case 'gt': + n = n1.cmp(n2) > 0; + break; + case 'lte': + n = n1.cmp(n2) <= 0; + break; + case 'gte': + n = n1.cmp(n2) >= 0; + break; + case 'min': + n = n1.cmp(n2) < 0 ? n1 : n2; + break; + case 'max': + n = n1.cmp(n2) > 0 ? n1 : n2; + break; + default: + return false; + } + var res = n.cmpn(0) !== 0; + if (o === 'numeqverify') { + if (!res) + return false; + } else { + stack.push(n.toArray()); + // stack.push(res ? [ 1 ] : []); + } + break; + case 'within': + if (stack.length < 3) + return false; + var n3 = new bn(stack.pop()); + var n2 = new bn(stack.pop()); + var n1 = new bn(stack.pop()); + var val = n2.cmp(n1) <= 0 && n1.cmp(n3) < 0; + stack.push(val.cmpn(0) !== 0 ? [ 1 ] : []); + break; + } - var keys = []; - for (var i = 0; i < n; i++) { - var key = stack.pop(); - if (!(33 <= key.length && key.length <= 65)) + break; + } + case 'codesep': { + lastSep = pc; + break; + } + case 'ripemd160': { + if (stack.length === 0) + return false; + stack.push(utils.ripemd160(stack.pop())); + break; + } + case 'sha1': { + if (stack.length === 0) + return false; + stack.push(utils.sha1(stack.pop())); + break; + } + case 'sha256': { + if (stack.length === 0) + return false; + stack.push(utils.sha256(stack.pop())); + break; + } + case 'hash256': { + if (stack.length === 0) + return false; + stack.push(utils.dsha256(stack.pop())); + break; + } + case 'hash160': { + if (stack.length === 0) + return false; + stack.push(utils.ripesha(stack.pop())); + break; + } + case 'eqverify': + case 'eq': { + if (stack.length < 2) + return false; + var res = utils.isEqual(stack.pop(), stack.pop()); + if (o === 'eqverify') { + if (!res) + return false; + } else { + stack.push(res ? [ 1 ] : []); + } + break; + } + case 'checksigverify': + case 'checksig': { + if (!tx || stack.length < 2) return false; - keys.push(key); - } - - var m = stack.pop(); - if (m.length !== 1 || !(1 <= m[0] && m[0] <= n)) - return false; - m = m[0]; - - if (stack.length < m + 1) - return false; - - // Get signatures - var succ = 0; - for (var i = 0, j = 0; i < m && j < n; i++) { + var pub = stack.pop(); var sig = stack.pop(); var type = sig[sig.length - 1]; - if (type !== 1) + if (!constants.rhashType[type & 0x7f]) return false; - var res = false; - for (; !res && j < n; j++) - res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), keys[j]); - if (res) - succ++; - } - - // Extra value - stack.pop(); - - var res = succ >= m; - if (o === 'checkmultisigverify') { - if (!res) + if (!script.isValidSig(sig)) return false; - } else { - stack.push(res ? [ 1 ] : []); + + var subscript = s.slice(lastSep + 1); + var hash = tx.subscriptHash(index, subscript, type); + + var res = script.verify(hash, sig.slice(0, -1), pub); + if (o === 'checksigverify') { + if (!res) + return false; + } else { + stack.push(res ? [ 1 ] : []); + } + + break; + } + case 'checkmultisigverify': + case 'checkmultisig': { + if (!tx || stack.length < 3) + return false; + + var n = stack.pop(); + if (n.length !== 1 || !(1 <= n[0] && n[0] <= 3)) + return false; + n = n[0] || 0; + + if (stack.length < n + 1) + return false; + + var keys = []; + for (var i = 0; i < n; i++) { + var key = stack.pop(); + if (!(33 <= key.length && key.length <= 65)) + return false; + + keys.push(key); + } + + var m = stack.pop(); + if (m.length !== 1 || !(1 <= m[0] && m[0] <= n)) + return false; + m = m[0] || 0; + + if (stack.length < m + 1) + return false; + + // Get signatures + var succ = 0; + for (var i = 0; i < m; i++) { + var sig = stack.pop(); + var type = sig[sig.length - 1]; + if (!constants.rhashType[type & 0x7f]) + return false; + + var subscript = s.slice(lastSep + 1); + var hash = tx.subscriptHash(index, subscript, type); + + if (!script.isValidSig(sig)) + return false; + + // Strict order: + var res = script.verify(hash, sig.slice(0, -1), keys.pop()); + if (res) + succ++; + } + + // Extra value + stack.pop(); + + var res = succ >= m; + if (o === 'checkmultisigverify') { + if (!res) + return false; + } else { + stack.push(res ? [ 1 ] : []); + } + + break; + } + case 'checklocktimeverify': { + // OP_CHECKLOCKTIMEVERIFY = OP_NOP2 + // input: [[], sig1, sig2, 1] + // prev_out: [[lock], 'checklocktimeverify', 'drop', + // 'dup', 'hash160', pubkey, 'equalverify', 'checksig'] + if (!tx || stack.length === 0) + return false; + + var lock = new bn(stack[stack.length - 1]).toNumber(); + + if (lock < 0) + return false; + + var threshold = constants.locktimeThreshold; + if (!( + (tx.lock < threshold && lock < threshold) || + (tx.lock >= threshold && lock >= threshold) + )) { + return false; + } + + if (lock > tx.lock) + return false; + + if (!tx.inputs[index] || tx.inputs[index].seq === 0xffffffff) + return false; + + break; + } + case 'eval_': { + // OP_EVAL = OP_NOP1 + // var evalScript = script.decode(stack.pop()); + // if (!Array.isArray(evalScript)) + // return false; + // var res = script.execute(evalScript, stack, tx, index); + // if (!res) + // return false; + break; + } + default: { + // Unknown operation + return false; } - } else { - // Unknown operation - return false; } } + if (stack.length + stack.alt.length > 1000) + return false; + return true; }; +script.multisig = function(keys, m, n) { + if (keys.length < m) + throw new Error('wrong amount of pubkeys for multisig script'); + + assert(m >= 1 && m <= n); + assert(n >= 1 && n <= 7); + + // Format: + // op_[m] [pubkey1-len] [pubkey1] ... op_[n] op_checkmultisig + + // Using pushdata ops for m and n: + // return [ [ m ] ].concat( + // keys, + // [ [ n ], 'checkmultisig' ] + // ); + + // Keys need to be in a predictable order. + keys = keys.sort(function(a, b) { + return new bn(a).cmp(new bn(b)) > 0; + }); + + // Using OP_1-16 for m and n: + return [ m ].concat( + keys, + [ n, 'checkmultisig' ] + ); +}; + script.isPubkeyhash = function isPubkeyhash(s, hash) { if (s.length !== 5) return false; @@ -253,17 +778,21 @@ script.isMultisig = function isMultisig(s, key) { return false; var m = s[0]; + if (typeof m === 'number' && m >= 1 && m <= 16) + m = [m]; if (!Array.isArray(m) || m.length !== 1) return false; - m = m[0]; + m = m[0] || 0; if (s[s.length - 1] !== 'checkmultisig') return false; var n = s[s.length - 2]; + if (typeof n === 'number' && n >= 1 && n <= 16) + n = [n]; if (!Array.isArray(n) || n.length !== 1) return false; - n = n[0]; + n = n[0] || 0; if (n + 3 !== s.length) return false; @@ -291,14 +820,22 @@ script.isPubkeyhashInput = function isPubkeyhashInput(s) { 33 <= s[1].length && s[1].length <= 65; }; -script.isScripthash = function isScripthash(s) { +script.isScripthash = function isScripthash(s, hash) { if (s.length !== 3) return false; - return s[0] === 'hash160' && - Array.isArray(s[1]) && - s[1].length === 20 && - s[2] === 'eq'; + var ret = s[0] === 'hash160' && + Array.isArray(s[1]) && + s[1].length === 20 && + s[2] === 'eq'; + + if (!ret) + return false; + + if (hash) + return utils.isEqual(s[1], hash); + + return true; }; script.isNullData = function isNullData(s) { @@ -309,3 +846,98 @@ script.isNullData = function isNullData(s) { Array.isArray(s[1]) && s[1].length <= 40; }; + +// https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki +/** + * A canonical signature exists of: <30> <02> <02> + * Where R and S are not negative (their first byte has its highest bit not set), and not + * excessively padded (do not start with a 0 byte, unless an otherwise negative number follows, + * in which case a single 0 byte is necessary and even required). + * + * See https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623 + * + * This function is consensus-critical since BIP66. + */ +script.isValidSig = function(sig) { + // Empty signature. Not strictly DER encoded, but allowed to provide a + // compact way to provide an invalid signature for use with CHECK(MULTI)SIG + if (sig.length === 0) + return true; + + // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash] + // * total-length: 1-byte length descriptor of everything that follows, + // excluding the sighash byte. + // * R-length: 1-byte length descriptor of the R value that follows. + // * R: arbitrary-length big-endian encoded R value. It must use the shortest + // possible encoding for a positive integers (which means no null bytes at + // the start, except a single one when the next byte has its highest bit set). + // * S-length: 1-byte length descriptor of the S value that follows. + // * S: arbitrary-length big-endian encoded S value. The same rules apply. + // * sighash: 1-byte value indicating what data is hashed (not part of the DER + // signature) + + // Minimum and maximum size constraints. + if (sig.length < 9) + return false; + if (sig.length > 73) + return false; + + // A signature is of type 0x30 (compound). + if (sig[0] !== 0x30) + return false; + + // Make sure the length covers the entire signature. + if (sig[1] !== sig.length - 3) + return false; + + // Extract the length of the R element. + var lenR = sig[3]; + + // Make sure the length of the S element is still inside the signature. + if (5 + lenR >= sig.length) + return false; + + // Extract the length of the S element. + var lenS = sig[5 + lenR]; + + // Verify that the length of the signature matches the sum of the length + // of the elements. + if (lenR + lenS + 7 !== sig.length) + return false; + + // Check whether the R element is an integer. + if (sig[2] !== 0x02) + return false; + + // Zero-length integers are not allowed for R. + if (lenR === 0) + return false; + + // Negative numbers are not allowed for R. + if (sig[4] & 0x80) + return false; + + // Null bytes at the start of R are not allowed, unless R would + // otherwise be interpreted as a negative number. + if (lenR > 1 && (sig[4] === 0x00) && !(sig[5] & 0x80)) + return false; + + // Check whether the S element is an integer. + if (sig[lenR + 4] !== 0x02) + return false; + + // Zero-length integers are not allowed for S. + if (lenS === 0) + return false; + + // Negative numbers are not allowed for S. + if (sig[lenR + 6] & 0x80) + return false; + + // Null bytes at the start of S are not allowed, unless S would otherwise be + // interpreted as a negative number. + if (lenS > 1 && (sig[lenR + 6] === 0x00) && !(sig[lenR + 7] & 0x80)) + return false; + + return true; +}; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 7b15e799..86c5eb68 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -3,6 +3,7 @@ var bn = require('bn.js'); var bcoin = require('../bcoin'); var utils = bcoin.utils; var assert = utils.assert; +var constants = bcoin.protocol.constants; function TX(data, block) { if (!(this instanceof TX)) @@ -40,6 +41,10 @@ function TX(data, block) { // ps = Pending Since this.ps = this.ts === 0 ? +new Date() / 1000 : 0; + + this.change = data.change || null; + this.fee = data.fee || 10000; + this.dust = 5460; } module.exports = TX; @@ -56,6 +61,11 @@ TX.prototype.render = function render() { return bcoin.protocol.framer.tx(this); }; +TX.prototype.input = function input(i, index) { + this._input(i, index); + return this; +}; + TX.prototype._input = function _input(i, index) { if (i instanceof TX) i = { tx: i, index: index }; @@ -87,10 +97,10 @@ TX.prototype._input = function _input(i, index) { var index = this._inputIndex(hash, index); if (index !== -1) { var ex = this.inputs[index]; - - ex.out.tx = input.out.tx || ex.out.tx; - ex.seq = input.seq || ex.seq; - ex.script = input.script.length ? input.script : ex.script; + input.out.tx = input.out.tx || ex.out.tx; + input.seq = input.seq || ex.seq; + input.script = input.script.length ? input.script : ex.script; + this.inputs[index] = input; } else { this.inputs.push(input); index = this.inputs.length - 1; @@ -111,14 +121,186 @@ TX.prototype._inputIndex = function _inputIndex(hash, index) { return -1; }; -TX.prototype.input = function input(i, index) { - this._input(i, index); - return this; +TX.prototype.signature = function(input, key, type) { + if (!type) + type = 'all'; + + if (typeof type === 'string') + type = bcoin.protocol.constants.hashType[type]; + + // Get the previous output's subscript + var s = input.out.tx.getSubscript(input.out.index); + + // Get the hash of the current tx, minus the other inputs, plus the sighash. + var hash = this.subscriptHash(tx.inputs.indexOf(input), s, type); + + // Sign the transaction with our one input + var signature = bcoin.ecdsa.sign(hash, key.priv).toDER(); + + // Add the sighash as a single byte to the signature + signature = signature.concat(type); + + return signature; }; -TX.prototype.out = function out(output, value) { +// Build the scriptSigs for inputs, excluding the signatures +TX.prototype.scriptInput = function(input, pub) { + // Get the previous output's subscript + var s = input.out.tx.getSubscript(input.out.index); + + // Already has a script template (at least) + if (input.script.length) + return; + + // P2PKH and simple tx + if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { + input.script = [ [], pub ]; + return; + } + + // NOTE for multisig: Technically we should create m signature slots, + // but we create n signature slots so we can order the signatures properly. + + // Multisig + // raw format: OP_FALSE [sig-1] [sig-2] ... + if (bcoin.script.isMultisig(s)) { + input.script = [ [] ]; + var n = s[s.length - 2]; + // If using pushdata instead of OP_1-16: + if (Array.isArray(n)) + n = n[0] || 0; + for (var i = 0; i < n; i++) + input.script[i + 1] = []; + return; + } + + // P2SH multisig + // p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script] + if (bcoin.script.isScripthash(s)) { + input.script = [ [] ]; + var redeem = bcoin.script.decode(pub); + var n = redeem[redeem.length - 2]; + // If using pushdata instead of OP_1-16: + if (Array.isArray(n)) + n = n[0] || 0; + for (var i = 0; i < n; i++) + input.script[i + 1] = []; + // P2SH requires the redeem script after signatures + input.script.push(pub); + return; + } + + throw new Error('scriptInput(): could not identify prev_out type'); +}; + +// Sign the now-built scriptSigs +TX.prototype.signInput = function(input, key, type) { + if (!type) + type = 'all'; + + if (typeof type === 'string') + type = bcoin.protocol.constants.hashType[type]; + + // Get the previous output's subscript + var s = input.out.tx.getSubscript(input.out.index); + + // Get the hash of the current tx, minus the other inputs, plus the sighash. + var hash = this.subscriptHash(this.inputs.indexOf(input), s, type); + + // Sign the transaction with our one input + var signature = bcoin.ecdsa.sign(hash, key.priv).toDER(); + + // Add the sighash as a single byte to the signature + signature = signature.concat(type); + + // P2PKH and simple tx + if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { + input.script[0] = signature; + return; + } + + // Multisig + // raw format: OP_FALSE [sig-1] [sig-2] ... + // p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script] + if (bcoin.script.isMultisig(s) || bcoin.script.isScripthash(s)) { + var len = input.script.length; + var redeem; + + if (bcoin.script.isScripthash(s)) { + len--; + redeem = bcoin.script.decode(input.script[input.script.length - 1]); + } else { + redeem = s; + } + + var m = redeem[0]; + // If using pushdata instead of OP_1-16: + if (Array.isArray(m)) + m = m[0] || 0; + + var keys = redeem.slice(1, -2); + var pub = key.getPublic(true, 'array'); + var pubn = key.getPublic(false, 'array'); + + // Find the key index so we can place + // the signature in the same index. + for (var ki = 0; ki < keys.length; ki++) { + if (utils.isEqual(pub, keys[ki]) || utils.isEqual(pubn, keys[ki])) + break; + } + + if (ki === keys.length) + throw new Error('Public key is not in the prev_out script'); + + if (ki + 1 > len - 1) + throw new Error('No signature slot available'); + + // Add our signature to the correct slot + // and count the total number of signatures. + var totalSigs = 0; + for (var i = 1; i < len; i++) { + if (Array.isArray(input.script[i]) && input.script[i].length) { + totalSigs++; + continue; + } + + if (i - 1 === ki) { + if (totalSigs >= m) + continue; + input.script[i] = signature; + totalSigs++; + } + } + + // All signatures added. Finalize by removing empty slots. + if (totalSigs >= m) { + for (var i = len - 1; i >= 1; i--) { + if (Array.isArray(input.script[i]) && !input.script[i].length) + input.script.splice(i, 1); + } + } + + return; + } + + throw new Error('signInput(): could not identify prev_out type'); +}; + +// Build the scriptSig and sign it +TX.prototype.scriptSig = function(input, key, pub, type) { + // Build script for input + this.scriptInput(input, pub); + + // Sign input + this.signInput(input, key, type); + + return input.script; +}; + +TX.prototype.output = function output(output, value) { if (output instanceof bcoin.wallet) output = output.getAddress(); + if (typeof output === 'string') { output = { address: output, @@ -126,34 +308,86 @@ TX.prototype.out = function out(output, value) { }; } - var script = output.script ? output.script.slice() : []; + this.outputs.push({ + value: new bn(output.value), + script: this.scriptOutput(output) + }); - // Multisig script if given addresses - if (Array.isArray(output.keys || output.address)) { - var keys = output.keys || output.address; + return this; +}; + +// compat +TX.prototype.out = TX.prototype.output; + +TX.prototype.scriptOutput = function(options) { + var script = options.script ? options.script.slice() : []; + + if (Array.isArray(options.keys || options.address)) { + // Raw multisig transaction + // https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki + // https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki + // https://github.com/bitcoin/bips/blob/master/bip-0019.mediawiki + // [required-sigs] [pubkey-hash1] [pubkey-hash2] ... [number-of-keys] checkmultisig + var keys = options.keys || options.address; + + if (keys === options.address) { + keys = keys.map(function(address) { + return bcoin.wallet.addr2hash(address, 'normal'); + }); + } + + keys = keys.map(function(key) { + if (typeof key === 'string') + return utils.toKeyArray(key); + return key; + }); + + // compat: + options.m = options.minSignatures || options.m; + var m = options.m || keys.length; + var n = options.n || keys.length; + + assert(m >= 1 && m <= n); + if (options.hash) + assert(n >= 1 && n <= 7); + else + assert(n >= 1 && n <= 3); + + script = bcoin.script.multisig(keys, m, n); + } else if (bcoin.wallet.validateAddress(options.address, 'p2sh')) { + // p2sh transaction + // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki + // hash160 [20-byte-redeemscript-hash] equal script = [ - [ output.minSignatures || keys.length ] - ].concat( - keys, - [ [ keys.length ], 'checkmultisig' ] - ); - // Default script if given address - } else if (output.address) { + 'hash160', + bcoin.wallet.addr2hash(options.address, 'p2sh'), + 'eq' + ]; + } else if (options.address) { + // p2pkh transaction + // dup hash160 [pubkey-hash] equalverify checksig script = [ 'dup', 'hash160', - bcoin.wallet.addr2hash(output.address), + bcoin.wallet.addr2hash(options.address, 'normal'), 'eqverify', 'checksig' ]; } - this.outputs.push({ - value: new bn(output.value), - script: script - }); + // make it p2sh + if (options.hash) { + var redeem = script; + var hash = utils.ripesha(bcoin.script.encode(redeem)); + script = [ + 'hash160', + hash, + 'eq' + ]; + script.redeem = redeem; + } - return this; + return script; }; TX.prototype.getSubscript = function getSubscript(index) { @@ -167,13 +401,14 @@ TX.prototype.getSubscript = function getSubscript(index) { TX.prototype.subscriptHash = function subscriptHash(index, s, type) { var copy = this.clone(); + if (typeof type === 'string') + type = bcoin.protocol.constants.hashType[type]; + copy.inputs.forEach(function(input, i) { input.script = index === i ? s : []; }); var verifyStr = copy.render(); - verifyStr = verifyStr.concat( - bcoin.protocol.constants.hashType[type], 0, 0, 0 - ); + utils.writeU32(verifyStr, type, verifyStr.length); var hash = utils.dsha256(verifyStr); return hash; @@ -184,6 +419,9 @@ TX.prototype.verify = function verify(index, force) { if (!force && this.ts !== 0) return true; + if (this.inputs.length === 0) + return false; + return this.inputs.every(function(input, i) { if (index !== undefined && index !== i) return true; @@ -193,20 +431,46 @@ TX.prototype.verify = function verify(index, force) { assert(input.out.tx.outputs.length > input.out.index); - var subscript = input.out.tx.getSubscript(input.out.index); - var hash = this.subscriptHash(i, subscript, 'all'); - var stack = []; - bcoin.script.execute(input.script, stack); var prev = input.out.tx.outputs[input.out.index].script; - var res = bcoin.script.execute(prev, stack, hash); + + if (bcoin.script.isScripthash(prev)) { + // p2sh transactions cannot have anything + // other than pushdata ops in the scriptSig + var push = !input.script.slice(1).every(Array.isArray); + if (push) + return false; + } + + bcoin.script.execute(input.script, stack, this, i); + var res = bcoin.script.execute(prev, stack, this, i); if (!res) return false; - return stack.length > 0 && utils.isEqual(stack.pop(), [ 1 ]); + // Might be necessary for arithmetic: + // if (stack.length === 0 || new bn(stack.pop()).cmp(0) !== 0) + + if (stack.length === 0 || !utils.isEqual(stack.pop(), [ 1 ])) + return false; + + if (bcoin.script.isScripthash(prev)) { + var redeem = input.script[input.script.length - 1]; + if (!Array.isArray(redeem)) + return false; + redeem = bcoin.script.decode(redeem); + res = bcoin.script.execute(redeem, stack, this, i); + if (!res) + return false; + } + + return true; }, this); }; +TX.prototype.isCoinbase = function isCoinbase() { + return this.inputs.length === 1 && +this.inputs[0].out.hash === 0; +}; + TX.prototype.maxSize = function maxSize() { // Create copy with 0-script inputs var copy = this.clone(); @@ -217,9 +481,14 @@ TX.prototype.maxSize = function maxSize() { var size = copy.render().length; // Add size for signatures and public keys - copy.inputs.forEach(function(input) { - var s = input.out.tx.outputs[input.out.index].script; - if (bcoin.script.isPubkeyhash(s)) { + copy.inputs.forEach(function(input, i) { + // Get the previous output's script + // var s = input.out.tx.outputs[input.out.index].script; + + // Get the previous output's subscript + var s = input.out.tx.getSubscript(input.out.index); + + if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { // Signature + len size += 74; // Pub key + len @@ -227,23 +496,170 @@ TX.prototype.maxSize = function maxSize() { return; } - // Multisig - // Empty byte - size += 1; - // Signature + len - size += 74; - }); + if (bcoin.script.isMultisig(s)) { + // Multisig + // Empty byte + size += 1; + // Signature + len + var m = s[0]; + // If using pushdata instead of OP_1-16: + if (Array.isArray(m)) + m = m[0] || 0; + assert(m >= 1 && m <= 3); + size += 74 * m; + return; + } + + if (bcoin.script.isScripthash(s)) { + var script = this.inputs[i].script; + var redeem, m, n; + if (script.length) { + redeem = bcoin.script.decode(script[script.length - 1]); + m = redeem[0]; + n = redeem[redeem.length - 2]; + // If using pushdata instead of OP_1-16: + if (Array.isArray(m)) + m = m[0] || 0; + if (Array.isArray(n)) + n = n[0] || 0; + } else { + // May end up in a higher fee if we + // do not have the redeem script available. + m = 7; + n = 7; + } + assert(m >= 1 && m <= n); + assert(n >= 1 && n <= 7); + // Multisig + // Empty byte + size += 1; + // Signature + len + size += 74 * m; + // Redeem script + // m byte + size += 1; + // 1 byte length + 65 byte pubkey + size += 66 * n; + // n byte + size += 1; + // checkmultisig byte + size += 1; + return; + } + }, this); return size; }; +// Building a TX: +// 1. Add outputs: +// - this.output({ address: ..., value: ... }); +// - this.output({ address: ..., value: ... }); +// 2. Add inputs with utxos and change output: +// - this.fillUnspent(unspentItems, [changeAddr]); +// 3. Fill input scripts (for each input): +// - this.scriptInput(input, pub) +// - this.signInput(input, key, [sigHashType]) +TX.prototype.utxos = function utxos(unspent) { + // NOTE: tx should be prefilled with all outputs + var cost = this.funds('out'); + + // Use initial fee for starters + var fee = 1; + + // total = cost + fee + var total = cost.add(new bn(this.fee)); + + var inputs = this.inputs.slice(); + var utxos = []; + + var lastAdded = 0; + function addInput(unspent, i) { + // Add new inputs until TX will have enough funds to cover both + // minimum post cost and fee + var index = this._input(unspent); + utxos.push(this.inputs[index]); + lastAdded++; + return this.funds('in').cmp(total) < 0; + } + + // Transfer `total` funds maximum + // var unspent = wallet.unspent(); + unspent.every(addInput, this); + + // Add dummy output (for `left`) to calculate maximum TX size + this.output({ address: null, value: new bn(0) }); + + // Change fee value if it is more than 1024 bytes + // (10000 satoshi for every 1024 bytes) + do { + // Calculate maximum possible size after signing + var byteSize = this.maxSize(); + + var addFee = Math.ceil(byteSize / 1024) - fee; + total.iadd(new bn(addFee * this.fee)); + fee += addFee; + + // Failed to get enough funds, add more inputs + if (this.funds('in').cmp(total) < 0) + unspent.slice(lastAdded).every(addInput, this); + } while (this.funds('in').cmp(total) < 0 && lastAdded < unspent.length); + + // Still failing to get enough funds + if (this.funds('in').cmp(total) < 0) { + this.inputs = inputs; + this.outputs.pop(); + this.cost = total; + return null; + } + + // How much money is left after sending outputs + var left = this.funds('in').sub(total); + + // Clear the tx of everything we added. + this.inputs = inputs; + this.outputs.pop(); + this.cost = total; + + // Return necessary utxos and change. + return { + utxos: utxos, + change: left, + cost: total + }; +}; + +TX.prototype.fillUnspent = function fillUnspent(unspent, change) { + var result = this.utxos(unspent); + + if (!result) + return result; + + result.utxos.forEach(function(utxo) { + this.input(utxo, null); + }, this); + + // Not enough money, transfer everything to owner + if (result.change.cmpn(this.dust) < 0) { + // NOTE: that this output is either `postCost` or one of the `dust` values + this.outputs[this.outputs.length - 1].value.iadd(result.change); + } else { + this.output({ + address: change || this.change, + value: result.change + }); + } + + return result; +}; + TX.prototype.inputAddrs = function inputAddrs() { return this.inputs.filter(function(input) { return bcoin.script.isPubkeyhashInput(input.script); }).map(function(input) { var pub = input.script[1]; var hash = utils.ripesha(pub); - return bcoin.wallet.hash2addr(hash); + return bcoin.wallet.hash2addr(hash, 'normal'); }); }; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 44ac8387..a58e0acd 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -2,6 +2,7 @@ var utils = exports; var bn = require('bn.js'); var hash = require('hash.js'); +var util = require('util'); function toArray(msg, enc) { if (Array.isArray(msg)) @@ -118,6 +119,14 @@ utils.fromBase58 = function fromBase58(str) { return z.concat(res.toArray()); }; +utils.ripemd160 = function ripemd160(data, enc) { + return hash.ripemd160().update(data, enc).digest(); +}; + +utils.sha1 = function sha1(data, enc) { + return hash.sha1().update(data, enc).digest(); +}; + utils.ripesha = function ripesha(data, enc) { return hash.ripemd160().update(utils.sha256(data, enc)).digest(); }; @@ -417,6 +426,8 @@ RequestCache.prototype.fullfill = function fullfill(id, err, data) { utils.asyncify = function asyncify(fn) { return function _asynicifedFn(err, data1, data2) { + if (!fn) + return; utils.nextTick(function() { fn(err, data1, data2); }); @@ -466,3 +477,56 @@ utils.isIP = function(ip) { return 0; }; + +utils.isHex = function(msg) { + return typeof msg === 'string' && /^[0-9a-f]+$/i.test(msg); +}; + +utils.toKeyArray = function(msg) { + if (typeof msg !== 'string') + return msg; + + if (utils.isHex(msg)) + return utils.toArray(msg, 'hex'); + + return utils.fromBase58(msg); +}; + +utils.inspect = function(obj) { + return typeof obj !== 'string' + ? util.inspect(obj, null, 20, true) + : obj; +}; + +utils.print = function(msg) { + return typeof msg === 'object' + ? process.stdout.write(utils.inspect(msg) + '\n') + : console.log.apply(console, arguments); +}; + +utils.debug = function() { + var args = Array.prototype.slice.call(arguments); + args[0] = '\x1b[31m' + args[0] + '\x1b[m'; + return utils.print.apply(null, args); +}; + +utils.merge = function(target) { + var args = Array.prototype.slice.call(arguments, 1); + args.forEach(function(obj) { + Object.keys(obj).forEach(function(key) { + target[key] = obj[key]; + }); + }); + return target; +}; + +utils.fromBTC = function(btc) { + var satoshi = new bn(+btc || 0); + satoshi.imuln(100000000); + return satoshi; +}; + +utils.ntoBTC = function(satoshi) { + satoshi = new bn(Math.floor(+satoshi || 0).toString(16), 16); + return bcoin.utils.toBTC(satoshi); +}; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 877f7a44..dc4864aa 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -5,6 +5,8 @@ var inherits = require('inherits'); var EventEmitter = require('events').EventEmitter; var utils = bcoin.utils; var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; function Wallet(options, passphrase) { if (!(this instanceof Wallet)) @@ -22,6 +24,7 @@ function Wallet(options, passphrase) { if (!options) options = {}; + this.options = options; this.compressed = typeof options.compressed !== 'undefined' ? options.compressed : true; this.storage = options.storage; @@ -29,18 +32,45 @@ function Wallet(options, passphrase) { this.loaded = false; this.lastTs = 0; - if (options.passphrase) { + if (options.priv instanceof bcoin.hd.priv) { + this.hd = options.priv; + this.key = this.hd; + } else if (options.pub instanceof bcoin.hd.pub) { + this.hd = options.pub; + this.key = this.hd; + } else if (options.hd) { + this.hd = bcoin.hd.priv(options); + this.key = this.hd; + } else if (options.key) { + if ((options.key instanceof bcoin.hd.priv) + || (options.key instanceof bcoin.hd.pub)) { + this.hd = options.key; + this.key = options.key; + } else { + this.key = options.key; + } + } else if (options.passphrase) { this.key = bcoin.ecdsa.genKeyPair({ pers: options.scope, entropy: hash.sha256().update(options.passphrase).digest() }); } else if (options.priv || options.pub) { - this.key = bcoin.ecdsa.keyPair(options.priv || options.pub, 'hex'); + this.key = bcoin.ecdsa.keyPair({ + priv: options.priv, + pub: options.pub + }); } else { this.key = bcoin.ecdsa.genKeyPair(); } - this.prefix = 'bt/' + this.getAddress() + '/'; + this.addressType = 'normal'; + this.sharedKeys = []; + this.m = 1; + this.n = 1; + + this.multisig(options.multisig || {}); + + this.prefix = 'bt/' + this.getOwnAddress() + '/'; this.tx = new bcoin.txPool(this); // Just a constants, actually @@ -84,6 +114,48 @@ Wallet.prototype._init = function init() { }); }; +Wallet.prototype.multisig = function multisig(options) { + var pub = this.getOwnPublicKey(); + + options.type = options.type || options.addressType; + options.keys = options.keys || options.sharedKeys; + + this.addressType = options.type || 'normal'; + + // Multisig + this.sharedKeys = (options.keys || []).map(utils.toKeyArray); + this.m = options.m || 1; + this.n = options.n || 1; + + this.sharedKeys = this.sharedKeys.filter(function(key) { + return !utils.isEqual(key, pub); + }); + + // Use p2sh multisig by default + if (!options.addressType && this.sharedKeys.length) + this.addressType = 'p2sh'; + + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + + if (this.n < 1 || this.n > 7) + throw new Error('n ranges between 1 and 7'); + + if (this.sharedKeys.length < this.m - 1) + throw new Error(this.m + ' public keys required'); +}; + +Wallet.prototype.derive = function derive() { + var options = this.options; + + if (!this.hd) + throw new Error('wallet is not HD'); + + options.priv = this.hd.derive.apply(this.hd, arguments); + + return bcoin.wallet(options); +}; + Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { var priv = this.key.getPrivate(); if (priv) @@ -95,7 +167,7 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { if (enc === 'base58') { // We'll be using ncompressed public key as an address - var arr = [ 128 ]; + var arr = [ network.prefixes.privkey ]; // 0-pad key while (arr.length + priv.length < 33) @@ -110,39 +182,99 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { } }; -Wallet.prototype.getPublicKey = function getPublicKey(enc) { - var pub = this.key.getPublic(this.compressed, 'array'); +Wallet.prototype.getFullPublicKey = function getFullPublicKey(enc) { + var pub = this.getOwnPublicKey(); + + if (this.addressType === 'p2sh') { + var keys = this.getPublicKeys(); + pub = bcoin.script.encode(bcoin.script.multisig(keys, this.m, this.n)); + } + if (enc === 'base58') return utils.toBase58(pub); + else if (enc === 'hex') + return utils.toHex(pub); else return pub; }; +Wallet.prototype.getOwnPublicKey = function getOwnPublicKey(enc) { + var pub = this.key.getPublic(this.compressed, 'array'); + + if (enc === 'base58') + return utils.toBase58(pub); + else if (enc === 'hex') + return utils.toHex(pub); + else + return pub; +}; + +Wallet.prototype.getPublicKey = function getPublicKey(enc) { + return this.getFullPublicKey(enc); +}; + +Wallet.prototype.getPublicKeys = function() { + var pub = this.getOwnPublicKey(); + + this.sharedKeys = this.sharedKeys.filter(function(key) { + return !utils.isEqual(key, pub); + }); + + var keys = this.sharedKeys.slice().map(utils.toKeyArray); + + keys.push(pub); + + // Keys need to be in a predictable order. + keys = keys.sort(function(a, b) { + return new bn(a).cmp(new bn(b)) > 0; + }); + + return keys; +}; + +Wallet.prototype.getFullHash = function getFullHash() { + return utils.ripesha(this.getFullPublicKey()); +}; + +Wallet.prototype.getFullAddress = function getFullAddress() { + return Wallet.hash2addr(this.getFullHash(), this.addressType); +}; + +Wallet.prototype.getOwnHash = function getOwnHash() { + return utils.ripesha(this.getOwnPublicKey()); +}; + +Wallet.prototype.getOwnAddress = function getOwnAddress() { + return Wallet.hash2addr(this.getOwnHash(), this.addressType); +}; + Wallet.prototype.getHash = function getHash() { - return utils.ripesha(this.getPublicKey()); + return utils.ripesha(this.getFullPublicKey()); }; Wallet.prototype.getAddress = function getAddress() { - return Wallet.hash2addr(this.getHash()); + return Wallet.hash2addr(this.getFullHash(), this.addressType); }; -Wallet.hash2addr = function hash2addr(hash) { +Wallet.hash2addr = function hash2addr(hash, version) { hash = utils.toArray(hash, 'hex'); - // Add version - hash = [ 0 ].concat(hash); + version = network.prefixes[version || 'normal']; + hash = [ version ].concat(hash); var addr = hash.concat(utils.checksum(hash)); return utils.toBase58(addr); }; -Wallet.addr2hash = function addr2hash(addr) { +Wallet.addr2hash = function addr2hash(addr, version) { if (!Array.isArray(addr)) addr = utils.fromBase58(addr); + version = network.prefixes[version || 'normal']; + if (addr.length !== 25) return []; - if (addr[0] !== 0) + if (addr[0] !== version) return []; var chk = utils.checksum(addr.slice(0, -4)); @@ -152,15 +284,19 @@ Wallet.addr2hash = function addr2hash(addr) { return addr.slice(1, -4); }; -Wallet.prototype.validateAddress = function validateAddress(addr) { - var p = Wallet.addr2hash(addr); +Wallet.prototype.validateAddress = function validateAddress(addr, version) { + if (!addr) + return false; + var p = Wallet.addr2hash(addr, version); return p.length !== 0; }; Wallet.validateAddress = Wallet.prototype.validateAddress; Wallet.prototype.ownOutput = function ownOutput(tx, index) { - var hash = this.getHash(); - var key = this.getPublicKey(); + var scriptHash = this.getFullHash(); + var hash = this.getOwnHash(); + var key = this.getOwnPublicKey(); + var outputs = tx.outputs.filter(function(output, i) { if (index !== undefined && index !== i) return false; @@ -176,6 +312,9 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) { if (bcoin.script.isMultisig(s, key)) return true; + if (bcoin.script.isScripthash(s, scriptHash)) + return true; + return false; }, this); if (outputs.length === 0) @@ -185,8 +324,9 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) { }; Wallet.prototype.ownInput = function ownInput(tx, index) { - var hash = this.getHash(); - var key = this.getPublicKey(); + var scriptHash = this.getFullHash(); + var hash = this.getOwnHash(); + var key = this.getOwnPublicKey(); var inputs = tx.inputs.filter(function(input, i) { if (index !== undefined && index !== i) @@ -206,6 +346,9 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { if (bcoin.script.isMultisig(s, key)) return true; + if (bcoin.script.isScripthash(s, scriptHash)) + return true; + return false; }, this); if (inputs.length === 0) @@ -214,35 +357,22 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { return inputs; }; -Wallet.prototype.sign = function sign(tx, type, inputs, off) { +Wallet.prototype.sign = function sign(tx, type, inputs) { if (!type) type = 'all'; - assert.equal(type, 'all'); - if (!off) - off = 0; + var pub = this.getFullPublicKey(); + var key = this.key; - var pub = this.getPublicKey(); inputs = inputs || tx.inputs; // Add signature script to each input - inputs = inputs.filter(function(input, i) { + inputs = inputs.filter(function(input) { // Filter inputs that this wallet own if (!input.out.tx || !this.ownOutput(input.out.tx)) return false; - var s = input.out.tx.getSubscript(input.out.index); - var hash = tx.subscriptHash(off + i, s, type); - var signature = bcoin.ecdsa.sign(hash, this.key).toDER(); - signature = signature.concat(bcoin.protocol.constants.hashType[type]); - - if (bcoin.script.isPubkeyhash(s)) { - input.script = [ signature, pub ]; - return true; - } - - // Multisig - input.script = [ [], signature ]; + tx.scriptSig(input, key, pub, type); return true; }, this); @@ -272,89 +402,40 @@ Wallet.prototype.balance = function balance() { Wallet.prototype.fill = function fill(tx, cb) { cb = utils.asyncify(cb); - - // NOTE: tx should be prefilled with all outputs - var cost = tx.funds('out'); - - // Use initial fee for starters - var fee = 1; - - // total = cost + fee - var total = cost.add(new bn(this.fee)); - - var lastAdded = 0; - function addInput(unspent, i) { - // Add new inputs until TX will have enough funds to cover both - // minimum post cost and fee - tx.input(unspent); - lastAdded++; - return tx.funds('in').cmp(total) < 0; - } - - // Transfer `total` funds maximum - var unspent = this.unspent(); - unspent.every(addInput, this); - - // Add dummy output (for `left`) to calculate maximum TX size - tx.out(this, new bn(0)); - - // Change fee value if it is more than 1024 bytes - // (10000 satoshi for every 1024 bytes) - do { - // Calculate maximum possible size after signing - var byteSize = tx.maxSize(); - - var addFee = Math.ceil(byteSize / 1024) - fee; - total.iadd(new bn(addFee * this.fee)); - fee += addFee; - - // Failed to get enough funds, add more inputs - if (tx.funds('in').cmp(total) < 0) - unspent.slice(lastAdded).every(addInput, this); - } while (tx.funds('in').cmp(total) < 0 && lastAdded < unspent.length); - - // Still failing to get enough funds, notify caller - if (tx.funds('in').cmp(total) < 0) { + var result = tx.fillUnspent(this.unspent(), this.getAddress()); + if (!result) { var err = new Error('Not enough funds'); - err.minBalance = total; - return cb(err); + err.minBalance = tx.cost; + cb(err); + return null; } - - // How much money is left after sending outputs - var left = tx.funds('in').sub(total); - - // Not enough money, transfer everything to owner - if (left.cmpn(this.dust) < 0) { - // NOTE: that this output is either `postCost` or one of the `dust` values - tx.outputs[tx.outputs.length - 2].value.iadd(left); - left = new bn(0); - } - - // Change or remove last output if there is some money left - if (left.cmpn(0) === 0) - tx.outputs.pop(); - else - tx.outputs[tx.outputs.length - 1].value = left; - - // Sign transaction this.sign(tx); - cb(null, tx); + return tx; }; Wallet.prototype.toJSON = function toJSON() { return { v: 1, type: 'wallet', - pub: this.getPublicKey('base58'), + network: network.type, + pub: this.getOwnPublicKey('base58'), priv: this.getPrivateKey('base58'), - tx: this.tx.toJSON() + tx: this.tx.toJSON(), + multisig: { + type: this.addressType, + keys: this.sharedKeys.map(utils.toBase58), + m: this.m, + n: this.n + } }; }; Wallet.fromJSON = function fromJSON(json) { assert.equal(json.v, 1); assert.equal(json.type, 'wallet'); + if (json.network) + assert.equal(json.network, network.type); var priv; var pub; @@ -363,7 +444,7 @@ Wallet.fromJSON = function fromJSON(json) { if (json.priv) { var key = bcoin.utils.fromBase58(json.priv); assert(utils.isEqual(key.slice(-4), utils.checksum(key.slice(0, -4)))); - assert.equal(key[0], 128); + assert.equal(key[0], network.prefixes.privkey); key = key.slice(0, -4); if (key.length === 34) { @@ -379,10 +460,14 @@ Wallet.fromJSON = function fromJSON(json) { compressed = pub[0] !== 0x04; } + if (json.multisig && json.multisig.keys) + json.multisig.keys = json.multisig.keys.map(utils.toKeyArray); + var w = new Wallet({ priv: priv, pub: pub, - compressed: compressed + compressed: compressed, + multisig: json.multisig }); w.tx.fromJSON(json.tx); diff --git a/package.json b/package.json index 26759a4d..4e256420 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,9 @@ "homepage": "https://github.com/indutny/bcoin", "dependencies": { "async": "^0.8.0", - "bn.js": "^0.10.0", - "elliptic": "^0.14.1", - "hash.js": "^0.2.0", + "bn.js": "^4.5.0", + "elliptic": "^6.0.2", + "hash.js": "^1.0.3", "inherits": "^2.0.1" }, "devDependencies": { diff --git a/test/hd-test.js b/test/hd-test.js new file mode 100644 index 00000000..ac7d8534 --- /dev/null +++ b/test/hd-test.js @@ -0,0 +1,95 @@ +// var assert = require('assert'); +var bn = require('bn.js'); +var bcoin = require('../'); +var utils = bcoin.utils; +var assert = utils.assert; + +describe('HD', function() { + var phrase = 'volume doll flush federal inflict tomato result property total curtain shield aisle'; + + var seed = '5559092716434b83f158bffb51337a944529ae30d7e62d46d3be0c66fa4b36e8d60ccfd2c976b831885dc9df9ac3716ee4bf90003f25621070a49cbea58f528b'; + + var master_priv = 'xprv9s21ZrQH143K35zTejeVRhkXgegDFUVpoh8Mxs2BQmXEB4w9SZ1CuoJPuQ2KGQrS1ZF3Pk7V7KWHn7FqR2JbAE9Bh8PURnrFnrmArj4kxos'; + var master_pub = 'xpub661MyMwAqRbcFa4vkmBVnqhGEgWhewDgAv3xmFRny74D3sGHz6KTTbcskg2vZEMbEwxc4oaR435oczhSu4GdNwhwiVewcewU8A1Rr6HehAU'; + + var child1_priv = 'xprv9v414VeuxMoGt3t7jzkPni79suCfkgFwjxG38X2wgfg2mrYtV4Bhj3prhDDCcBiJrz9n4xLYoDtBFRuQmreVLKzmiZAqvbGpx5q4yHfzfah'; + var child1_pub = 'xpub693MU1BonjMa6Xxar2HQ9r3tRw3AA8yo7BBdvuSZF1D1eet32bVxGr9LYViWMtaLfQaa2StXeUmDG5VELFkU9pc3yfTzCk61WQJdR6ezj7a'; + + var child2_pub = 'xpub693MU1BonjMa8MMoz9opJhrFejcXcGmhMP9gzySLsip4Dz1UrSLT4i2pAimHDyM2onW2H2L2HkbwrZqoizQLMoErXu8mPYxDf8tJUBAfBuT'; + + var child3_priv = 'xprv9v414VeuxMoGusHLt8GowZuX6hn3Cp3qzAE6Cb2jKPH5MBgLJu2CWuiLKTdWV8WoNFYvpCcBfbpWfeyEQ8zytZW5qy39roTcugBGUkeAvCc'; + var child3_pub = 'xpub693MU1BonjMa8MMoz9opJhrFejcXcGmhMP9gzySLsip4Dz1UrSLT4i2pAimHDyM2onW2H2L2HkbwrZqoizQLMoErXu8mPYxDf8tJUBAfBuT'; + + var child4_priv = 'xprv9v414VeuxMoGyViVYuzEN5vLDzff3nkrH5Bf4KzD1iTeY855Q4cCc6xPPNoc6MJcsqqRQiGqR977cEEGK2mhVp7ALKHqY1icEw3Q9UmfQ1v'; + var child4_pub = 'xpub693MU1BonjMaBynxewXEjDs4n2W9TFUheJ7FriPpa3zdQvQDwbvT9uGsEebvioAcYbtRUU7ge4yVgj8WDLrwtwuXKTWiieFoYX2B1JYUEfK'; + + var child5_priv = 'xprv9xaK29Nm86ytEwsV9YSsL3jWYR6KtZYY3cKdjAbxHrwKyxH9YWoxvqKwtgQmExGpxAEDrwB4WK9YG1iukth3XiSgsxXLK1W3NB31gLee8fi'; + var child5_pub = 'xpub6BZfReuexUYBTRwxFZyshBgF6SvpJ2GPQqFEXZ1ZrCUJrkcJ648DUdeRjx9QiNQxQvPcHYV3rGkvuExFQbVRS4kU5ynx4fAsWWhHgyPh1pP'; + + var child6_priv = 'xprv9xaK29Nm86ytGx9uDhNKUBjvbJ1sAEM11aYxGQS66Rmg6oHwy7HbB6kWwMHvukzdbPpGhfNXhZgePWFHm1DCh5PACPFywJJKr1AnUJTLjUc'; + var child6_pub = 'xpub6BZfReuexUYBVSENKiuKqKgf9KrMZh4rNoUZ4nqhemJeybd6Webqiu4zndBwa9UB4Jvr5jB5Bcgng6reXAKCuDiVm7zhzJ13BUDBiM8HidZ'; + + var master, child1, child2, child3, child4, child5, child6; + + it('should create a pbkdf2 seed', function() { + var checkSeed = utils.toHex(bcoin.hd.pbkdf2(phrase, 'mnemonic' + 'foo', 2048, 64)); + assert.equal(checkSeed, seed); + }); + + it('should create master private key', function() { + master = bcoin.hd.priv({ seed: seed }); + assert.equal(master.xprivkey, master_priv); + assert.equal(master.xpubkey, master_pub); + }); + + it('should derive(0) child from master', function() { + child1 = master.derive(0); + assert.equal(child1.xprivkey, child1_priv); + assert.equal(child1.xpubkey, child1_pub); + }); + + it('should derive(1) child from master public key', function() { + child2 = master.hdpub.derive(1); + assert.equal(child2.xpubkey, child2_pub); + }); + + it('should derive(1) child from master', function() { + child3 = master.derive(1); + assert.equal(child3.xprivkey, child3_priv); + assert.equal(child3.xpubkey, child3_pub); + }); + + it('should derive(2) child from master', function() { + child4 = master.derive(2); + assert.equal(child4.xprivkey, child4_priv); + assert.equal(child4.xpubkey, child4_pub); + }); + + it('should derive(0) child from child(2)', function() { + child5 = child4.derive(0); + assert.equal(child5.xprivkey, child5_priv); + assert.equal(child5.xpubkey, child5_pub); + }); + + it('should derive(1) child from child(2)', function() { + child6 = child4.derive(1); + assert.equal(child6.xprivkey, child6_priv); + assert.equal(child6.xpubkey, child6_pub); + }); + + it('should deserialize master private key', function() { + master._unbuild(master.xprivkey); + }); + + it('should deserialize master public key', function() { + master.hdpub._unbuild(master.hdpub.xpubkey); + }); + + it('should create an hd seed', function() { + var seed = new bcoin.hd.seed({ + // I have the same combination on my luggage: + entropy: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + passphrase: 'foo' + }); + }); +}); diff --git a/test/script-test.js b/test/script-test.js index 3b5e24ea..e3a91f8e 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -44,4 +44,46 @@ describe('Script', function() { var decoded = bcoin.script.decode(encoded); assert(bcoin.script.isNullData(decoded)) }) + + it('should handle if statements correctly', function () { + var inputScript = [1, 2]; + var prevOutScript = [2, 'eq', 'if_', 3, 'else_', 4, 'endif', 5]; + var stack = []; + bcoin.script.execute(inputScript, stack); + var res = bcoin.script.execute(prevOutScript, stack); + assert(res); + assert.deepEqual(stack.slice(), [[1], [3], [5]]); + + var inputScript = [1, 2]; + var prevOutScript = [9, 'eq', 'if_', 3, 'else_', 4, 'endif', 5]; + var stack = []; + bcoin.script.execute(inputScript, stack); + var res = bcoin.script.execute(prevOutScript, stack); + assert(res); + assert.deepEqual(stack.slice(), [[1], [4], [5]]); + + var inputScript = [1, 2]; + var prevOutScript = [2, 'eq', 'if_', 3, 'endif', 5]; + var stack = []; + bcoin.script.execute(inputScript, stack); + var res = bcoin.script.execute(prevOutScript, stack); + assert(res); + assert.deepEqual(stack.slice(), [[1], [3], [5]]); + + var inputScript = [1, 2]; + var prevOutScript = [9, 'eq', 'if_', 3, 'endif', 5]; + var stack = []; + bcoin.script.execute(inputScript, stack); + var res = bcoin.script.execute(prevOutScript, stack); + assert(res); + assert.deepEqual(stack.slice(), [[1], [5]]); + + var inputScript = [1, 2]; + var prevOutScript = [9, 'eq', 'notif', 3, 'endif', 5]; + var stack = []; + bcoin.script.execute(inputScript, stack); + var res = bcoin.script.execute(prevOutScript, stack); + assert(res); + assert.deepEqual(stack.slice(), [[1], [3], [5]]); + }) }); diff --git a/test/wallet-test.js b/test/wallet-test.js index e244bba3..1b88b002 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -2,6 +2,33 @@ var assert = require('assert'); var bn = require('bn.js'); var bcoin = require('../'); +function printScript(input) { + var scripts = []; + var script = input.script; + scripts.push(script); + var prev = input.out.tx.outputs[input.out.index].script; + scripts.push(prev); + if (bcoin.script.isScripthash(prev)) { + var redeem = bcoin.script.decode(input.script[input.script.length - 1]); + scripts.push(redeem); + } + scripts = scripts.map(function(script) { + return script.map(function(chunk) { + if (Array.isArray(chunk)) { + if (chunk.length === 0) + return [0]; + return [bcoin.utils.toHex(chunk)]; + } + if (typeof chunk === 'number') + return [chunk]; + return chunk; + }); + }); + scripts.forEach(function(script) { + console.log(script); + }); +} + describe('Wallet', function() { it('should generate new key and address', function() { var w = bcoin.wallet(); @@ -46,13 +73,15 @@ describe('Wallet', function() { it('should multisign/verify TX', function() { var w = bcoin.wallet(); + var k2 = bcoin.wallet().getPublicKey(); // Input transcation var src = bcoin.tx({ outputs: [{ value: 5460 * 2, minSignatures: 1, - address: [ w.getPublicKey(), w.getPublicKey().concat(1) ] + keys: [ w.getPublicKey(), w.getPublicKey().concat(1) ] + // keys: [ w.getPublicKey(), k2 ] }, { value: 5460 * 2, address: w.getAddress() + 'x' @@ -223,4 +252,88 @@ describe('Wallet', function() { cb(); }); + + it('should verify 2-of-3 p2sh tx', function(cb) { + var hd = bcoin.hd.priv(); + var hd1 = hd.derive(0); + var hd2 = hd.derive(1); + var hd3 = hd.derive(2); + + // Generate 3 key pairs + var key1 = bcoin.ecdsa.genKeyPair(); + var key2 = bcoin.ecdsa.genKeyPair(); + var key3 = bcoin.ecdsa.genKeyPair(); + + // var key1 = hd1; + // var key2 = hd2; + // var key3 = hd3; + + // Grab the 3 pubkeys + var pub1 = key1.getPublic(true, 'array'); + var pub2 = key2.getPublic(true, 'array'); + var pub3 = key3.getPublic(true, 'array'); + + // Create 3 2-of-3 wallets with our pubkeys as "shared keys" + var w1 = bcoin.wallet({ + key: key1, + multisig: { + type: 'p2sh', + keys: [pub2, pub3], + m: 2, + n: 3 + } + }); + var w2 = bcoin.wallet({ + key: key2, + multisig: { + type: 'p2sh', + keys: [pub1, pub3], + m: 2, + n: 3 + } + }); + var w3 = bcoin.wallet({ + key: key3, + multisig: { + type: 'p2sh', + keys: [pub1, pub2], + m: 2, + n: 3 + } + }); + var receive = bcoin.wallet(); + + // Our p2sh address + var addr = w1.getAddress(); + assert.equal(w1.getAddress(), addr); + assert.equal(w2.getAddress(), addr); + assert.equal(w3.getAddress(), addr); + + // Add a shared unspent transaction to our wallets + var utx = bcoin.tx(); + utx.output({ address: addr, value: 5460 * 10 }); + + w1.addTX(utx); + w2.addTX(utx); + w3.addTX(utx); + + // Create a tx requiring 2 signatures + var send = bcoin.tx(); + send.output({ address: receive.getAddress(), value: 5460 }); + assert(!send.verify()); + var result = w1.fill(send); + assert(result); + + // printScript(send.inputs[0]); + + assert(!send.verify()); + w2.sign(send); + + assert(send.verify()); + + send.inputs[0].script[2] = []; + assert(!send.verify()); + + cb(); + }); });