diff --git a/.mailmap b/.mailmap
new file mode 100644
index 000000000000..cc9834bb9fe5
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,12 @@
+Jeffrey Wilcke <jeffrey@ethereum.org>
+Jeffrey Wilcke <jeffrey@ethereum.org> <geffobscura@gmail.com>
+Jeffrey Wilcke <jeffrey@ethereum.org> <obscuren@obscura.com>
+Jeffrey Wilcke <jeffrey@ethereum.org> <obscuren@users.noreply.github.com>
+
+Viktor TrĂ³n <viktor.tron@gmail.com>
+
+Joseph Goulden <joegoulden@gmail.com>
+
+Nick Savers <nicksavers@gmail.com>
+
+Maran Hidskes <maran.hidskes@gmail.com>
\ No newline at end of file
diff --git a/update-license.go b/update-license.go
new file mode 100644
index 000000000000..d5e21fdd3f75
--- /dev/null
+++ b/update-license.go
@@ -0,0 +1,247 @@
+// +build none
+/*
+This command generates GPL license headers on top of all source files.
+You can run it once per month, before cutting a release or just
+whenever you feel like it.
+
+	go run update-license.go
+
+The copyright in each file is assigned to any authors for which git
+can find commits in the file's history. It will try to follow renames
+throughout history. The author names are mapped and deduplicated using
+the .mailmap file. You can use .mailmap to set the canonical name and
+address for each author. See git-shortlog(1) for an explanation
+of the .mailmap format.
+
+Please review the resulting diff to check whether the correct
+copyright assignments are performed.
+*/
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"regexp"
+	"runtime"
+	"sort"
+	"strings"
+	"sync"
+	"text/template"
+)
+
+var (
+	// only files with these extensions will be considered
+	extensions = []string{".go", ".js", ".qml"}
+
+	// paths with any of these prefixes will be skipped
+	skipPrefixes = []string{"tests/files/", "cmd/mist/assets/ext/", "cmd/mist/assets/muted/"}
+
+	// paths with this prefix are licensed as GPL. all other files are LGPL.
+	gplPrefixes = []string{"cmd/"}
+
+	// this regexp must match the entire license comment at the
+	// beginning of each file.
+	licenseCommentRE = regexp.MustCompile(`(?s)^/\*\s*(Copyright|This file is part of) .*?\*/\n*`)
+
+	// this line is used when git doesn't find any authors for a file
+	defaultCopyright = "Copyright (C) 2014 Jeffrey Wilcke <jeffrey@ethereum.org>"
+)
+
+// this template generates the license comment.
+// its input is an info structure.
+var licenseT = template.Must(template.New("").Parse(`/*
+	{{.Copyrights}}
+
+	This file is part of go-ethereum
+
+	go-ethereum is free software: you can redistribute it and/or modify
+	it under the terms of the GNU {{.License}} as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	go-ethereum is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU {{.License}} for more details.
+
+	You should have received a copy of the GNU {{.License}}
+	along with go-ethereum.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+`))
+
+type info struct {
+	file    string
+	mode    os.FileMode
+	authors map[string][]string // map keys are authors, values are years
+	gpl     bool
+}
+
+func (i info) Copyrights() string {
+	var lines []string
+	for name, years := range i.authors {
+		lines = append(lines, "Copyright (C) "+strings.Join(years, ", ")+" "+name)
+	}
+	if len(lines) == 0 {
+		lines = []string{defaultCopyright}
+	}
+	sort.Strings(lines)
+	return strings.Join(lines, "\n\t")
+}
+
+func (i info) License() string {
+	if i.gpl {
+		return "General Public License"
+	} else {
+		return "Lesser General Public License"
+	}
+}
+
+func (i info) ShortLicense() string {
+	if i.gpl {
+		return "GPL"
+	} else {
+		return "LGPL"
+	}
+}
+
+func (i *info) addAuthorYear(name, year string) {
+	for _, y := range i.authors[name] {
+		if y == year {
+			return
+		}
+	}
+	i.authors[name] = append(i.authors[name], year)
+	sort.Strings(i.authors[name])
+}
+
+func main() {
+	files := make(chan string)
+	infos := make(chan *info)
+	wg := new(sync.WaitGroup)
+
+	go getFiles(files)
+	for i := runtime.NumCPU(); i >= 0; i-- {
+		// getting file info is slow and needs to be parallel
+		wg.Add(1)
+		go getInfo(files, infos, wg)
+	}
+	go func() { wg.Wait(); close(infos) }()
+	writeLicenses(infos)
+}
+
+func getFiles(out chan<- string) {
+	cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD")
+	err := doLines(cmd, func(line string) {
+		for _, p := range skipPrefixes {
+			if strings.HasPrefix(line, p) {
+				return
+			}
+		}
+		ext := path.Ext(line)
+		for _, wantExt := range extensions {
+			if ext == wantExt {
+				goto send
+			}
+		}
+		return
+
+	send:
+		out <- line
+	})
+	if err != nil {
+		fmt.Println("error getting files:", err)
+	}
+	close(out)
+}
+
+func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) {
+	for file := range files {
+		stat, err := os.Lstat(file)
+		if err != nil {
+			fmt.Printf("ERROR %s: %v\n", file, err)
+			continue
+		}
+		if !stat.Mode().IsRegular() {
+			continue
+		}
+		info, err := fileInfo(file)
+		if err != nil {
+			fmt.Printf("ERROR %s: %v\n", file, err)
+			continue
+		}
+		info.mode = stat.Mode()
+		out <- info
+	}
+	wg.Done()
+}
+
+func fileInfo(file string) (*info, error) {
+	info := &info{file: file, authors: make(map[string][]string)}
+	for _, p := range gplPrefixes {
+		if strings.HasPrefix(file, p) {
+			info.gpl = true
+			break
+		}
+	}
+	cmd := exec.Command("git", "log", "--follow", "--find-copies", "--pretty=format:%aI | %aN <%aE>", "--", file)
+	err := doLines(cmd, func(line string) {
+		sep := strings.IndexByte(line, '|')
+		year, name := line[:4], line[sep+2:]
+		info.addAuthorYear(name, year)
+	})
+	return info, err
+}
+
+func writeLicenses(infos <-chan *info) {
+	buf := new(bytes.Buffer)
+	for info := range infos {
+		content, err := ioutil.ReadFile(info.file)
+		if err != nil {
+			fmt.Printf("ERROR: couldn't read %s: %v\n", info.file, err)
+			continue
+		}
+
+		// construct new file content
+		buf.Reset()
+		licenseT.Execute(buf, info)
+		if m := licenseCommentRE.FindIndex(content); m != nil && m[0] == 0 {
+			buf.Write(content[m[1]:])
+		} else {
+			buf.Write(content)
+		}
+
+		if !bytes.Equal(content, buf.Bytes()) {
+			fmt.Println("writing", info.ShortLicense(), info.file)
+			if err := ioutil.WriteFile(info.file, buf.Bytes(), info.mode); err != nil {
+				fmt.Printf("ERROR: couldn't write %s: %v", info.file, err)
+			}
+		}
+	}
+}
+
+func doLines(cmd *exec.Cmd, f func(string)) error {
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return err
+	}
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+	s := bufio.NewScanner(stdout)
+	for s.Scan() {
+		f(s.Text())
+	}
+	if s.Err() != nil {
+		return s.Err()
+	}
+	if err := cmd.Wait(); err != nil {
+		return fmt.Errorf("%v (for %s)", err, strings.Join(cmd.Args, " "))
+	}
+	return nil
+}