Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7d577f383e | |||
| 03f30f1968 | |||
|   | ba1196a962 | ||
|   | a0aba28a2a | ||
|   | 4cb1ec89c0 | ||
|   | 1b0ecd993e | ||
|   | 57291497e6 | 
							
								
								
									
										203
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										203
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,80 +1,175 @@ | |||||||
| # opentimestamps | # opentimestamps | ||||||
|  |  | ||||||
| Interact with calendar servers, create and verify OTS attestations. | A fork of [github.com/nbd-wtf/opentimestamps](https://github.com/nbd-wtf/opentimestamps) that lets you interact with calendar servers, create and verify OTS attestations. | ||||||
|  |  | ||||||
| # How to use | # How to use | ||||||
|  |  | ||||||
| Full documentation at https://pkg.go.dev/github.com/nbd-wtf/opentimestamps. See some commented pseudocode below (you probably should not try to run it as it is). | Here's an example of how to use the library to create a timestamp, attempt to upgrade it periodically, and display information about it: | ||||||
|  |  | ||||||
| ```go | ```go | ||||||
| package main | package main | ||||||
|  |  | ||||||
| import "github.com/nbd-wtf/opentimestamps" | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| func main () { | 	"git.intruders.space/public/opentimestamps" | ||||||
|     // create a timestamp at a specific calendar server | 	"git.intruders.space/public/opentimestamps/ots" | ||||||
|     hash := sha256.Sum256([]byte{1,2,3,4,5,6}) | ) | ||||||
|     seq, _ := opentimestamps.Stamp(context.Background(), "https://alice.btc.calendar.opentimestamps.org/", hash) |  | ||||||
|  |  | ||||||
|     // you can just call .Upgrade() to get the upgraded sequence (or an error if not yet available) | func main() { | ||||||
|     upgradedSeq, err := seq.Upgrade(context.Background(), hash[:]) | 	// Read a file to timestamp | ||||||
|     if err != nil { | 	fileData, err := os.ReadFile("document.txt") | ||||||
|         fmt.Println("wait more") | 	if err != nil { | ||||||
|     } | 		fmt.Println("Error reading file:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|     // a File is a struct that represents the content of an .ots file, which contains the initial digest and any number of sequences | 	// Calculate the digest | ||||||
|     file := File{ | 	digest := sha256.Sum256(fileData) | ||||||
|         Digest: hash, | 	fmt.Printf("File digest: %x\n", digest) | ||||||
|         Sequences: []Sequence{seq}, |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // it can be written to disk | 	// Define calendar servers | ||||||
|     os.WriteFile("file.ots", file.SerializeToFile(), 0644) | 	calendars := []string{ | ||||||
|  | 		"https://alice.btc.calendar.opentimestamps.org", | ||||||
|  | 		"https://bob.btc.calendar.opentimestamps.org", | ||||||
|  | 		"https://finney.calendar.eternitywall.com", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|     // or printed in human-readable format | 	// Create a timestamp using each calendar | ||||||
|     fmt.Println(file.Human()) | 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|     // sequences are always composed of a bunch of operation instructions -- these can be, for example, "append", "prepend", "sha256" | 	var sequences []ots.Sequence | ||||||
|     fmt.Println(seq[0].Operation.Name) // "append" | 	for _, calendarURL := range calendars { | ||||||
|     fmt.Println(seq[1].Operation.Name) // "sha256" | 		fmt.Printf("Submitting to %s...\n", calendarURL) | ||||||
|     fmt.Println(seq[2].Operation.Name) // "prepend" | 		seq, err := opentimestamps.Stamp(ctx, calendarURL, digest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("Failed to submit to %s: %v\n", calendarURL, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("Submission to %s successful\n", calendarURL) | ||||||
|  | 		sequences = append(sequences, seq) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|     // "prepend" and "append" are "binary", i.e. they take an argument | 	if len(sequences) == 0 { | ||||||
|     fmt.Println(hex.EncodeToString(seq[2].Argument)) // "c40fe258f9b828a0b5a7" | 		fmt.Println("Failed to create any timestamps") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|     // all these instructions can be executed in order, starting from the initial hash | 	// Create the timestamp file | ||||||
|     result := seq.Compute(hash) // this is the value we send to the calendar server in order to get the upgraded sequence on .Upgrade() | 	file := &ots.File{ | ||||||
|     finalResult := upgradedSeq.Compute(hash) // this should be the merkle root of a bitcoin block if this sequence is upgraded | 		Digest:    digest[:], | ||||||
|  | 		Sequences: sequences, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|     // each sequence always ends in an "attestation" | 	// Save the OTS file | ||||||
|     // it can be either a pending attestation, i.e. a reference to a calendar server from which we will upgrade this sequence later | 	otsData := file.SerializeToFile() | ||||||
|     fmt.Println(seq[len(seq)-1].Attestation.CalendarServerURL) // "https://alice.btc.calendar.opentimestamps.org/" | 	if err := os.WriteFile("document.txt.ots", otsData, 0644); err != nil { | ||||||
|     // or it can be a reference to a bitcoin block, the merkle root of which we will check against the result of Compute() for verifying | 		fmt.Println("Failed to save OTS file:", err) | ||||||
|     fmt.Println(upgradedSeq[len(upgradedSeq)-1].Attestation.BitcoinBlockHeight) // 810041 | 		return | ||||||
|  | 	} | ||||||
|  | 	fmt.Println("Timestamp file created successfully") | ||||||
|  |  | ||||||
|     // speaking of verifying, this is how we do it: | 	// Display initial timestamp info | ||||||
|     // first we need some source of bitcoin blocks, | 	fmt.Println("\nInitial timestamp info:") | ||||||
|     var bitcoin opentimestamps.Bitcoin | 	fmt.Println(file.Human(false)) | ||||||
|     if useLocallyRunningBitcoindNode { |  | ||||||
|         // it can be either a locally running bitcoind node |  | ||||||
|         bitcoin, _ = opentimestamps.NewBitcoindInterface(rpcclient.ConnConfig{ |  | ||||||
|             User:         "nakamoto", |  | ||||||
|             Pass:         "mumbojumbo", |  | ||||||
|             HTTPPostMode: true, |  | ||||||
|         }) |  | ||||||
|     } else { |  | ||||||
|         // or an esplora HTTP endpoint |  | ||||||
|         bitcoin = opentimestamps.NewEsploraClient("https://blockstream.info/api") |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // then we pass that to a sequence | 	// Attempt to upgrade the timestamp every 20 minutes | ||||||
|     if err := upgradedSeq.Verify(bitcoin, hash); err == nil { | 	fmt.Println("\nWill check for upgrades every 20 minutes...") | ||||||
|         fmt.Println("it works!") |  | ||||||
|     } | 	maxAttempts := 12 // Try for about 4 hours (12 * 20 minutes) | ||||||
|  | 	for attempt := 0; attempt < maxAttempts; attempt++ { | ||||||
|  | 		if attempt > 0 { | ||||||
|  | 			fmt.Printf("\nWaiting 20 minutes before next upgrade attempt (%d/%d)...\n", attempt+1, maxAttempts) | ||||||
|  | 			time.Sleep(20 * time.Minute) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		upgraded := false | ||||||
|  | 		pendingSequences := file.GetPendingSequences() | ||||||
|  | 		if len(pendingSequences) == 0 { | ||||||
|  | 			fmt.Println("No pending sequences to upgrade") | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		fmt.Printf("Attempting to upgrade %d pending sequences...\n", len(pendingSequences)) | ||||||
|  |  | ||||||
|  | 		upgradeCtx, upgradeCancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
|  | 		for _, seq := range pendingSequences { | ||||||
|  | 			att := seq.GetAttestation() | ||||||
|  | 			fmt.Printf("Trying to upgrade sequence from %s...\n", att.CalendarServerURL) | ||||||
|  |  | ||||||
|  | 			upgradedSeq, err := opentimestamps.UpgradeSequence(upgradeCtx, seq, digest[:]) | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Printf("Failed to upgrade sequence from %s: %v\n", att.CalendarServerURL, err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Replace the sequence in the file | ||||||
|  | 			for i, origSeq := range file.Sequences { | ||||||
|  | 				origAtt := origSeq.GetAttestation() | ||||||
|  | 				if origAtt.CalendarServerURL == att.CalendarServerURL { | ||||||
|  | 					file.Sequences[i] = upgradedSeq | ||||||
|  | 					upgraded = true | ||||||
|  |  | ||||||
|  | 					newAtt := upgradedSeq.GetAttestation() | ||||||
|  | 					if newAtt.BitcoinBlockHeight > 0 { | ||||||
|  | 						fmt.Printf("Sequence upgraded! Confirmed in Bitcoin block %d\n", newAtt.BitcoinBlockHeight) | ||||||
|  | 					} else { | ||||||
|  | 						fmt.Println("Sequence updated but still pending") | ||||||
|  | 					} | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		upgradeCancel() | ||||||
|  |  | ||||||
|  | 		if upgraded { | ||||||
|  | 			// Save the upgraded file | ||||||
|  | 			otsData = file.SerializeToFile() | ||||||
|  | 			if err := os.WriteFile("document.txt.ots", otsData, 0644); err != nil { | ||||||
|  | 				fmt.Println("Failed to save upgraded OTS file:", err) | ||||||
|  | 			} else { | ||||||
|  | 				fmt.Println("Upgraded timestamp file saved") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If all sequences are confirmed, we're done | ||||||
|  | 		if len(file.GetPendingSequences()) == 0 { | ||||||
|  | 			fmt.Println("All sequences are now confirmed in the Bitcoin blockchain!") | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Final report | ||||||
|  | 	fmt.Println("\nFinal timestamp status:") | ||||||
|  |  | ||||||
|  | 	confirmedSeqs := file.GetBitcoinAttestedSequences() | ||||||
|  | 	pendingSeqs := file.GetPendingSequences() | ||||||
|  |  | ||||||
|  | 	fmt.Printf("Confirmed attestations: %d\n", len(confirmedSeqs)) | ||||||
|  | 	for _, seq := range confirmedSeqs { | ||||||
|  | 		att := seq.GetAttestation() | ||||||
|  | 		fmt.Printf("- Confirmed in Bitcoin block %d\n", att.BitcoinBlockHeight) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fmt.Printf("Pending attestations: %d\n", len(pendingSeqs)) | ||||||
|  | 	for _, seq := range pendingSeqs { | ||||||
|  | 		att := seq.GetAttestation() | ||||||
|  | 		fmt.Printf("- Still pending at %s\n", att.CalendarServerURL) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fmt.Println("\nDetailed timestamp info:") | ||||||
|  | 	fmt.Println(file.Human(true)) | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| You can also take a look at [`ots`](https://github.com/fiatjaf/ots), a simple CLI to OpenTimestamps which is basically a wrapper over this library. | This repository includes an `ots` command line tool which provides a convenient interface to the core library functionality. Run it without arguments to see usage information. | ||||||
|  |  | ||||||
|  | You can also take a look at the original [`ots`](https://github.com/fiatjaf/ots) CLI which is another implementation based on the same concepts. | ||||||
|  |  | ||||||
| # License | # License | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										158
									
								
								cmd/example/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								cmd/example/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/ots" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	// Read a file to timestamp | ||||||
|  | 	fileData, err := os.ReadFile("document.txt") | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println("Error reading file:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Calculate the digest | ||||||
|  | 	digest := sha256.Sum256(fileData) | ||||||
|  | 	fmt.Printf("File digest: %x\n", digest) | ||||||
|  |  | ||||||
|  | 	// Define calendar servers | ||||||
|  | 	calendars := []string{ | ||||||
|  | 		"https://alice.btc.calendar.opentimestamps.org", | ||||||
|  | 		"https://bob.btc.calendar.opentimestamps.org", | ||||||
|  | 		"https://finney.calendar.eternitywall.com", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create a timestamp using each calendar | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	var sequences []ots.Sequence | ||||||
|  | 	for _, calendarURL := range calendars { | ||||||
|  | 		fmt.Printf("Submitting to %s...\n", calendarURL) | ||||||
|  | 		seq, err := opentimestamps.Stamp(ctx, calendarURL, digest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("Failed to submit to %s: %v\n", calendarURL, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("Submission to %s successful\n", calendarURL) | ||||||
|  | 		sequences = append(sequences, seq) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(sequences) == 0 { | ||||||
|  | 		fmt.Println("Failed to create any timestamps") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create the timestamp file | ||||||
|  | 	file := &ots.File{ | ||||||
|  | 		Digest:    digest[:], | ||||||
|  | 		Sequences: sequences, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Save the OTS file | ||||||
|  | 	otsData := file.SerializeToFile() | ||||||
|  | 	if err := os.WriteFile("document.txt.ots", otsData, 0644); err != nil { | ||||||
|  | 		fmt.Println("Failed to save OTS file:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fmt.Println("Timestamp file created successfully") | ||||||
|  |  | ||||||
|  | 	// Display initial timestamp info | ||||||
|  | 	fmt.Println("\nInitial timestamp info:") | ||||||
|  | 	fmt.Println(file.Human(false)) | ||||||
|  |  | ||||||
|  | 	// Attempt to upgrade the timestamp every 20 minutes | ||||||
|  | 	fmt.Println("\nWill check for upgrades every 20 minutes...") | ||||||
|  |  | ||||||
|  | 	maxAttempts := 12 // Try for about 4 hours (12 * 20 minutes) | ||||||
|  | 	for attempt := 0; attempt < maxAttempts; attempt++ { | ||||||
|  | 		if attempt > 0 { | ||||||
|  | 			fmt.Printf("\nWaiting 20 minutes before next upgrade attempt (%d/%d)...\n", attempt+1, maxAttempts) | ||||||
|  | 			time.Sleep(20 * time.Minute) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		upgraded := false | ||||||
|  | 		pendingSequences := file.GetPendingSequences() | ||||||
|  | 		if len(pendingSequences) == 0 { | ||||||
|  | 			fmt.Println("No pending sequences to upgrade") | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		fmt.Printf("Attempting to upgrade %d pending sequences...\n", len(pendingSequences)) | ||||||
|  |  | ||||||
|  | 		upgradeCtx, upgradeCancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
|  | 		for _, seq := range pendingSequences { | ||||||
|  | 			att := seq.GetAttestation() | ||||||
|  | 			fmt.Printf("Trying to upgrade sequence from %s...\n", att.CalendarServerURL) | ||||||
|  |  | ||||||
|  | 			upgradedSeq, err := opentimestamps.UpgradeSequence(upgradeCtx, seq, digest[:]) | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Printf("Failed to upgrade sequence from %s: %v\n", att.CalendarServerURL, err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Replace the sequence in the file | ||||||
|  | 			for i, origSeq := range file.Sequences { | ||||||
|  | 				origAtt := origSeq.GetAttestation() | ||||||
|  | 				if origAtt.CalendarServerURL == att.CalendarServerURL { | ||||||
|  | 					file.Sequences[i] = upgradedSeq | ||||||
|  | 					upgraded = true | ||||||
|  |  | ||||||
|  | 					newAtt := upgradedSeq.GetAttestation() | ||||||
|  | 					if newAtt.BitcoinBlockHeight > 0 { | ||||||
|  | 						fmt.Printf("Sequence upgraded! Confirmed in Bitcoin block %d\n", newAtt.BitcoinBlockHeight) | ||||||
|  | 					} else { | ||||||
|  | 						fmt.Println("Sequence updated but still pending") | ||||||
|  | 					} | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		upgradeCancel() | ||||||
|  |  | ||||||
|  | 		if upgraded { | ||||||
|  | 			// Save the upgraded file | ||||||
|  | 			otsData = file.SerializeToFile() | ||||||
|  | 			if err := os.WriteFile("document.txt.ots", otsData, 0644); err != nil { | ||||||
|  | 				fmt.Println("Failed to save upgraded OTS file:", err) | ||||||
|  | 			} else { | ||||||
|  | 				fmt.Println("Upgraded timestamp file saved") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If all sequences are confirmed, we're done | ||||||
|  | 		if len(file.GetPendingSequences()) == 0 { | ||||||
|  | 			fmt.Println("All sequences are now confirmed in the Bitcoin blockchain!") | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Final report | ||||||
|  | 	fmt.Println("\nFinal timestamp status:") | ||||||
|  |  | ||||||
|  | 	confirmedSeqs := file.GetBitcoinAttestedSequences() | ||||||
|  | 	pendingSeqs := file.GetPendingSequences() | ||||||
|  |  | ||||||
|  | 	fmt.Printf("Confirmed attestations: %d\n", len(confirmedSeqs)) | ||||||
|  | 	for _, seq := range confirmedSeqs { | ||||||
|  | 		att := seq.GetAttestation() | ||||||
|  | 		fmt.Printf("- Confirmed in Bitcoin block %d\n", att.BitcoinBlockHeight) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fmt.Printf("Pending attestations: %d\n", len(pendingSeqs)) | ||||||
|  | 	for _, seq := range pendingSeqs { | ||||||
|  | 		att := seq.GetAttestation() | ||||||
|  | 		fmt.Printf("- Still pending at %s\n", att.CalendarServerURL) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fmt.Println("\nDetailed timestamp info:") | ||||||
|  | 	fmt.Println(file.Human(true)) | ||||||
|  | } | ||||||
							
								
								
									
										130
									
								
								cmd/ots/create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								cmd/ots/create.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/ots" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Default calendar servers | ||||||
|  | var defaultCalendars = []string{ | ||||||
|  | 	"https://alice.btc.calendar.opentimestamps.org", | ||||||
|  | 	"https://bob.btc.calendar.opentimestamps.org", | ||||||
|  | 	"https://finney.calendar.eternitywall.com", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// Create command flags | ||||||
|  | 	createOutput       string | ||||||
|  | 	createCalendarsStr string | ||||||
|  | 	createTimeout      time.Duration | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // createCmd represents the create command | ||||||
|  | var createCmd = &cobra.Command{ | ||||||
|  | 	Use:   "create [flags] <file>", | ||||||
|  | 	Short: "Create a timestamp for a file", | ||||||
|  | 	Long: `Create a timestamp for a file by submitting its digest to OpenTimestamps calendar servers. | ||||||
|  | The resulting timestamp is saved to a .ots file, which can later be verified or upgraded.`, | ||||||
|  | 	Args: cobra.ExactArgs(1), | ||||||
|  | 	Run:  runCreateCmd, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// Local flags for the create command | ||||||
|  | 	createCmd.Flags().StringVarP(&createOutput, "output", "o", "", "Output filename (default: input filename with .ots extension)") | ||||||
|  | 	createCmd.Flags().StringVar(&createCalendarsStr, "calendar", strings.Join(defaultCalendars, ","), "Comma-separated list of calendar server URLs") | ||||||
|  | 	createCmd.Flags().DurationVar(&createTimeout, "timeout", 30*time.Second, "Timeout for calendar server connections") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func runCreateCmd(cmd *cobra.Command, args []string) { | ||||||
|  | 	inputPath := args[0] | ||||||
|  |  | ||||||
|  | 	// Determine output file path | ||||||
|  | 	outputPath := createOutput | ||||||
|  | 	if outputPath == "" { | ||||||
|  | 		outputPath = inputPath + ".ots" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Parse calendar servers | ||||||
|  | 	calendars := strings.Split(createCalendarsStr, ",") | ||||||
|  | 	if len(calendars) == 0 { | ||||||
|  | 		calendars = defaultCalendars | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Read the input file | ||||||
|  | 	fileData, err := os.ReadFile(inputPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read input file", "file", inputPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Compute the file digest | ||||||
|  | 	digest := sha256.Sum256(fileData) | ||||||
|  | 	slog.Info("Computed file digest", "digest", fmt.Sprintf("%x", digest)) | ||||||
|  |  | ||||||
|  | 	// Create context with timeout | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), createTimeout) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	// Create timestamps using all calendar servers | ||||||
|  | 	var sequences []ots.Sequence | ||||||
|  | 	var errors []string | ||||||
|  |  | ||||||
|  | 	for _, calendarURL := range calendars { | ||||||
|  | 		calendarURL = strings.TrimSpace(calendarURL) | ||||||
|  | 		if calendarURL == "" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		slog.Info("Submitting to calendar", "url", calendarURL) | ||||||
|  | 		seq, err := opentimestamps.Stamp(ctx, calendarURL, digest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Warn("Calendar submission failed", "url", calendarURL, "error", err) | ||||||
|  | 			errors = append(errors, fmt.Sprintf("%s: %v", calendarURL, err)) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		sequences = append(sequences, seq) | ||||||
|  | 		slog.Info("Calendar submission successful", "url", calendarURL) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(sequences) == 0 { | ||||||
|  | 		slog.Error("All calendar submissions failed", "errors", errors) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create the timestamp file | ||||||
|  | 	file := &ots.File{ | ||||||
|  | 		Digest:    digest[:], | ||||||
|  | 		Sequences: sequences, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Write the OTS file | ||||||
|  | 	otsData := file.SerializeToFile() | ||||||
|  | 	err = os.WriteFile(outputPath, otsData, 0644) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to write OTS file", "file", outputPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	slog.Info("Timestamp file created successfully", | ||||||
|  | 		"file", outputPath, | ||||||
|  | 		"timestamps", len(sequences), | ||||||
|  | 		"size", len(otsData)) | ||||||
|  |  | ||||||
|  | 	// Print human-readable representation | ||||||
|  | 	slog.Debug("Timestamp details", "info", file.Human(false)) | ||||||
|  |  | ||||||
|  | 	slog.Info("Timestamp creation complete", | ||||||
|  | 		"status", "pending", | ||||||
|  | 		"note", "Use 'ots upgrade' to upgrade this timestamp when it's confirmed in the Bitcoin blockchain") | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								cmd/ots/info.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								cmd/ots/info.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/ots" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // infoCmd represents the info command | ||||||
|  | var infoCmd = &cobra.Command{ | ||||||
|  | 	Use:   "info [flags] <file.ots>", | ||||||
|  | 	Short: "Display information about a timestamp", | ||||||
|  | 	Long: `Display detailed information about a timestamp file in human-readable format. | ||||||
|  | Shows the file digest and the sequence of operations and attestations.`, | ||||||
|  | 	Args: cobra.ExactArgs(1), | ||||||
|  | 	Run:  runInfoCmd, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// No specific flags needed for info command | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func runInfoCmd(cmd *cobra.Command, args []string) { | ||||||
|  | 	otsPath := args[0] | ||||||
|  |  | ||||||
|  | 	// Read and parse the OTS file | ||||||
|  | 	otsData, err := os.ReadFile(otsPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read OTS file", "file", otsPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	timestampFile, err := opentimestamps.ReadFromFile(otsData) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to parse OTS file", "file", otsPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Print the timestamp information in the requested format | ||||||
|  | 	fmt.Printf("File sha256 hash: %s\n", hex.EncodeToString(timestampFile.Digest)) | ||||||
|  | 	fmt.Println("Timestamp:") | ||||||
|  |  | ||||||
|  | 	// Format and print each sequence | ||||||
|  | 	for i, seq := range timestampFile.Sequences { | ||||||
|  | 		if i > 0 { | ||||||
|  | 			// Add a separator between sequences if needed | ||||||
|  | 			fmt.Println() | ||||||
|  | 		} | ||||||
|  | 		printSequenceInfo(seq, 0, timestampFile.Digest) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func printSequenceInfo(seq ots.Sequence, depth int, initialDigest []byte) { | ||||||
|  | 	prefix := strings.Repeat(" ", depth) | ||||||
|  |  | ||||||
|  | 	// Track the current result as we apply operations | ||||||
|  | 	current := initialDigest | ||||||
|  |  | ||||||
|  | 	// For the first level (depth 0), don't add the arrow | ||||||
|  | 	arrowPrefix := "" | ||||||
|  | 	if depth > 0 { | ||||||
|  | 		arrowPrefix = " -> " | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, inst := range seq { | ||||||
|  | 		// Skip attestation for now, we'll handle it at the end | ||||||
|  | 		if inst.Attestation != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Print operation | ||||||
|  | 		if inst.Operation != nil { | ||||||
|  | 			line := fmt.Sprintf("%s%s%s", prefix, arrowPrefix, inst.Operation.Name) | ||||||
|  | 			if inst.Operation.Binary { | ||||||
|  | 				line += fmt.Sprintf(" %s", hex.EncodeToString(inst.Argument)) | ||||||
|  | 			} | ||||||
|  | 			fmt.Println(line) | ||||||
|  |  | ||||||
|  | 			// Update current result | ||||||
|  | 			current = inst.Operation.Apply(current, inst.Argument) | ||||||
|  |  | ||||||
|  | 			// Only show arrow prefix for the first line | ||||||
|  | 			arrowPrefix = "" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if there's an attestation at the end | ||||||
|  | 	if len(seq) > 0 && seq[len(seq)-1].Attestation != nil { | ||||||
|  | 		att := seq[len(seq)-1].Attestation | ||||||
|  | 		var attLine string | ||||||
|  |  | ||||||
|  | 		if att.BitcoinBlockHeight > 0 { | ||||||
|  | 			attLine = fmt.Sprintf("verify BitcoinAttestation(block %d)", att.BitcoinBlockHeight) | ||||||
|  | 		} else if att.CalendarServerURL != "" { | ||||||
|  | 			attLine = fmt.Sprintf("verify PendingAttestation('%s')", att.CalendarServerURL) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if attLine != "" { | ||||||
|  | 			fmt.Printf("%s%s\n", prefix, attLine) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								cmd/ots/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								cmd/ots/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	if err := rootCmd.Execute(); err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										374
									
								
								cmd/ots/pdf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								cmd/ots/pdf.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,374 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/ots" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/verifyer" | ||||||
|  | 	"github.com/jung-kurt/gofpdf" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// PDF command flags | ||||||
|  | 	pdfOutput         string | ||||||
|  | 	pdfIncludeContent bool | ||||||
|  | 	pdfTitle          string | ||||||
|  | 	pdfComment        string | ||||||
|  | 	pdfEsploraURL     string | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // pdfCmd represents the pdf command | ||||||
|  | var pdfCmd = &cobra.Command{ | ||||||
|  | 	Use:   "pdf [flags] <file> <file.ots>", | ||||||
|  | 	Short: "Generate a PDF certificate for a timestamp", | ||||||
|  | 	Long: `Generate a PDF certificate that explains how to verify the timestamp. | ||||||
|  | The PDF includes all necessary information such as hashes, operations, | ||||||
|  | and instructions for manual verification.`, | ||||||
|  | 	Args: cobra.ExactArgs(2), | ||||||
|  | 	Run:  runPdfCmd, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// Local flags for the pdf command | ||||||
|  | 	pdfCmd.Flags().StringVarP(&pdfOutput, "output", "o", "", "Output PDF filename (default: original filename with .pdf extension)") | ||||||
|  | 	pdfCmd.Flags().BoolVar(&pdfIncludeContent, "include-content", false, "Include the original file content in the PDF (for text files only)") | ||||||
|  | 	pdfCmd.Flags().StringVar(&pdfTitle, "title", "OpenTimestamps Certificate", "Title for the PDF document") | ||||||
|  | 	pdfCmd.Flags().StringVar(&pdfComment, "comment", "", "Additional comment to include in the certificate") | ||||||
|  | 	pdfCmd.Flags().StringVar(&pdfEsploraURL, "esplora", "https://blockstream.info/api", "URL of Esplora API for fetching block information") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func runPdfCmd(cmd *cobra.Command, args []string) { | ||||||
|  | 	filePath := args[0] | ||||||
|  | 	otsPath := args[1] | ||||||
|  |  | ||||||
|  | 	// Determine output file path | ||||||
|  | 	outputPath := pdfOutput | ||||||
|  | 	if outputPath == "" { | ||||||
|  | 		outputPath = filePath + ".certificate.pdf" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Read the original file | ||||||
|  | 	fileData, err := os.ReadFile(filePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read file", "file", filePath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Compute the file digest | ||||||
|  | 	fileDigest := sha256.Sum256(fileData) | ||||||
|  | 	slog.Debug("Computed file digest", "digest", hex.EncodeToString(fileDigest[:])) | ||||||
|  |  | ||||||
|  | 	// Read and parse the OTS file | ||||||
|  | 	otsData, err := os.ReadFile(otsPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read OTS file", "file", otsPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	timestampFile, err := opentimestamps.ReadFromFile(otsData) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to parse OTS file", "file", otsPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if the digests match | ||||||
|  | 	if !bytes.Equal(fileDigest[:], timestampFile.Digest) { | ||||||
|  | 		slog.Warn("Warning: File digest doesn't match timestamp digest", | ||||||
|  | 			"file_digest", hex.EncodeToString(fileDigest[:]), | ||||||
|  | 			"timestamp_digest", hex.EncodeToString(timestampFile.Digest)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Extract Bitcoin attestations | ||||||
|  | 	bitcoinSeqs := timestampFile.GetBitcoinAttestedSequences() | ||||||
|  | 	if len(bitcoinSeqs) == 0 { | ||||||
|  | 		slog.Warn("No Bitcoin attestations found in timestamp, certificate will be for pending timestamps") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create Bitcoin interface for block time lookup | ||||||
|  | 	bitcoin := verifyer.NewEsploraClient(pdfEsploraURL, 30*time.Second) | ||||||
|  | 	slog.Info("Using Esplora API for block information", "url", pdfEsploraURL) | ||||||
|  |  | ||||||
|  | 	// Generate the PDF | ||||||
|  | 	err = generateVerificationPDF( | ||||||
|  | 		outputPath, | ||||||
|  | 		filePath, | ||||||
|  | 		fileData, | ||||||
|  | 		fileDigest[:], | ||||||
|  | 		timestampFile, | ||||||
|  | 		bitcoinSeqs, | ||||||
|  | 		bitcoin, | ||||||
|  | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to generate PDF", "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	slog.Info("PDF certificate generated successfully", "file", outputPath) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func generateVerificationPDF(outputPath, filePath string, fileData []byte, fileDigest []byte, | ||||||
|  | 	timestamp *ots.File, bitcoinSeqs []ots.Sequence, bitcoin verifyer.Bitcoin) error { | ||||||
|  |  | ||||||
|  | 	// Create a new PDF | ||||||
|  | 	pdf := gofpdf.New("P", "mm", "A4", "") | ||||||
|  | 	pdf.SetTitle(pdfTitle, true) | ||||||
|  | 	pdf.SetAuthor("OpenTimestamps", true) | ||||||
|  | 	pdf.SetCreator("OpenTimestamps CLI", true) | ||||||
|  |  | ||||||
|  | 	// Set margins | ||||||
|  | 	pdf.SetMargins(25, 25, 25) | ||||||
|  | 	pdf.SetAutoPageBreak(true, 25) | ||||||
|  |  | ||||||
|  | 	// Add first page - Certificate | ||||||
|  | 	pdf.AddPage() | ||||||
|  |  | ||||||
|  | 	// Title | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 24) | ||||||
|  | 	pdf.CellFormat(0, 12, pdfTitle, "", 1, "C", false, 0, "") | ||||||
|  | 	pdf.Ln(10) | ||||||
|  |  | ||||||
|  | 	// Timestamp information section | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 18) | ||||||
|  | 	pdf.CellFormat(0, 10, "Timestamp Information", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 	pdf.CellFormat(0, 8, "Filename:", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 	pdf.CellFormat(0, 8, filepath.Base(filePath), "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.Ln(2) | ||||||
|  |  | ||||||
|  | 	// Timestamp date | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 	if len(bitcoinSeqs) > 0 { | ||||||
|  | 		// Find the earliest Bitcoin attestation | ||||||
|  | 		var earliestHeight uint64 | ||||||
|  | 		var earliestBlockTime time.Time | ||||||
|  |  | ||||||
|  | 		for _, seq := range bitcoinSeqs { | ||||||
|  | 			att := seq.GetAttestation() | ||||||
|  | 			if earliestHeight == 0 || att.BitcoinBlockHeight < earliestHeight { | ||||||
|  | 				earliestHeight = att.BitcoinBlockHeight | ||||||
|  |  | ||||||
|  | 				// Fetch the block time from the blockchain | ||||||
|  | 				blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) | ||||||
|  | 				if err == nil { | ||||||
|  | 					blockHeader, err := bitcoin.GetBlockHeader(blockHash) | ||||||
|  | 					if err == nil { | ||||||
|  | 						earliestBlockTime = time.Unix(int64(blockHeader.Timestamp.Unix()), 0) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Show Bitcoin timestamp | ||||||
|  | 		pdf.CellFormat(0, 8, "Timestamp Created:", "", 1, "L", false, 0, "") | ||||||
|  | 		pdf.SetFont("Helvetica", "", 12) | ||||||
|  |  | ||||||
|  | 		if !earliestBlockTime.IsZero() { | ||||||
|  | 			// If we successfully retrieved the block time, display it | ||||||
|  | 			pdf.CellFormat(0, 8, fmt.Sprintf("Bitcoin Block #%d (%s)", | ||||||
|  | 				earliestHeight, earliestBlockTime.Format("Jan 02, 2006 15:04:05 UTC")), | ||||||
|  | 				"", 1, "L", false, 0, "") | ||||||
|  | 		} else { | ||||||
|  | 			// Fallback to just showing the block height | ||||||
|  | 			pdf.CellFormat(0, 8, fmt.Sprintf("Bitcoin Block #%d", earliestHeight), "", 1, "L", false, 0, "") | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// Pending attestation | ||||||
|  | 		pdf.CellFormat(0, 8, "Status:", "", 1, "L", false, 0, "") | ||||||
|  | 		pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 		pdf.CellFormat(0, 8, "Pending confirmation in the Bitcoin blockchain", "", 1, "L", false, 0, "") | ||||||
|  | 	} | ||||||
|  | 	pdf.Ln(2) | ||||||
|  |  | ||||||
|  | 	// File hash | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 	pdf.CellFormat(0, 8, "File SHA-256 Digest:", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.SetFont("Courier", "", 10) | ||||||
|  | 	pdf.CellFormat(0, 8, hex.EncodeToString(fileDigest), "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 	// Include comment if provided | ||||||
|  | 	if pdfComment != "" { | ||||||
|  | 		pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 		pdf.CellFormat(0, 8, "Comment:", "", 1, "L", false, 0, "") | ||||||
|  | 		pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 		pdf.MultiCell(0, 8, pdfComment, "", "L", false) | ||||||
|  | 		pdf.Ln(5) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Include file content if requested and looks like text | ||||||
|  | 	if pdfIncludeContent && isTextFile(fileData) && len(fileData) < 5000 { | ||||||
|  | 		pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 		pdf.CellFormat(0, 8, "File Content:", "", 1, "L", false, 0, "") | ||||||
|  | 		pdf.SetFont("Courier", "", 10) | ||||||
|  | 		content := string(fileData) | ||||||
|  | 		content = strings.ReplaceAll(content, "\r\n", "\n") | ||||||
|  | 		pdf.MultiCell(0, 5, content, "", "L", false) | ||||||
|  | 		pdf.Ln(5) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add Bitcoin transaction information for each attestation | ||||||
|  | 	if len(bitcoinSeqs) > 0 { | ||||||
|  | 		pdf.SetFont("Helvetica", "B", 18) | ||||||
|  | 		pdf.CellFormat(0, 10, "Bitcoin Attestations", "", 1, "L", false, 0, "") | ||||||
|  | 		pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 		for i, seq := range bitcoinSeqs { | ||||||
|  | 			att := seq.GetAttestation() | ||||||
|  |  | ||||||
|  | 			pdf.SetFont("Helvetica", "B", 14) | ||||||
|  | 			pdf.CellFormat(0, 8, fmt.Sprintf("Attestation #%d", i+1), "", 1, "L", false, 0, "") | ||||||
|  | 			pdf.Ln(2) | ||||||
|  |  | ||||||
|  | 			// Get block details | ||||||
|  | 			blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) | ||||||
|  | 			blockTime := "Unknown" | ||||||
|  | 			if err == nil { | ||||||
|  | 				blockHeader, err := bitcoin.GetBlockHeader(blockHash) | ||||||
|  | 				if err == nil { | ||||||
|  | 					blockTime = blockHeader.Timestamp.Format("Jan 02, 2006 15:04:05 UTC") | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 			pdf.CellFormat(0, 8, "Bitcoin Block Height:", "", 1, "L", false, 0, "") | ||||||
|  | 			pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 			pdf.CellFormat(0, 8, fmt.Sprintf("%d", att.BitcoinBlockHeight), "", 1, "L", false, 0, "") | ||||||
|  |  | ||||||
|  | 			pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 			pdf.CellFormat(0, 8, "Block Time:", "", 1, "L", false, 0, "") | ||||||
|  | 			pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 			pdf.CellFormat(0, 8, blockTime, "", 1, "L", false, 0, "") | ||||||
|  |  | ||||||
|  | 			pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 			pdf.CellFormat(0, 8, "Block Hash:", "", 1, "L", false, 0, "") | ||||||
|  | 			pdf.SetFont("Courier", "", 10) | ||||||
|  | 			if blockHash != nil { | ||||||
|  | 				pdf.CellFormat(0, 8, blockHash.String(), "", 1, "L", false, 0, "") | ||||||
|  | 			} else { | ||||||
|  | 				pdf.CellFormat(0, 8, "Unable to retrieve", "", 1, "L", false, 0, "") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// In a full implementation, we would show the Bitcoin merkle root and transaction ID | ||||||
|  | 			// This would require additional API calls to get that information | ||||||
|  |  | ||||||
|  | 			// Show operations sequence | ||||||
|  | 			pdf.SetFont("Helvetica", "B", 12) | ||||||
|  | 			pdf.CellFormat(0, 8, "Verification Operations:", "", 1, "L", false, 0, "") | ||||||
|  | 			pdf.SetFont("Courier", "", 10) | ||||||
|  |  | ||||||
|  | 			// Format the operations | ||||||
|  | 			var operationsText string | ||||||
|  | 			merkleRoot, _ := seq.Compute(timestamp.Digest) | ||||||
|  |  | ||||||
|  | 			operationsText += fmt.Sprintf("Starting with digest: %s\n", hex.EncodeToString(timestamp.Digest)) | ||||||
|  |  | ||||||
|  | 			for j, inst := range seq { | ||||||
|  | 				if inst.Operation != nil { | ||||||
|  | 					if inst.Operation.Binary { | ||||||
|  | 						operationsText += fmt.Sprintf("%d. %s %s\n", j+1, inst.Operation.Name, hex.EncodeToString(inst.Argument)) | ||||||
|  | 					} else { | ||||||
|  | 						operationsText += fmt.Sprintf("%d. %s\n", j+1, inst.Operation.Name) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			operationsText += fmt.Sprintf("\nResulting merkle root: %s\n", hex.EncodeToString(merkleRoot)) | ||||||
|  |  | ||||||
|  | 			pdf.MultiCell(0, 5, operationsText, "", "L", false) | ||||||
|  | 			pdf.Ln(5) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add second page - Verification Instructions | ||||||
|  | 	pdf.AddPage() | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 18) | ||||||
|  | 	pdf.CellFormat(0, 10, "Verification Instructions", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 	// Instructions | ||||||
|  | 	addVerificationInstructions(pdf) | ||||||
|  |  | ||||||
|  | 	// Save the PDF | ||||||
|  | 	return pdf.OutputFileAndClose(outputPath) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func addVerificationInstructions(pdf *gofpdf.Fpdf) { | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 14) | ||||||
|  | 	pdf.CellFormat(0, 8, "1. Compute the SHA-256 digest of your file", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 	pdf.MultiCell(0, 6, `To verify this timestamp, first compute the SHA-256 digest of your original file. You can use various tools for this: | ||||||
|  |  | ||||||
|  | - On Linux/macOS: Use the command "shasum -a 256 filename" | ||||||
|  | - On Windows: Use tools like PowerShell's "Get-FileHash" or third-party tools | ||||||
|  | - Online: Various websites can calculate file hashes, but only use trusted sources for sensitive files | ||||||
|  |  | ||||||
|  | Verify that the digest matches the one shown in this certificate.`, "", "L", false) | ||||||
|  | 	pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 14) | ||||||
|  | 	pdf.CellFormat(0, 8, "2. Verify the operations sequence", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 	pdf.MultiCell(0, 6, `The operations sequence shown in this certificate transforms the file digest into a value that was stored in the Bitcoin blockchain. To verify manually: | ||||||
|  |  | ||||||
|  | - Start with the file's SHA-256 digest | ||||||
|  | - Apply each operation in sequence as shown in the certificate | ||||||
|  | - For "append" operations, add the shown bytes to the end of the current value | ||||||
|  | - For "prepend" operations, add the shown bytes to the beginning of the current value | ||||||
|  | - For "sha256" operations, compute the SHA-256 hash of the current value | ||||||
|  | - The final result should match the merkle root shown in the certificate`, "", "L", false) | ||||||
|  | 	pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 14) | ||||||
|  | 	pdf.CellFormat(0, 8, "3. Verify the Bitcoin attestation", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 	pdf.MultiCell(0, 6, `For full verification, check that the merkle root was recorded in the Bitcoin blockchain: | ||||||
|  |  | ||||||
|  | - Find the Bitcoin block at the height shown in the certificate | ||||||
|  | - The block's merkle root or transaction should contain the computed value | ||||||
|  | - You can use block explorers like blockstream.info or blockchain.com | ||||||
|  | - The timestamp is confirmed when the merkle root appears in the blockchain`, "", "L", false) | ||||||
|  | 	pdf.Ln(5) | ||||||
|  |  | ||||||
|  | 	pdf.SetFont("Helvetica", "B", 14) | ||||||
|  | 	pdf.CellFormat(0, 8, "4. Use the OpenTimestamps verifier", "", 1, "L", false, 0, "") | ||||||
|  | 	pdf.SetFont("Helvetica", "", 12) | ||||||
|  | 	pdf.MultiCell(0, 6, `For easier verification, use the OpenTimestamps verification tools: | ||||||
|  |  | ||||||
|  | - Command line: Use "ots verify <file> <file.ots>" | ||||||
|  | - Online: Visit https://opentimestamps.org and upload your file and .ots timestamp | ||||||
|  | - The tools will perform all verification steps automatically`, "", "L", false) | ||||||
|  |  | ||||||
|  | 	// Add footer with generation time | ||||||
|  | 	pdf.Ln(10) | ||||||
|  | 	pdf.SetFont("Helvetica", "I", 10) | ||||||
|  | 	pdf.CellFormat(0, 6, fmt.Sprintf("Certificate generated at: %s", time.Now().Format(time.RFC3339)), "", 1, "C", false, 0, "") | ||||||
|  | 	pdf.CellFormat(0, 6, "OpenTimestamps - https://opentimestamps.org", "", 1, "C", false, 0, "") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // isTextFile tries to determine if a file is likely to be text | ||||||
|  | func isTextFile(data []byte) bool { | ||||||
|  | 	// Simple heuristic: if the file doesn't contain too many non-printable characters, | ||||||
|  | 	// it's probably text | ||||||
|  | 	nonPrintable := 0 | ||||||
|  | 	for _, b := range data { | ||||||
|  | 		if (b < 32 || b > 126) && b != 9 && b != 10 && b != 13 { | ||||||
|  | 			nonPrintable++ | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Allow up to 1% non-printable characters | ||||||
|  | 	return nonPrintable <= len(data)/100 | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								cmd/ots/root.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cmd/ots/root.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// Global flags | ||||||
|  | 	jsonLog  bool | ||||||
|  | 	logLevel string | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // rootCmd represents the base command when called without any subcommands | ||||||
|  | var rootCmd = &cobra.Command{ | ||||||
|  | 	Use:   "ots", | ||||||
|  | 	Short: "OpenTimestamps CLI tool", | ||||||
|  | 	Long: `A command-line interface for OpenTimestamps operations. | ||||||
|  | It allows creating, verifying and upgrading timestamps for files.`, | ||||||
|  | 	PersistentPreRun: setupLogging, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// Global flags for all commands | ||||||
|  | 	rootCmd.PersistentFlags().BoolVar(&jsonLog, "json", false, "Use JSON format for logging") | ||||||
|  | 	rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Log level: debug, info, warn, error") | ||||||
|  |  | ||||||
|  | 	// Add subcommands | ||||||
|  | 	rootCmd.AddCommand(createCmd) | ||||||
|  | 	rootCmd.AddCommand(verifyCmd) | ||||||
|  | 	rootCmd.AddCommand(upgradeCmd) | ||||||
|  | 	rootCmd.AddCommand(infoCmd) | ||||||
|  | 	rootCmd.AddCommand(pdfCmd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // setupLogging configures the global logger based on the flags | ||||||
|  | func setupLogging(cmd *cobra.Command, args []string) { | ||||||
|  | 	loggerOptions := slog.HandlerOptions{} | ||||||
|  |  | ||||||
|  | 	switch strings.ToLower(logLevel) { | ||||||
|  | 	case "debug": | ||||||
|  | 		loggerOptions.Level = slog.LevelDebug | ||||||
|  | 	case "info": | ||||||
|  | 		loggerOptions.Level = slog.LevelInfo | ||||||
|  | 	case "warn": | ||||||
|  | 		loggerOptions.Level = slog.LevelWarn | ||||||
|  | 	case "error": | ||||||
|  | 		loggerOptions.Level = slog.LevelError | ||||||
|  | 	default: | ||||||
|  | 		loggerOptions.Level = slog.LevelInfo | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var logHandler slog.Handler | ||||||
|  | 	if jsonLog { | ||||||
|  | 		logHandler = slog.NewJSONHandler(os.Stdout, &loggerOptions) | ||||||
|  | 	} else { | ||||||
|  | 		logHandler = slog.NewTextHandler(os.Stdout, &loggerOptions) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger := slog.New(logHandler) | ||||||
|  | 	slog.SetDefault(logger) | ||||||
|  | } | ||||||
							
								
								
									
										138
									
								
								cmd/ots/upgrade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								cmd/ots/upgrade.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// Upgrade command flags | ||||||
|  | 	upgradeOutput  string | ||||||
|  | 	upgradeTimeout time.Duration | ||||||
|  | 	upgradeDryRun  bool | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // upgradeCmd represents the upgrade command | ||||||
|  | var upgradeCmd = &cobra.Command{ | ||||||
|  | 	Use:   "upgrade [flags] <file.ots>", | ||||||
|  | 	Short: "Upgrade a timestamp", | ||||||
|  | 	Long: `Upgrade a timestamp by checking if pending attestations have been confirmed in the Bitcoin blockchain. | ||||||
|  | If confirmed, the timestamp will be updated with the Bitcoin block information.`, | ||||||
|  | 	Args: cobra.ExactArgs(1), | ||||||
|  | 	Run:  runUpgradeCmd, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// Local flags for the upgrade command | ||||||
|  | 	upgradeCmd.Flags().StringVarP(&upgradeOutput, "output", "o", "", "Output filename (default: overwrites the input file)") | ||||||
|  | 	upgradeCmd.Flags().DurationVar(&upgradeTimeout, "timeout", 30*time.Second, "Timeout for calendar server connections") | ||||||
|  | 	upgradeCmd.Flags().BoolVar(&upgradeDryRun, "dry-run", false, "Don't write output file, just check if upgrade is possible") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func runUpgradeCmd(cmd *cobra.Command, args []string) { | ||||||
|  | 	inputPath := args[0] | ||||||
|  |  | ||||||
|  | 	// Determine output file path | ||||||
|  | 	outputPath := upgradeOutput | ||||||
|  | 	if outputPath == "" { | ||||||
|  | 		outputPath = inputPath | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Read and parse the OTS file | ||||||
|  | 	otsData, err := os.ReadFile(inputPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read OTS file", "file", inputPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	timestampFile, err := opentimestamps.ReadFromFile(otsData) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to parse OTS file", "file", inputPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get pending sequences to upgrade | ||||||
|  | 	pendingSequences := timestampFile.GetPendingSequences() | ||||||
|  | 	if len(pendingSequences) == 0 { | ||||||
|  | 		slog.Info("No pending timestamps found, file is already fully upgraded", "file", inputPath) | ||||||
|  | 		os.Exit(0) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	slog.Info("Found pending timestamps", "count", len(pendingSequences)) | ||||||
|  |  | ||||||
|  | 	// Create context with timeout | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), upgradeTimeout) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	// Try to upgrade each pending sequence | ||||||
|  | 	upgradedCount := 0 | ||||||
|  | 	for i, seq := range pendingSequences { | ||||||
|  | 		att := seq.GetAttestation() | ||||||
|  | 		slog.Info("Attempting to upgrade timestamp", | ||||||
|  | 			"index", i+1, | ||||||
|  | 			"calendar", att.CalendarServerURL) | ||||||
|  |  | ||||||
|  | 		upgraded, err := opentimestamps.UpgradeSequence(ctx, seq, timestampFile.Digest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Warn("Upgrade failed", | ||||||
|  | 				"calendar", att.CalendarServerURL, | ||||||
|  | 				"error", err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Replace the pending sequence with the upgraded one | ||||||
|  | 		for j, origSeq := range timestampFile.Sequences { | ||||||
|  | 			if origSeq[len(origSeq)-1].Attestation != nil && | ||||||
|  | 				origSeq[len(origSeq)-1].Attestation.CalendarServerURL == att.CalendarServerURL { | ||||||
|  | 				timestampFile.Sequences[j] = upgraded | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		upgradedCount++ | ||||||
|  |  | ||||||
|  | 		newAtt := upgraded.GetAttestation() | ||||||
|  | 		if newAtt.BitcoinBlockHeight > 0 { | ||||||
|  | 			slog.Info("Timestamp upgraded successfully", | ||||||
|  | 				"calendar", att.CalendarServerURL, | ||||||
|  | 				"block", newAtt.BitcoinBlockHeight) | ||||||
|  | 		} else { | ||||||
|  | 			slog.Info("Timestamp replaced but still pending", "calendar", att.CalendarServerURL) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if upgradedCount == 0 { | ||||||
|  | 		slog.Warn("No timestamps could be upgraded at this time. Try again later.", "file", inputPath) | ||||||
|  | 		if !upgradeDryRun { | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		os.Exit(0) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// In dry run mode, don't write the file | ||||||
|  | 	if upgradeDryRun { | ||||||
|  | 		slog.Info("Dry run completed", "upgraded", upgradedCount, "total", len(pendingSequences)) | ||||||
|  | 		os.Exit(0) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Write the updated OTS file | ||||||
|  | 	newOtsData := timestampFile.SerializeToFile() | ||||||
|  | 	err = os.WriteFile(outputPath, newOtsData, 0644) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to write updated OTS file", "file", outputPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	slog.Info("Timestamp file upgraded successfully", | ||||||
|  | 		"file", outputPath, | ||||||
|  | 		"upgraded", upgradedCount, | ||||||
|  | 		"total", len(pendingSequences)) | ||||||
|  |  | ||||||
|  | 	// Print human-readable representation | ||||||
|  | 	slog.Debug("Updated timestamp details", "info", timestampFile.Human(false)) | ||||||
|  | } | ||||||
							
								
								
									
										170
									
								
								cmd/ots/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								cmd/ots/verify.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/verifyer" | ||||||
|  | 	"github.com/btcsuite/btcd/rpcclient" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// Verify command flags | ||||||
|  | 	verifyEsploraURL   string | ||||||
|  | 	verifyBitcoindHost string | ||||||
|  | 	verifyBitcoindUser string | ||||||
|  | 	verifyBitcoindPass string | ||||||
|  | 	verifyTimeout      time.Duration | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // verifyCmd represents the verify command | ||||||
|  | var verifyCmd = &cobra.Command{ | ||||||
|  | 	Use:   "verify [flags] <file> <file.ots>", | ||||||
|  | 	Short: "Verify a timestamp", | ||||||
|  | 	Long: `Verify a timestamp against the Bitcoin blockchain. | ||||||
|  | It computes the file digest, checks it against the timestamp, | ||||||
|  | and verifies the timestamp against Bitcoin using either Esplora API or bitcoind.`, | ||||||
|  | 	Args: cobra.ExactArgs(2), | ||||||
|  | 	Run:  runVerifyCmd, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// Local flags for the verify command | ||||||
|  | 	verifyCmd.Flags().StringVar(&verifyEsploraURL, "esplora", "https://blockstream.info/api", "URL of Esplora API") | ||||||
|  | 	verifyCmd.Flags().StringVar(&verifyBitcoindHost, "bitcoind", "", "Host:port of bitcoind RPC (e.g. localhost:8332)") | ||||||
|  | 	verifyCmd.Flags().StringVar(&verifyBitcoindUser, "rpcuser", "", "Bitcoin RPC username") | ||||||
|  | 	verifyCmd.Flags().StringVar(&verifyBitcoindPass, "rpcpass", "", "Bitcoin RPC password") | ||||||
|  | 	verifyCmd.Flags().DurationVar(&verifyTimeout, "timeout", 30*time.Second, "Connection timeout") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func runVerifyCmd(cmd *cobra.Command, args []string) { | ||||||
|  | 	filePath := args[0] | ||||||
|  | 	otsPath := args[1] | ||||||
|  |  | ||||||
|  | 	// Read the original file | ||||||
|  | 	fileData, err := os.ReadFile(filePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read file", "file", filePath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Compute the file digest | ||||||
|  | 	digest := sha256.Sum256(fileData) | ||||||
|  | 	slog.Debug("Computed file digest", "digest", digest) | ||||||
|  |  | ||||||
|  | 	// Read and parse the OTS file | ||||||
|  | 	otsData, err := os.ReadFile(otsPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to read OTS file", "file", otsPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	timestampFile, err := opentimestamps.ReadFromFile(otsData) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("Failed to parse OTS file", "file", otsPath, "error", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Setup Bitcoin connection | ||||||
|  | 	var bitcoin verifyer.Bitcoin | ||||||
|  | 	if verifyBitcoindHost != "" { | ||||||
|  | 		if verifyBitcoindUser == "" || verifyBitcoindPass == "" { | ||||||
|  | 			slog.Error("Bitcoind RPC credentials required with --bitcoind") | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Create bitcoind connection config | ||||||
|  | 		config := rpcclient.ConnConfig{ | ||||||
|  | 			Host:         verifyBitcoindHost, | ||||||
|  | 			User:         verifyBitcoindUser, | ||||||
|  | 			Pass:         verifyBitcoindPass, | ||||||
|  | 			HTTPPostMode: true, | ||||||
|  | 			DisableTLS:   true, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		btc, err := verifyer.NewBitcoindInterface(config) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Error("Failed to connect to bitcoind", "error", err) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		bitcoin = btc | ||||||
|  | 		slog.Info("Using bitcoind", "host", verifyBitcoindHost) | ||||||
|  | 	} else { | ||||||
|  | 		// Use Esplora API | ||||||
|  | 		bitcoin = verifyer.NewEsploraClient(verifyEsploraURL, verifyTimeout) | ||||||
|  | 		slog.Info("Using Esplora API", "url", verifyEsploraURL) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if digests match | ||||||
|  | 	if string(digest[:]) != string(timestampFile.Digest) { | ||||||
|  | 		slog.Warn("File digest doesn't match timestamp digest", | ||||||
|  | 			"file_digest", digest, | ||||||
|  | 			"timestamp_digest", timestampFile.Digest) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get Bitcoin-attested sequences | ||||||
|  | 	bitcoinSequences := timestampFile.GetBitcoinAttestedSequences() | ||||||
|  | 	if len(bitcoinSequences) == 0 { | ||||||
|  | 		// If no Bitcoin sequences, check if there are pending sequences | ||||||
|  | 		pendingSequences := timestampFile.GetPendingSequences() | ||||||
|  | 		if len(pendingSequences) > 0 { | ||||||
|  | 			slog.Info("Timestamp is pending confirmation in the Bitcoin blockchain") | ||||||
|  | 			slog.Info("Use 'ots upgrade <file.ots>' to try upgrading it") | ||||||
|  |  | ||||||
|  | 			for _, seq := range pendingSequences { | ||||||
|  | 				att := seq.GetAttestation() | ||||||
|  | 				slog.Info("Pending at calendar", "url", att.CalendarServerURL) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			slog.Error("No valid attestations found in this timestamp") | ||||||
|  | 		} | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Verify each Bitcoin-attested sequence | ||||||
|  | 	verificationSuccess := false | ||||||
|  | 	for i, seq := range bitcoinSequences { | ||||||
|  | 		slog.Info("Verifying sequence", "index", i+1, "total", len(bitcoinSequences)) | ||||||
|  |  | ||||||
|  | 		tx, err := seq.Verify(bitcoin, timestampFile.Digest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Warn("Verification failed", "error", err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		att := seq.GetAttestation() | ||||||
|  |  | ||||||
|  | 		// Get the block time | ||||||
|  | 		blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) | ||||||
|  | 		blockTime := "" | ||||||
|  | 		if err == nil { | ||||||
|  | 			blockHeader, err := bitcoin.GetBlockHeader(blockHash) | ||||||
|  | 			if err == nil { | ||||||
|  | 				// Format the block time | ||||||
|  | 				blockTime = fmt.Sprintf(" (%s)", blockHeader.Timestamp.Format(time.RFC3339)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		slog.Info("Verification successful", | ||||||
|  | 			"block_height", att.BitcoinBlockHeight, | ||||||
|  | 			"block_time", strings.TrimPrefix(blockTime, " ")) | ||||||
|  |  | ||||||
|  | 		if tx != nil { | ||||||
|  | 			slog.Info("Bitcoin transaction", "txid", tx.TxHash().String()) | ||||||
|  | 		} | ||||||
|  | 		verificationSuccess = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !verificationSuccess { | ||||||
|  | 		slog.Error("All verification attempts failed") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	slog.Info("Timestamp successfully verified") | ||||||
|  | } | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| The timestamp on this file is well-formatted, but will fail Bitcoin block |  | ||||||
| header validation. |  | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1 +0,0 @@ | |||||||
| Hello World! |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1 +0,0 @@ | |||||||
| The timestamp on this file is incomplete, and can be upgraded. |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1,2 +0,0 @@ | |||||||
| This file's timestamp has two attestations, one from a known notary, and one |  | ||||||
| from an unknown notary. |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1,2 +0,0 @@ | |||||||
| This file is one of three different files that have been timestamped together |  | ||||||
| with a single merkle tree. (1/3) |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1,2 +0,0 @@ | |||||||
| This file is one of three different files that have been timestamped together |  | ||||||
| with a single merkle tree. (2/3) |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1,2 +0,0 @@ | |||||||
| This file is one of three different files that have been timestamped together |  | ||||||
| with a single merkle tree. (3/3) |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1 +0,0 @@ | |||||||
| This file has an (incomplete) timestamp with two different calendars. |  | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -1 +0,0 @@ | |||||||
| This file's timestamp has a single attestation from an unknown notary. |  | ||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								fixtures/flatearthers-united.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fixtures/flatearthers-united.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | earth is flat | ||||||
							
								
								
									
										
											BIN
										
									
								
								fixtures/flatearthers-united.txt.certificate.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								fixtures/flatearthers-united.txt.certificate.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								fixtures/flatearthers-united.txt.ots
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								fixtures/flatearthers-united.txt.ots
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										32
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,22 +1,28 @@ | |||||||
| module github.com/nbd-wtf/opentimestamps | module git.intruders.space/public/opentimestamps | ||||||
|  |  | ||||||
| go 1.21 | go 1.24.2 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/btcsuite/btcd v0.23.4 | 	github.com/btcsuite/btcd v0.24.2 | ||||||
| 	github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 | 	github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 | ||||||
| 	golang.org/x/crypto v0.13.0 | 	github.com/jung-kurt/gofpdf v1.16.2 | ||||||
| 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 | 	github.com/spf13/cobra v1.9.1 | ||||||
|  | 	github.com/stretchr/testify v1.10.0 | ||||||
|  | 	golang.org/x/crypto v0.36.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect | 	github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect | ||||||
| 	github.com/btcsuite/btcd/btcutil v1.1.0 // indirect | 	github.com/btcsuite/btcd/btcutil v1.1.6 // indirect | ||||||
| 	github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect | 	github.com/btcsuite/btclog v1.0.0 // indirect | ||||||
| 	github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect | 	github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect | ||||||
| 	github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect | 	github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect | ||||||
| 	github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect | 	github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect | ||||||
| 	github.com/stretchr/testify v1.8.4 // indirect | 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect | ||||||
| 	golang.org/x/sys v0.12.0 // indirect | 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||||
|  | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
|  | 	github.com/spf13/pflag v1.0.6 // indirect | ||||||
|  | 	golang.org/x/sys v0.32.0 // indirect | ||||||
|  | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										68
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,18 +1,26 @@ | |||||||
| github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= | ||||||
|  | github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= | ||||||
| github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= | ||||||
| github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= | ||||||
| github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= | ||||||
| github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= | github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= | ||||||
|  | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= | ||||||
| github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= | ||||||
| github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= |  | ||||||
| github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= | ||||||
|  | github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= | ||||||
|  | github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= | ||||||
| github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= | ||||||
| github.com/btcsuite/btcd/btcutil v1.1.0 h1:MO4klnGY+EWJdoWF12Wkuf4AWDBPMpZNeN/jRLrklUU= |  | ||||||
| github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= | ||||||
| github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= | ||||||
|  | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= | ||||||
|  | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= | ||||||
|  | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= | ||||||
| github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= | ||||||
| github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= | ||||||
|  | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= | ||||||
| github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= | ||||||
|  | github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns= | ||||||
|  | github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= | ||||||
| github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= | ||||||
| github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= | ||||||
| github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= | ||||||
| @@ -23,13 +31,17 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg | |||||||
| github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= | ||||||
| github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= | ||||||
| github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= | ||||||
|  | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= | ||||||
| github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
|  | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= |  | ||||||
| github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= | ||||||
| github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= | github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= | ||||||
|  | github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= | ||||||
| github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= | ||||||
|  | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= | ||||||
|  | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= | ||||||
| github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= | ||||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||||
| github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= | ||||||
| @@ -40,13 +52,21 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU | |||||||
| github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | ||||||
| github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | ||||||
| github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||||
|  | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||||
| github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
| github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
| github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||||
|  | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||||
|  | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||||||
|  | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||||||
| github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | ||||||
| github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | ||||||
| github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= | ||||||
|  | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= | ||||||
|  | github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= | ||||||
|  | github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= | ||||||
| github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= | ||||||
| github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= | ||||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||||
| @@ -57,17 +77,33 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 | |||||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||||
| github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= | ||||||
| github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= | ||||||
|  | github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= | ||||||
|  | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||||
|  | github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= | ||||||
|  | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= | ||||||
|  | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= | ||||||
|  | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= | ||||||
|  | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||||
|  | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
|  | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
|  | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
|  | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
|  | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
|  | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||||
|  | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
|  | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= | ||||||
| golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= | ||||||
| golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= | ||||||
| golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||||
| golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= |  | ||||||
| golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| @@ -83,8 +119,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w | |||||||
| golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= | ||||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| @@ -97,11 +133,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ | |||||||
| google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | ||||||
| google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | ||||||
| google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||||
|  | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
|  | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								ots/attestation.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								ots/attestation.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | package ots | ||||||
|  |  | ||||||
|  | import "fmt" | ||||||
|  |  | ||||||
|  | type Attestation struct { | ||||||
|  | 	BitcoinBlockHeight uint64 | ||||||
|  | 	CalendarServerURL  string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (att Attestation) Name() string { | ||||||
|  | 	if att.BitcoinBlockHeight != 0 { | ||||||
|  | 		return "bitcoin" | ||||||
|  | 	} else if att.CalendarServerURL != "" { | ||||||
|  | 		return "pending" | ||||||
|  | 	} else { | ||||||
|  | 		return "unknown/broken" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (att Attestation) Human() string { | ||||||
|  | 	if att.BitcoinBlockHeight != 0 { | ||||||
|  | 		return fmt.Sprintf("bitcoin(%d)", att.BitcoinBlockHeight) | ||||||
|  | 	} else if att.CalendarServerURL != "" { | ||||||
|  | 		return fmt.Sprintf("pending(%s)", att.CalendarServerURL) | ||||||
|  | 	} else { | ||||||
|  | 		return "unknown/broken" | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package opentimestamps | package ots | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	deprecated_ripemd160 "golang.org/x/crypto/ripemd160" | 	deprecated_ripemd160 "golang.org/x/crypto/ripemd160" | ||||||
| @@ -1,57 +1,23 @@ | |||||||
| package opentimestamps | package ots | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"crypto/sha256" |  | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"golang.org/x/exp/slices" | 	"git.intruders.space/public/opentimestamps/varn" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| /* | // Header magic bytes | ||||||
|  * Header magic bytes | // Designed to be give the user some information in a hexdump, while being identified as 'data' by the file utility. | ||||||
|  * Designed to be give the user some information in a hexdump, while being identified as 'data' by the file utility. | // \x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94 | ||||||
|  * \x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94 |  | ||||||
|  */ |  | ||||||
| var headerMagic = []byte{0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94} |  | ||||||
| 
 |  | ||||||
| var ( | var ( | ||||||
| 	pendingMagic = []byte{0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e} | 	HeaderMagic  = []byte{0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94} | ||||||
| 	bitcoinMagic = []byte{0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01} | 	PendingMagic = []byte{0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e} | ||||||
|  | 	BitcoinMagic = []byte{0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type Operation struct { |  | ||||||
| 	Name   string |  | ||||||
| 	Tag    byte |  | ||||||
| 	Binary bool // it's an operation that takes one argument, otherwise takes none |  | ||||||
| 	Apply  func(curr []byte, arg []byte) []byte |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var tags = map[byte]*Operation{ |  | ||||||
| 	0xf0: {"append", 0xf0, true, func(curr []byte, arg []byte) []byte { |  | ||||||
| 		result := make([]byte, len(curr)+len(arg)) |  | ||||||
| 		copy(result[0:], curr) |  | ||||||
| 		copy(result[len(curr):], arg) |  | ||||||
| 		return result |  | ||||||
| 	}}, |  | ||||||
| 	0xf1: {"prepend", 0xf1, true, func(curr []byte, arg []byte) []byte { |  | ||||||
| 		result := make([]byte, len(curr)+len(arg)) |  | ||||||
| 		copy(result[0:], arg) |  | ||||||
| 		copy(result[len(arg):], curr) |  | ||||||
| 		return result |  | ||||||
| 	}}, |  | ||||||
| 	0xf2: {"reverse", 0xf2, false, func(curr []byte, arg []byte) []byte { panic("reverse not implemented") }}, |  | ||||||
| 	0xf3: {"hexlify", 0xf3, false, func(curr []byte, arg []byte) []byte { panic("hexlify not implemented") }}, |  | ||||||
| 	0x02: {"sha1", 0x02, false, func(curr []byte, arg []byte) []byte { panic("sha1 not implemented") }}, |  | ||||||
| 	0x03: {"ripemd160", 0x03, false, ripemd160}, |  | ||||||
| 	0x08: {"sha256", 0x08, false, func(curr []byte, arg []byte) []byte { |  | ||||||
| 		v := sha256.Sum256(curr) |  | ||||||
| 		return v[:] |  | ||||||
| 	}}, |  | ||||||
| 	0x67: {"keccak256", 0x67, false, func(curr []byte, arg []byte) []byte { panic("keccak256 not implemented") }}, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // A File represents the parsed content of an .ots file: it has an initial digest and | // A File represents the parsed content of an .ots file: it has an initial digest and | ||||||
| // a series of sequences of instructions. Each sequence must be evaluated separately, applying the operations | // a series of sequences of instructions. Each sequence must be evaluated separately, applying the operations | ||||||
| // on top of each other, starting with the .Digest until they end on an attestation. | // on top of each other, starting with the .Digest until they end on an attestation. | ||||||
| @@ -60,28 +26,6 @@ type File struct { | |||||||
| 	Sequences []Sequence | 	Sequences []Sequence | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // a Instruction can be an operation like "append" or "prepend" (this will be the case when .Operation != nil) |  | ||||||
| // or an attestation (when .Attestation != nil). |  | ||||||
| // It will have a non-nil .Argument whenever the operation requires an argument. |  | ||||||
| type Instruction struct { |  | ||||||
| 	*Operation |  | ||||||
| 	Argument []byte |  | ||||||
| 	*Attestation |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Sequence []Instruction |  | ||||||
| 
 |  | ||||||
| func (seq Sequence) Compute(initial []byte) []byte { |  | ||||||
| 	current := initial |  | ||||||
| 	for _, inst := range seq { |  | ||||||
| 		if inst.Operation == nil { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 		current = inst.Operation.Apply(current, inst.Argument) |  | ||||||
| 	} |  | ||||||
| 	return current |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ts File) GetPendingSequences() []Sequence { | func (ts File) GetPendingSequences() []Sequence { | ||||||
| 	bitcoin := ts.GetBitcoinAttestedSequences() | 	bitcoin := ts.GetBitcoinAttestedSequences() | ||||||
| 
 | 
 | ||||||
| @@ -122,20 +66,26 @@ func (ts File) GetBitcoinAttestedSequences() []Sequence { | |||||||
| 	return results | 	return results | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ts File) Human() string { | func (ts File) Human(withPartials bool) string { | ||||||
| 	strs := make([]string, 0, 100) | 	strs := make([]string, 0, 100) | ||||||
| 	strs = append(strs, fmt.Sprintf("file digest: %x", ts.Digest)) | 	strs = append(strs, fmt.Sprintf("file digest: %x", ts.Digest)) | ||||||
| 	strs = append(strs, fmt.Sprintf("hashed with: sha256")) | 	strs = append(strs, fmt.Sprintf("hashed with: sha256")) | ||||||
| 	strs = append(strs, "instruction sequences:") | 	strs = append(strs, "instruction sequences:") | ||||||
| 	for _, seq := range ts.Sequences { | 	for _, seq := range ts.Sequences { | ||||||
|  | 		curr := ts.Digest | ||||||
| 		strs = append(strs, "~>") | 		strs = append(strs, "~>") | ||||||
|  | 		strs = append(strs, "  start "+hex.EncodeToString(curr)) | ||||||
| 		for _, inst := range seq { | 		for _, inst := range seq { | ||||||
| 			line := "  " | 			line := "  " | ||||||
| 			if inst.Operation != nil { | 			if inst.Operation != nil { | ||||||
| 				line += inst.Operation.Name | 				line += inst.Operation.Name | ||||||
|  | 				curr = inst.Operation.Apply(curr, inst.Argument) | ||||||
| 				if inst.Operation.Binary { | 				if inst.Operation.Binary { | ||||||
| 					line += " " + hex.EncodeToString(inst.Argument) | 					line += " " + hex.EncodeToString(inst.Argument) | ||||||
| 				} | 				} | ||||||
|  | 				if withPartials { | ||||||
|  | 					line += " = " + hex.EncodeToString(curr) | ||||||
|  | 				} | ||||||
| 			} else if inst.Attestation != nil { | 			} else if inst.Attestation != nil { | ||||||
| 				line += inst.Attestation.Human() | 				line += inst.Attestation.Human() | ||||||
| 			} else { | 			} else { | ||||||
| @@ -149,8 +99,8 @@ func (ts File) Human() string { | |||||||
| 
 | 
 | ||||||
| func (ts File) SerializeToFile() []byte { | func (ts File) SerializeToFile() []byte { | ||||||
| 	data := make([]byte, 0, 5050) | 	data := make([]byte, 0, 5050) | ||||||
| 	data = append(data, headerMagic...) | 	data = append(data, HeaderMagic...) | ||||||
| 	data = appendVarUint(data, 1) | 	data = varn.AppendVarUint(data, 1) | ||||||
| 	data = append(data, 0x08) // sha256 | 	data = append(data, 0x08) // sha256 | ||||||
| 	data = append(data, ts.Digest...) | 	data = append(data, ts.Digest...) | ||||||
| 	data = append(data, ts.SerializeInstructionSequences()...) | 	data = append(data, ts.SerializeInstructionSequences()...) | ||||||
| @@ -170,7 +120,7 @@ func (ts File) SerializeInstructionSequences() []byte { | |||||||
| 		// keep an ordered slice of all the checkpoints we will potentially leave during our write journey for this sequence | 		// keep an ordered slice of all the checkpoints we will potentially leave during our write journey for this sequence | ||||||
| 		checkpoints := make([]int, 0, len(sequences[s1])) | 		checkpoints := make([]int, 0, len(sequences[s1])) | ||||||
| 		for s2 := s1 + 1; s2 < len(sequences); s2++ { | 		for s2 := s1 + 1; s2 < len(sequences); s2++ { | ||||||
| 			chp := getCommonPrefixIndex(sequences[s1], sequences[s2]) | 			chp := GetCommonPrefixIndex(sequences[s1], sequences[s2]) | ||||||
| 			if pos, found := slices.BinarySearch(checkpoints, chp); !found { | 			if pos, found := slices.BinarySearch(checkpoints, chp); !found { | ||||||
| 				checkpoints = append(checkpoints, -1)        // make room | 				checkpoints = append(checkpoints, -1)        // make room | ||||||
| 				copy(checkpoints[pos+1:], checkpoints[pos:]) // move elements to the right | 				copy(checkpoints[pos+1:], checkpoints[pos:]) // move elements to the right | ||||||
| @@ -203,7 +153,7 @@ func (ts File) SerializeInstructionSequences() []byte { | |||||||
| 				// write normal operation | 				// write normal operation | ||||||
| 				result = append(result, inst.Operation.Tag) | 				result = append(result, inst.Operation.Tag) | ||||||
| 				if inst.Operation.Binary { | 				if inst.Operation.Binary { | ||||||
| 					result = appendVarBytes(result, inst.Argument) | 					result = varn.AppendVarBytes(result, inst.Argument) | ||||||
| 				} | 				} | ||||||
| 			} else if inst.Attestation != nil { | 			} else if inst.Attestation != nil { | ||||||
| 				// write attestation record | 				// write attestation record | ||||||
| @@ -212,15 +162,15 @@ func (ts File) SerializeInstructionSequences() []byte { | |||||||
| 					// will use a new buffer for the actual attestation result | 					// will use a new buffer for the actual attestation result | ||||||
| 					abuf := make([]byte, 0, 100) | 					abuf := make([]byte, 0, 100) | ||||||
| 					if inst.BitcoinBlockHeight != 0 { | 					if inst.BitcoinBlockHeight != 0 { | ||||||
| 						result = append(result, bitcoinMagic...) // this goes in the main result buffer | 						result = append(result, BitcoinMagic...) // this goes in the main result buffer | ||||||
| 						abuf = appendVarUint(abuf, inst.BitcoinBlockHeight) | 						abuf = varn.AppendVarUint(abuf, inst.BitcoinBlockHeight) | ||||||
| 					} else if inst.CalendarServerURL != "" { | 					} else if inst.CalendarServerURL != "" { | ||||||
| 						result = append(result, pendingMagic...) // this goes in the main result buffer | 						result = append(result, PendingMagic...) // this goes in the main result buffer | ||||||
| 						abuf = appendVarBytes(abuf, []byte(inst.CalendarServerURL)) | 						abuf = varn.AppendVarBytes(abuf, []byte(inst.CalendarServerURL)) | ||||||
| 					} else { | 					} else { | ||||||
| 						panic(fmt.Sprintf("invalid attestation: %v", inst)) | 						panic(fmt.Sprintf("invalid attestation: %v", inst)) | ||||||
| 					} | 					} | ||||||
| 					result = appendVarBytes(result, abuf) // we append that result as varbytes | 					result = varn.AppendVarBytes(result, abuf) // we append that result as varbytes | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				panic(fmt.Sprintf("invalid instruction: %v", inst)) | 				panic(fmt.Sprintf("invalid instruction: %v", inst)) | ||||||
| @@ -230,27 +180,43 @@ func (ts File) SerializeInstructionSequences() []byte { | |||||||
| 	return result | 	return result | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Attestation struct { | func ParseOTSFile(buf varn.Buffer) (*File, error) { | ||||||
| 	BitcoinBlockHeight uint64 | 	// read magic | ||||||
| 	CalendarServerURL  string | 	// read version [1 byte] | ||||||
| } | 	// read crypto operation for file digest [1 byte] | ||||||
| 
 | 	// read file digest [32 byte (depends)] | ||||||
| func (att Attestation) Name() string { | 	if magic, err := buf.ReadBytes(len(HeaderMagic)); err != nil || !slices.Equal(HeaderMagic, magic) { | ||||||
| 	if att.BitcoinBlockHeight != 0 { | 		return nil, fmt.Errorf("invalid ots file header '%s': %w", magic, err) | ||||||
| 		return "bitcoin" |  | ||||||
| 	} else if att.CalendarServerURL != "" { |  | ||||||
| 		return "pending" |  | ||||||
| 	} else { |  | ||||||
| 		return "unknown/broken" |  | ||||||
| 	} | 	} | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func (att Attestation) Human() string { | 	if version, err := buf.ReadVarUint(); err != nil || version != 1 { | ||||||
| 	if att.BitcoinBlockHeight != 0 { | 		return nil, fmt.Errorf("invalid ots file version '%v': %w", version, err) | ||||||
| 		return fmt.Sprintf("bitcoin(%d)", att.BitcoinBlockHeight) |  | ||||||
| 	} else if att.CalendarServerURL != "" { |  | ||||||
| 		return fmt.Sprintf("pending(%s)", att.CalendarServerURL) |  | ||||||
| 	} else { |  | ||||||
| 		return "unknown/broken" |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	tag, err := buf.ReadByte() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to read operation byte: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if op, err := ReadInstruction(buf, tag); err != nil || op.Operation.Name != "sha256" { | ||||||
|  | 		return nil, fmt.Errorf("invalid crypto operation '%v', only sha256 supported: %w", op, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// if we got here assume the digest is sha256 | ||||||
|  | 	digest, err := buf.ReadBytes(32) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to read 32-byte digest: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ts := &File{ | ||||||
|  | 		Digest: digest, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if seqs, err := ParseTimestamp(buf); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} else { | ||||||
|  | 		ts.Sequences = seqs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ts, nil | ||||||
| } | } | ||||||
| @@ -1,11 +1,32 @@ | |||||||
| package opentimestamps | package ots | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"golang.org/x/exp/slices" | 	"git.intruders.space/public/opentimestamps/varn" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // a Instruction can be an operation like "append" or "prepend" (this will be the case when .Operation != nil) | ||||||
|  | // or an attestation (when .Attestation != nil). | ||||||
|  | // It will have a non-nil .Argument whenever the operation requires an argument. | ||||||
|  | type Instruction struct { | ||||||
|  | 	*Operation | ||||||
|  | 	Argument []byte | ||||||
|  | 	*Attestation | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetCommonPrefixIndex(s1 []Instruction, s2 []Instruction) int { | ||||||
|  | 	n := min(len(s1), len(s2)) | ||||||
|  | 	for i := 0; i < n; i++ { | ||||||
|  | 		if CompareInstructions(s1[i], s2[i]) != 0 { | ||||||
|  | 			return i | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return n | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // CompareInstructions returns negative if a<b, 0 if a=b and positive if a>b. | // CompareInstructions returns negative if a<b, 0 if a=b and positive if a>b. | ||||||
| // It considers an operation smaller than an attestation, a pending attestation smaller than a Bitcoin attestation. | // It considers an operation smaller than an attestation, a pending attestation smaller than a Bitcoin attestation. | ||||||
| // It orders operations by their tag byte and then by their argument. | // It orders operations by their tag byte and then by their argument. | ||||||
| @@ -50,3 +71,24 @@ func CompareInstructions(a, b Instruction) int { | |||||||
| 		return 0 | 		return 0 | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func ReadInstruction(buf varn.Buffer, tag byte) (*Instruction, error) { | ||||||
|  | 	op, ok := Tags[tag] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("unknown tag %v", tag) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	inst := Instruction{ | ||||||
|  | 		Operation: op, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if op.Binary { | ||||||
|  | 		val, err := buf.ReadVarBytes() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error reading argument: %w", err) | ||||||
|  | 		} | ||||||
|  | 		inst.Argument = val | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &inst, nil | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								ots/operation.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								ots/operation.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | package ots | ||||||
|  |  | ||||||
|  | import "crypto/sha256" | ||||||
|  |  | ||||||
|  | type Operation struct { | ||||||
|  | 	Name   string | ||||||
|  | 	Tag    byte | ||||||
|  | 	Binary bool // it's an operation that takes one argument, otherwise takes none | ||||||
|  | 	Apply  func(curr []byte, arg []byte) []byte | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var Tags = map[byte]*Operation{ | ||||||
|  | 	0xf0: {"append", 0xf0, true, func(curr []byte, arg []byte) []byte { | ||||||
|  | 		result := make([]byte, len(curr)+len(arg)) | ||||||
|  | 		copy(result[0:], curr) | ||||||
|  | 		copy(result[len(curr):], arg) | ||||||
|  | 		return result | ||||||
|  | 	}}, | ||||||
|  | 	0xf1: {"prepend", 0xf1, true, func(curr []byte, arg []byte) []byte { | ||||||
|  | 		result := make([]byte, len(curr)+len(arg)) | ||||||
|  | 		copy(result[0:], arg) | ||||||
|  | 		copy(result[len(arg):], curr) | ||||||
|  | 		return result | ||||||
|  | 	}}, | ||||||
|  | 	0xf2: {"reverse", 0xf2, false, func(curr []byte, arg []byte) []byte { panic("reverse not implemented") }}, | ||||||
|  | 	0xf3: {"hexlify", 0xf3, false, func(curr []byte, arg []byte) []byte { panic("hexlify not implemented") }}, | ||||||
|  | 	0x02: {"sha1", 0x02, false, func(curr []byte, arg []byte) []byte { panic("sha1 not implemented") }}, | ||||||
|  | 	0x03: {"ripemd160", 0x03, false, ripemd160}, | ||||||
|  | 	0x08: {"sha256", 0x08, false, func(curr []byte, arg []byte) []byte { | ||||||
|  | 		v := sha256.Sum256(curr) | ||||||
|  | 		return v[:] | ||||||
|  | 	}}, | ||||||
|  | 	0x67: {"keccak256", 0x67, false, func(curr []byte, arg []byte) []byte { panic("keccak256 not implemented") }}, | ||||||
|  | } | ||||||
							
								
								
									
										176
									
								
								ots/sequence.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								ots/sequence.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | package ots | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"slices" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps/varn" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/verifyer" | ||||||
|  | 	"github.com/btcsuite/btcd/wire" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Sequence []Instruction | ||||||
|  |  | ||||||
|  | func (seq Sequence) GetAttestation() Attestation { | ||||||
|  | 	if len(seq) == 0 { | ||||||
|  | 		return Attestation{} | ||||||
|  | 	} | ||||||
|  | 	att := seq[len(seq)-1] | ||||||
|  | 	if att.Attestation == nil { | ||||||
|  | 		return Attestation{} | ||||||
|  | 	} | ||||||
|  | 	return *att.Attestation | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Compute runs a sequence of operations on top of an initial digest and returns the result, which is often a | ||||||
|  | // Bitcoin block merkle root. It also tries to identify the point in the sequence in which an actual Bitcoin | ||||||
|  | // transaction is formed and parse that. | ||||||
|  | func (seq Sequence) Compute(initial []byte) (merkleRoot []byte, bitcoinTx *wire.MsgTx) { | ||||||
|  | 	current := initial | ||||||
|  | 	for i, inst := range seq { | ||||||
|  | 		if inst.Operation == nil { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// the first time we do a double-sha256 that is likely a bitcoin transaction | ||||||
|  | 		if bitcoinTx == nil && | ||||||
|  | 			inst.Operation.Name == "sha256" && | ||||||
|  | 			len(seq) > i+1 && seq[i+1].Operation != nil && | ||||||
|  | 			seq[i+1].Operation.Name == "sha256" { | ||||||
|  | 			tx := &wire.MsgTx{} | ||||||
|  | 			tx.Deserialize(bytes.NewReader(current)) | ||||||
|  | 			bitcoinTx = tx | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		current = inst.Operation.Apply(current, inst.Argument) | ||||||
|  | 	} | ||||||
|  | 	return current, bitcoinTx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Verify validates sequence of operations that starts with digest and ends on a Bitcoin attestation against | ||||||
|  | // an actual Bitcoin block, as given by the provided Bitcoin interface. | ||||||
|  | func (seq Sequence) Verify(bitcoin verifyer.Bitcoin, digest []byte) (*wire.MsgTx, error) { | ||||||
|  | 	if len(seq) == 0 { | ||||||
|  | 		return nil, fmt.Errorf("empty sequence") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	att := seq[len(seq)-1] | ||||||
|  | 	if att.Attestation == nil || att.BitcoinBlockHeight == 0 { | ||||||
|  | 		return nil, fmt.Errorf("sequence doesn't include a bitcoin attestation") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to get block %d hash: %w", att.BitcoinBlockHeight, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	blockHeader, err := bitcoin.GetBlockHeader(blockHash) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to get block %s header: %w", blockHash, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	merkleRoot := blockHeader.MerkleRoot[:] | ||||||
|  | 	result, tx := seq.Compute(digest) | ||||||
|  |  | ||||||
|  | 	if !bytes.Equal(result, merkleRoot) { | ||||||
|  | 		return nil, fmt.Errorf("sequence result '%x' doesn't match the bitcoin merkle root for block %d: %x", | ||||||
|  | 			result, att.BitcoinBlockHeight, merkleRoot) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return tx, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ParseTimestamp(buf varn.Buffer) ([]Sequence, error) { | ||||||
|  | 	// read instructions | ||||||
|  | 	//   if operation = push | ||||||
|  | 	//   if 0x00 = attestation | ||||||
|  | 	//      read tag [8 bytes] | ||||||
|  | 	//      readvarbytes | ||||||
|  | 	//        interpret these depending on the type of attestation | ||||||
|  | 	//          if bitcoin: readvaruint as the block height | ||||||
|  | 	//          if pending from calendar: readvarbytes as the utf-8 calendar url | ||||||
|  | 	//      end or go back to last continuation byte | ||||||
|  | 	//   if 0xff = pick up a continuation byte (checkpoint) and add it to stack | ||||||
|  |  | ||||||
|  | 	currInstructionsBlock := 0 | ||||||
|  | 	seqs := make([]Sequence, 0, 10) | ||||||
|  |  | ||||||
|  | 	// we will store checkpoints here | ||||||
|  | 	checkpoints := make([][]Instruction, 0, 4) | ||||||
|  |  | ||||||
|  | 	// start first instruction block | ||||||
|  | 	seqs = append(seqs, make([]Instruction, 0, 30)) | ||||||
|  |  | ||||||
|  | 	// go read these tags | ||||||
|  | 	for { | ||||||
|  | 		tag, err := buf.ReadByte() | ||||||
|  | 		if err != nil { | ||||||
|  | 			if err == io.EOF { | ||||||
|  | 				return seqs, nil | ||||||
|  | 			} | ||||||
|  | 			return nil, fmt.Errorf("failed to read operation byte: %w", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if tag == 0x00 { | ||||||
|  | 			// enter an attestation context | ||||||
|  | 			magic, err := buf.ReadBytes(8) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("failed to read attestion magic bytes: %w", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			this, err := buf.ReadVarBytes() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("failed to read attestation bytes: %w", err) | ||||||
|  | 			} | ||||||
|  | 			abuf := varn.NewBuffer(this) | ||||||
|  |  | ||||||
|  | 			switch { | ||||||
|  | 			case slices.Equal(magic, PendingMagic): | ||||||
|  | 				val, err := abuf.ReadVarBytes() | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, fmt.Errorf("failed reading calendar server url: %w", err) | ||||||
|  | 				} | ||||||
|  | 				seqs[currInstructionsBlock] = append( | ||||||
|  | 					seqs[currInstructionsBlock], | ||||||
|  | 					Instruction{Attestation: &Attestation{CalendarServerURL: string(val)}}, | ||||||
|  | 				) | ||||||
|  | 			case slices.Equal(magic, BitcoinMagic): | ||||||
|  | 				val, err := abuf.ReadVarUint() | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, fmt.Errorf("failed reading bitcoin block number: %w", err) | ||||||
|  | 				} | ||||||
|  | 				seqs[currInstructionsBlock] = append( | ||||||
|  | 					seqs[currInstructionsBlock], | ||||||
|  | 					Instruction{Attestation: &Attestation{BitcoinBlockHeight: val}}, | ||||||
|  | 				) | ||||||
|  | 			default: | ||||||
|  | 				return nil, fmt.Errorf("unsupported attestation type '%x': %x", magic, this) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// check if we have checkpoints and, if yes, copy them in a new block of instructions | ||||||
|  | 			ncheckpoints := len(checkpoints) | ||||||
|  | 			if ncheckpoints > 0 { | ||||||
|  | 				// use this checkpoint as the starting point for the next block | ||||||
|  | 				chp := checkpoints[ncheckpoints-1] | ||||||
|  | 				checkpoints = checkpoints[0 : ncheckpoints-1] // remove this from the stack | ||||||
|  | 				seqs = append(seqs, chp) | ||||||
|  | 				currInstructionsBlock++ | ||||||
|  | 			} | ||||||
|  | 		} else if tag == 0xff { | ||||||
|  | 			// pick up a checkpoint to be used later | ||||||
|  | 			currentBlock := seqs[currInstructionsBlock] | ||||||
|  | 			chp := make([]Instruction, len(currentBlock)) | ||||||
|  | 			copy(chp, currentBlock) | ||||||
|  | 			checkpoints = append(checkpoints, chp) | ||||||
|  | 		} else { | ||||||
|  | 			// a new operation in this block | ||||||
|  | 			inst, err := ReadInstruction(buf, tag) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("failed to read instruction: %w", err) | ||||||
|  | 			} | ||||||
|  | 			seqs[currInstructionsBlock] = append(seqs[currInstructionsBlock], *inst) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										175
									
								
								parsers.go
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								parsers.go
									
									
									
									
									
								
							| @@ -1,175 +0,0 @@ | |||||||
| package opentimestamps |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/exp/slices" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func parseCalendarServerResponse(buf Buffer) (Sequence, error) { |  | ||||||
| 	seqs, err := parseTimestamp(buf) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(seqs) != 1 { |  | ||||||
| 		return nil, fmt.Errorf("invalid number of sequences obtained: %d", len(seqs)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return seqs[0], nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func parseOTSFile(buf Buffer) (*File, error) { |  | ||||||
| 	// read magic |  | ||||||
| 	// read version [1 byte] |  | ||||||
| 	// read crypto operation for file digest [1 byte] |  | ||||||
| 	// read file digest [32 byte (depends)] |  | ||||||
| 	if magic, err := buf.readBytes(len(headerMagic)); err != nil || !slices.Equal(headerMagic, magic) { |  | ||||||
| 		return nil, fmt.Errorf("invalid ots file header '%s': %w", magic, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if version, err := buf.readVarUint(); err != nil || version != 1 { |  | ||||||
| 		return nil, fmt.Errorf("invalid ots file version '%v': %w", version, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	tag, err := buf.readByte() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to read operation byte: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if op, err := readInstruction(buf, tag); err != nil || op.Operation.Name != "sha256" { |  | ||||||
| 		return nil, fmt.Errorf("invalid crypto operation '%v', only sha256 supported: %w", op, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// if we got here assume the digest is sha256 |  | ||||||
| 	digest, err := buf.readBytes(32) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to read 32-byte digest: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ts := &File{ |  | ||||||
| 		Digest: digest, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if seqs, err := parseTimestamp(buf); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} else { |  | ||||||
| 		ts.Sequences = seqs |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return ts, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func parseTimestamp(buf Buffer) ([]Sequence, error) { |  | ||||||
| 	// read instructions |  | ||||||
| 	//   if operation = push |  | ||||||
| 	//   if 0x00 = attestation |  | ||||||
| 	//      read tag [8 bytes] |  | ||||||
| 	//      readvarbytes |  | ||||||
| 	//        interpret these depending on the type of attestation |  | ||||||
| 	//          if bitcoin: readvaruint as the block height |  | ||||||
| 	//          if pending from calendar: readvarbytes as the utf-8 calendar url |  | ||||||
| 	//      end or go back to last continuation byte |  | ||||||
| 	//   if 0xff = pick up a continuation byte (checkpoint) and add it to stack |  | ||||||
|  |  | ||||||
| 	currInstructionsBlock := 0 |  | ||||||
| 	seqs := make([]Sequence, 0, 10) |  | ||||||
|  |  | ||||||
| 	// we will store checkpoints here |  | ||||||
| 	checkpoints := make([][]Instruction, 0, 4) |  | ||||||
|  |  | ||||||
| 	// start first instruction block |  | ||||||
| 	seqs = append(seqs, make([]Instruction, 0, 30)) |  | ||||||
|  |  | ||||||
| 	// go read these tags |  | ||||||
| 	for { |  | ||||||
| 		tag, err := buf.readByte() |  | ||||||
| 		if err != nil { |  | ||||||
| 			if err == io.EOF { |  | ||||||
| 				return seqs, nil |  | ||||||
| 			} |  | ||||||
| 			return nil, fmt.Errorf("failed to read operation byte: %w", err) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if tag == 0x00 { |  | ||||||
| 			// enter an attestation context |  | ||||||
| 			magic, err := buf.readBytes(8) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, fmt.Errorf("failed to read attestion magic bytes: %w", err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			this, err := buf.readVarBytes() |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, fmt.Errorf("failed to read attestation bytes: %w", err) |  | ||||||
| 			} |  | ||||||
| 			abuf := newBuffer(this) |  | ||||||
|  |  | ||||||
| 			switch { |  | ||||||
| 			case slices.Equal(magic, pendingMagic): |  | ||||||
| 				val, err := abuf.readVarBytes() |  | ||||||
| 				if err != nil { |  | ||||||
| 					return nil, fmt.Errorf("failed reading calendar server url: %w", err) |  | ||||||
| 				} |  | ||||||
| 				seqs[currInstructionsBlock] = append( |  | ||||||
| 					seqs[currInstructionsBlock], |  | ||||||
| 					Instruction{Attestation: &Attestation{CalendarServerURL: string(val)}}, |  | ||||||
| 				) |  | ||||||
| 			case slices.Equal(magic, bitcoinMagic): |  | ||||||
| 				val, err := abuf.readVarUint() |  | ||||||
| 				if err != nil { |  | ||||||
| 					return nil, fmt.Errorf("failed reading bitcoin block number: %w", err) |  | ||||||
| 				} |  | ||||||
| 				seqs[currInstructionsBlock] = append( |  | ||||||
| 					seqs[currInstructionsBlock], |  | ||||||
| 					Instruction{Attestation: &Attestation{BitcoinBlockHeight: val}}, |  | ||||||
| 				) |  | ||||||
| 			default: |  | ||||||
| 				return nil, fmt.Errorf("unsupported attestation type '%x': %x", magic, this) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// check if we have checkpoints and, if yes, copy them in a new block of instructions |  | ||||||
| 			ncheckpoints := len(checkpoints) |  | ||||||
| 			if ncheckpoints > 0 { |  | ||||||
| 				// use this checkpoint as the starting point for the next block |  | ||||||
| 				chp := checkpoints[ncheckpoints-1] |  | ||||||
| 				checkpoints = checkpoints[0 : ncheckpoints-1] // remove this from the stack |  | ||||||
| 				seqs = append(seqs, chp) |  | ||||||
| 				currInstructionsBlock++ |  | ||||||
| 			} |  | ||||||
| 		} else if tag == 0xff { |  | ||||||
| 			// pick up a checkpoint to be used later |  | ||||||
| 			currentBlock := seqs[currInstructionsBlock] |  | ||||||
| 			chp := make([]Instruction, len(currentBlock)) |  | ||||||
| 			copy(chp, currentBlock) |  | ||||||
| 			checkpoints = append(checkpoints, chp) |  | ||||||
| 		} else { |  | ||||||
| 			// a new operation in this block |  | ||||||
| 			inst, err := readInstruction(buf, tag) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, fmt.Errorf("failed to read instruction: %w", err) |  | ||||||
| 			} |  | ||||||
| 			seqs[currInstructionsBlock] = append(seqs[currInstructionsBlock], *inst) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func readInstruction(buf Buffer, tag byte) (*Instruction, error) { |  | ||||||
| 	op, ok := tags[tag] |  | ||||||
| 	if !ok { |  | ||||||
| 		return nil, fmt.Errorf("unknown tag %v", tag) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	inst := Instruction{ |  | ||||||
| 		Operation: op, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if op.Binary { |  | ||||||
| 		val, err := buf.readVarBytes() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("error reading argument: %w", err) |  | ||||||
| 		} |  | ||||||
| 		inst.Argument = val |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return &inst, nil |  | ||||||
| } |  | ||||||
							
								
								
									
										52
									
								
								stamp.go
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								stamp.go
									
									
									
									
									
								
							| @@ -6,9 +6,15 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps/ots" | ||||||
|  | 	"git.intruders.space/public/opentimestamps/varn" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (Sequence, error) { | var httpClient = &http.Client{} | ||||||
|  |  | ||||||
|  | func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (ots.Sequence, error) { | ||||||
| 	body := bytes.NewBuffer(digest[:]) | 	body := bytes.NewBuffer(digest[:]) | ||||||
| 	req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/digest", body) | 	req, err := http.NewRequestWithContext(ctx, "POST", normalizeUrl(calendarUrl)+"/digest", body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -18,7 +24,8 @@ func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (Sequence, | |||||||
| 	req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps") | 	req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps") | ||||||
| 	req.Header.Add("Accept", "application/vnd.opentimestamps.v1") | 	req.Header.Add("Accept", "application/vnd.opentimestamps.v1") | ||||||
| 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||||||
| 	resp, err := http.DefaultClient.Do(req) |  | ||||||
|  | 	resp, err := httpClient.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("'%s' request failed: %w", calendarUrl, err) | 		return nil, fmt.Errorf("'%s' request failed: %w", calendarUrl, err) | ||||||
| 	} | 	} | ||||||
| @@ -29,7 +36,7 @@ func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (Sequence, | |||||||
| 	} | 	} | ||||||
| 	resp.Body.Close() | 	resp.Body.Close() | ||||||
|  |  | ||||||
| 	seq, err := parseCalendarServerResponse(newBuffer(full)) | 	seq, err := parseCalendarServerResponse(varn.NewBuffer(full)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to parse response from '%s': %w", calendarUrl, err) | 		return nil, fmt.Errorf("failed to parse response from '%s': %w", calendarUrl, err) | ||||||
| 	} | 	} | ||||||
| @@ -37,13 +44,13 @@ func Stamp(ctx context.Context, calendarUrl string, digest [32]byte) (Sequence, | |||||||
| 	return seq, nil | 	return seq, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func ReadFromFile(data []byte) (*File, error) { | func ReadFromFile(data []byte) (*ots.File, error) { | ||||||
| 	return parseOTSFile(newBuffer(data)) | 	return ots.ParseOTSFile(varn.NewBuffer(data)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (seq Sequence) Upgrade(ctx context.Context, initial []byte) (Sequence, error) { | func UpgradeSequence(ctx context.Context, seq ots.Sequence, initial []byte) (ots.Sequence, error) { | ||||||
| 	result := seq.Compute(initial) | 	result, _ := seq.Compute(initial) | ||||||
| 	attestation := seq[len(seq)-1] | 	attestation := seq.GetAttestation() | ||||||
|  |  | ||||||
| 	url := fmt.Sprintf("%s/timestamp/%x", normalizeUrl(attestation.CalendarServerURL), result) | 	url := fmt.Sprintf("%s/timestamp/%x", normalizeUrl(attestation.CalendarServerURL), result) | ||||||
| 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil) | 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil) | ||||||
| @@ -54,7 +61,8 @@ func (seq Sequence) Upgrade(ctx context.Context, initial []byte) (Sequence, erro | |||||||
| 	req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps") | 	req.Header.Add("User-Agent", "github.com/fiatjaf/opentimestamps") | ||||||
| 	req.Header.Add("Accept", "application/vnd.opentimestamps.v1") | 	req.Header.Add("Accept", "application/vnd.opentimestamps.v1") | ||||||
| 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||||||
| 	resp, err := http.DefaultClient.Do(req) |  | ||||||
|  | 	resp, err := httpClient.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("'%s' request failed: %w", attestation.CalendarServerURL, err) | 		return nil, fmt.Errorf("'%s' request failed: %w", attestation.CalendarServerURL, err) | ||||||
| 	} | 	} | ||||||
| @@ -69,10 +77,34 @@ func (seq Sequence) Upgrade(ctx context.Context, initial []byte) (Sequence, erro | |||||||
| 	} | 	} | ||||||
| 	resp.Body.Close() | 	resp.Body.Close() | ||||||
|  |  | ||||||
| 	newSeq, err := parseCalendarServerResponse(newBuffer(body)) | 	tail, err := parseCalendarServerResponse(varn.NewBuffer(body)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to parse response from '%s': %w", attestation.CalendarServerURL, err) | 		return nil, fmt.Errorf("failed to parse response from '%s': %w", attestation.CalendarServerURL, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	newSeq := make(ots.Sequence, len(seq)+len(tail)-1) | ||||||
|  | 	copy(newSeq, seq[0:len(seq)-1]) | ||||||
|  | 	copy(newSeq[len(seq)-1:], tail) | ||||||
|  |  | ||||||
| 	return newSeq, nil | 	return newSeq, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func parseCalendarServerResponse(buf varn.Buffer) (ots.Sequence, error) { | ||||||
|  | 	seqs, err := ots.ParseTimestamp(buf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if len(seqs) != 1 { | ||||||
|  | 		return nil, fmt.Errorf("invalid number of sequences obtained: %d", len(seqs)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return seqs[0], nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func normalizeUrl(u string) string { | ||||||
|  | 	u = strings.TrimSuffix(u, "/") | ||||||
|  | 	if !strings.HasPrefix(u, "https://") && !strings.HasPrefix(u, "http://") { | ||||||
|  | 		u = "http://" + u | ||||||
|  | 	} | ||||||
|  | 	return u | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										111
									
								
								utils.go
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								utils.go
									
									
									
									
									
								
							| @@ -1,111 +0,0 @@ | |||||||
| package opentimestamps |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"io" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func normalizeUrl(u string) string { |  | ||||||
| 	if strings.HasSuffix(u, "/") { |  | ||||||
| 		u = u[0 : len(u)-1] |  | ||||||
| 	} |  | ||||||
| 	if !strings.HasPrefix(u, "https://") && !strings.HasPrefix(u, "http://") { |  | ||||||
| 		u = "http://" + u |  | ||||||
| 	} |  | ||||||
| 	return u |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Buffer struct { |  | ||||||
| 	pos *int |  | ||||||
| 	buf []byte |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func newBuffer(buf []byte) Buffer { |  | ||||||
| 	zero := 0 |  | ||||||
| 	return Buffer{&zero, buf} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (buf Buffer) readBytes(n int) ([]byte, error) { |  | ||||||
| 	// fmt.Println("reading", n, "bytes") |  | ||||||
| 	if *buf.pos >= len(buf.buf) { |  | ||||||
| 		return nil, io.EOF |  | ||||||
| 	} |  | ||||||
| 	res := buf.buf[*buf.pos : *buf.pos+n] |  | ||||||
| 	*buf.pos = *buf.pos + n |  | ||||||
| 	// fmt.Println("->", hex.EncodeToString(res)) |  | ||||||
| 	return res, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (buf Buffer) readByte() (byte, error) { |  | ||||||
| 	b, err := buf.readBytes(1) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	return b[0], nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (buf Buffer) readVarUint() (uint64, error) { |  | ||||||
| 	var value uint64 = 0 |  | ||||||
| 	var shift uint64 = 0 |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		b, err := buf.readByte() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
| 		value |= (uint64(b) & 0b01111111) << shift |  | ||||||
| 		shift += 7 |  | ||||||
| 		if b&0b10000000 == 0 { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return value, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (buf Buffer) readVarBytes() ([]byte, error) { |  | ||||||
| 	v, err := buf.readVarUint() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	b, err := buf.readBytes(int(v)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return b, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func appendVarUint(buf []byte, value uint64) []byte { |  | ||||||
| 	if value == 0 { |  | ||||||
| 		buf = append(buf, 0) |  | ||||||
| 	} else { |  | ||||||
| 		for value != 0 { |  | ||||||
| 			b := byte(value & 0b01111111) |  | ||||||
| 			if value > 0b01111111 { |  | ||||||
| 				b |= 0b10000000 |  | ||||||
| 			} |  | ||||||
| 			buf = append(buf, b) |  | ||||||
| 			if value <= 0b01111111 { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 			value >>= 7 |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return buf |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func appendVarBytes(buf []byte, value []byte) []byte { |  | ||||||
| 	buf = appendVarUint(buf, uint64(len(value))) |  | ||||||
| 	buf = append(buf, value...) |  | ||||||
| 	return buf |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func getCommonPrefixIndex(s1 []Instruction, s2 []Instruction) int { |  | ||||||
| 	n := min(len(s1), len(s2)) |  | ||||||
| 	for i := 0; i < n; i++ { |  | ||||||
| 		if CompareInstructions(s1[i], s2[i]) != 0 { |  | ||||||
| 			return i |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return n |  | ||||||
| } |  | ||||||
							
								
								
									
										81
									
								
								varn/buffer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								varn/buffer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | package varn | ||||||
|  |  | ||||||
|  | import "io" | ||||||
|  |  | ||||||
|  | // Buffer is a simple buffer reader that allows reading bytes and | ||||||
|  | // variable-length integers. It is not thread-safe and should not be used | ||||||
|  | // concurrently. | ||||||
|  | type Buffer struct { | ||||||
|  | 	pos *int | ||||||
|  | 	buf []byte | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBuffer creates a new Buffer instance. | ||||||
|  | func NewBuffer(buf []byte) Buffer { | ||||||
|  | 	zero := 0 | ||||||
|  | 	return Buffer{&zero, buf} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ReadBytes reads n bytes from the buffer. | ||||||
|  | func (buf Buffer) ReadBytes(n int) ([]byte, error) { | ||||||
|  | 	if *buf.pos >= len(buf.buf) { | ||||||
|  | 		return nil, io.EOF | ||||||
|  | 	} | ||||||
|  | 	if *buf.pos+n > len(buf.buf) { | ||||||
|  | 		return nil, io.ErrUnexpectedEOF | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res := buf.buf[*buf.pos : *buf.pos+n] | ||||||
|  | 	*buf.pos = *buf.pos + n | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (buf Buffer) ReadByte() (byte, error) { | ||||||
|  | 	b, err := buf.ReadBytes(1) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return b[0], nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ReadVarUint reads a variable-length unsigned integer from the buffer. | ||||||
|  | // Returns io.EOF if the end of the buffer is reached before a complete integer | ||||||
|  | // is read. | ||||||
|  | func (buf Buffer) ReadVarUint() (uint64, error) { | ||||||
|  | 	var value uint64 = 0 | ||||||
|  | 	var shift uint64 = 0 | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		b, err := buf.ReadByte() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return 0, err | ||||||
|  | 		} | ||||||
|  | 		value |= (uint64(b) & 0b01111111) << shift | ||||||
|  | 		shift += 7 | ||||||
|  | 		if b&0b10000000 == 0 { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return value, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ReadVarBytes reads a variable-length byte array from the buffer. | ||||||
|  | // It first reads a variable-length unsigned integer to determine the length | ||||||
|  | // of the byte array, and then reads that many bytes from the buffer. | ||||||
|  | // Returns the byte array and an error if any occurs during reading. | ||||||
|  | // The function will return io.EOF if the end of the buffer is reached before | ||||||
|  | // the specified number of bytes is read. | ||||||
|  | // If the length of the byte array is 0, it will return an empty byte slice. | ||||||
|  | // If the length is greater than the remaining bytes in the buffer, it will | ||||||
|  | // return io.EOF. | ||||||
|  | func (buf Buffer) ReadVarBytes() ([]byte, error) { | ||||||
|  | 	v, err := buf.ReadVarUint() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	b, err := buf.ReadBytes(int(v)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return b, nil | ||||||
|  | } | ||||||
							
								
								
									
										155
									
								
								varn/buffer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								varn/buffer_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | |||||||
|  | package varn_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps/varn" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestNewBuffer(t *testing.T) { | ||||||
|  | 	data := []byte{0x01, 0x02, 0x03} | ||||||
|  | 	buf := varn.NewBuffer(data) | ||||||
|  |  | ||||||
|  | 	// Test the first byte to verify initialization | ||||||
|  | 	b, err := buf.ReadByte() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, byte(0x01), b) | ||||||
|  |  | ||||||
|  | 	// Test reading the second byte | ||||||
|  | 	b, err = buf.ReadByte() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, byte(0x02), b) | ||||||
|  |  | ||||||
|  | 	// Test reading the third byte | ||||||
|  | 	b, err = buf.ReadByte() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, byte(0x03), b) | ||||||
|  |  | ||||||
|  | 	// Test reading beyond the buffer | ||||||
|  | 	_, err = buf.ReadByte() | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  |  | ||||||
|  | 	// Test reading from an empty buffer | ||||||
|  | 	emptyBuf := varn.NewBuffer([]byte{}) | ||||||
|  | 	_, err = emptyBuf.ReadByte() | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  |  | ||||||
|  | 	// Test reading from a nil buffer | ||||||
|  | 	nilBuf := varn.NewBuffer(nil) | ||||||
|  | 	_, err = nilBuf.ReadByte() | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  |  | ||||||
|  | 	// Test reading from a buffer with a single byte | ||||||
|  | 	singleByteBuf := varn.NewBuffer([]byte{0xFF}) | ||||||
|  | 	b, err = singleByteBuf.ReadByte() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, byte(0xFF), b) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReadBytes(t *testing.T) { | ||||||
|  | 	data := []byte{0x01, 0x02, 0x03, 0x04, 0x05} | ||||||
|  | 	buf := varn.NewBuffer(data) | ||||||
|  |  | ||||||
|  | 	// Read 3 bytes | ||||||
|  | 	bytes, err := buf.ReadBytes(3) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, []byte{0x01, 0x02, 0x03}, bytes) | ||||||
|  |  | ||||||
|  | 	// Read 2 more bytes | ||||||
|  | 	bytes, err = buf.ReadBytes(2) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, []byte{0x04, 0x05}, bytes) | ||||||
|  |  | ||||||
|  | 	// Try to read beyond the buffer | ||||||
|  | 	_, err = buf.ReadBytes(1) | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReadByte(t *testing.T) { | ||||||
|  | 	data := []byte{0x0A, 0x0B} | ||||||
|  | 	buf := varn.NewBuffer(data) | ||||||
|  |  | ||||||
|  | 	// Read first byte | ||||||
|  | 	b, err := buf.ReadByte() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, byte(0x0A), b) | ||||||
|  |  | ||||||
|  | 	// Read second byte | ||||||
|  | 	b, err = buf.ReadByte() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, byte(0x0B), b) | ||||||
|  |  | ||||||
|  | 	// Try to read beyond the buffer | ||||||
|  | 	_, err = buf.ReadByte() | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReadVarUint(t *testing.T) { | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name     string | ||||||
|  | 		input    []byte | ||||||
|  | 		expected uint64 | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:     "Single byte", | ||||||
|  | 			input:    []byte{0x01}, | ||||||
|  | 			expected: 1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:     "Two bytes", | ||||||
|  | 			input:    []byte{0x81, 0x01}, // 129 in varint encoding | ||||||
|  | 			expected: 129, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:     "Large number", | ||||||
|  | 			input:    []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01}, // 536870911 in varint encoding | ||||||
|  | 			expected: 536870911, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			buf := varn.NewBuffer(tc.input) | ||||||
|  | 			val, err := buf.ReadVarUint() | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			assert.Equal(t, tc.expected, val) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test EOF | ||||||
|  | 	buf := varn.NewBuffer([]byte{0x81}) // Incomplete varint | ||||||
|  | 	_, err := buf.ReadVarUint() | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReadVarBytes(t *testing.T) { | ||||||
|  | 	// Test normal case: length followed by data | ||||||
|  | 	data := []byte{0x03, 0x0A, 0x0B, 0x0C, 0x0D} | ||||||
|  | 	buf := varn.NewBuffer(data) | ||||||
|  |  | ||||||
|  | 	bytes, err := buf.ReadVarBytes() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, []byte{0x0A, 0x0B, 0x0C}, bytes) | ||||||
|  |  | ||||||
|  | 	// Test empty array | ||||||
|  | 	data = []byte{0x00, 0x01} | ||||||
|  | 	buf = varn.NewBuffer(data) | ||||||
|  |  | ||||||
|  | 	bytes, err = buf.ReadVarBytes() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, []byte{}, bytes) | ||||||
|  |  | ||||||
|  | 	// Test EOF during length read | ||||||
|  | 	buf = varn.NewBuffer([]byte{}) | ||||||
|  | 	_, err = buf.ReadVarBytes() | ||||||
|  | 	assert.ErrorIs(t, err, io.EOF) | ||||||
|  |  | ||||||
|  | 	// Test error during data read (insufficient bytes) | ||||||
|  | 	data = []byte{0x03, 0x01} | ||||||
|  | 	buf = varn.NewBuffer(data) | ||||||
|  | 	_, err = buf.ReadVarBytes() | ||||||
|  | 	assert.ErrorIs(t, err, io.ErrUnexpectedEOF) | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								varn/varn.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								varn/varn.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | // Package varn implements variable-length encoding for unsigned integers and | ||||||
|  | // byte slices. It is used in the OpenTimestamps protocol to encode | ||||||
|  | // instructions and attestations. The encoding is similar to the one used in | ||||||
|  | // Protocol Buffers, but with a different format for the variable-length | ||||||
|  | // integers. The encoding is designed to be compact and efficient, while still | ||||||
|  | // being easy to decode. The package provides functions to read and write | ||||||
|  | // variable-length integers and byte slices, as well as a Buffer type for | ||||||
|  | // reading and writing data in a more convenient way. The package is not | ||||||
|  | // thread-safe and should not be used concurrently. It is intended for use in | ||||||
|  | // the OpenTimestamps protocol and is not a general-purpose encoding library. | ||||||
|  | package varn | ||||||
|  |  | ||||||
|  | // AppendVarUint appends a variable-length unsigned integer to the buffer | ||||||
|  | func AppendVarUint(buf []byte, value uint64) []byte { | ||||||
|  | 	if value == 0 { | ||||||
|  | 		buf = append(buf, 0) | ||||||
|  | 	} else { | ||||||
|  | 		for value != 0 { | ||||||
|  | 			b := byte(value & 0b01111111) | ||||||
|  | 			if value > 0b01111111 { | ||||||
|  | 				b |= 0b10000000 | ||||||
|  | 			} | ||||||
|  | 			buf = append(buf, b) | ||||||
|  | 			if value <= 0b01111111 { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			value >>= 7 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return buf | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func AppendVarBytes(buf []byte, value []byte) []byte { | ||||||
|  | 	buf = AppendVarUint(buf, uint64(len(value))) | ||||||
|  | 	buf = append(buf, value...) | ||||||
|  | 	return buf | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								varn/varn_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								varn/varn_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | package varn_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
|  |  | ||||||
|  | 	"git.intruders.space/public/opentimestamps/varn" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Helper functions for decoding (to verify encoding) | ||||||
|  | func readVarUint(t *testing.T, data []byte) (uint64, int) { | ||||||
|  | 	t.Helper() | ||||||
|  | 	require.NotZero(t, data, "empty buffer") | ||||||
|  |  | ||||||
|  | 	if data[0] == 0 { | ||||||
|  | 		return 0, 1 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var value uint64 | ||||||
|  | 	var bytesRead int | ||||||
|  |  | ||||||
|  | 	for i, b := range data { | ||||||
|  | 		bytesRead = i + 1 | ||||||
|  | 		value |= uint64(b&0x7F) << uint(7*i) | ||||||
|  | 		if b&0x80 == 0 { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		require.Less(t, i, 9, "varint too long") // 9 is max bytes needed for uint64 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return value, bytesRead | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func readVarBytes(t *testing.T, data []byte) ([]byte, int) { | ||||||
|  | 	t.Helper() | ||||||
|  | 	length, bytesRead := readVarUint(t, data) | ||||||
|  |  | ||||||
|  | 	require.GreaterOrEqual(t, uint64(len(data)-bytesRead), length, "buffer too short") | ||||||
|  |  | ||||||
|  | 	end := bytesRead + int(length) | ||||||
|  | 	return data[bytesRead:end], end | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAppendVarUint(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name     string | ||||||
|  | 		value    uint64 | ||||||
|  | 		expected string // hex representation of expected bytes | ||||||
|  | 	}{ | ||||||
|  | 		{"zero", 0, "00"}, | ||||||
|  | 		{"small value", 42, "2a"}, | ||||||
|  | 		{"value below 128", 127, "7f"}, | ||||||
|  | 		{"value requiring 2 bytes", 128, "8001"}, | ||||||
|  | 		{"medium value", 300, "ac02"}, | ||||||
|  | 		{"large value", 1234567890, "d285d8cc04"}, // Updated from "d2a6e58e07" to correct encoding | ||||||
|  | 		{"max uint64", 18446744073709551615, "ffffffffffffffffff01"}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			var buf []byte | ||||||
|  | 			result := varn.AppendVarUint(buf, tt.value) | ||||||
|  |  | ||||||
|  | 			// Check encoding | ||||||
|  | 			expected, err := hex.DecodeString(tt.expected) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			assert.Equal(t, expected, result) | ||||||
|  |  | ||||||
|  | 			// Verify by decoding | ||||||
|  | 			decoded, _ := readVarUint(t, result) | ||||||
|  | 			assert.Equal(t, tt.value, decoded) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAppendVarBytes(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name  string | ||||||
|  | 		value []byte | ||||||
|  | 	}{ | ||||||
|  | 		{"empty", []byte{}}, | ||||||
|  | 		{"small", []byte{0x01, 0x02, 0x03}}, | ||||||
|  | 		{"medium", bytes.Repeat([]byte{0xAA}, 127)}, | ||||||
|  | 		{"large", bytes.Repeat([]byte{0xBB}, 256)}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			var buf []byte | ||||||
|  | 			result := varn.AppendVarBytes(buf, tt.value) | ||||||
|  |  | ||||||
|  | 			// Verify by decoding | ||||||
|  | 			decoded, _ := readVarBytes(t, result) | ||||||
|  | 			assert.Equal(t, tt.value, decoded) | ||||||
|  |  | ||||||
|  | 			// Check that length is properly encoded | ||||||
|  | 			lenEncoded, _ := readVarUint(t, result) | ||||||
|  | 			assert.Equal(t, uint64(len(tt.value)), lenEncoded) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAppendingToExistingBuffer(t *testing.T) { | ||||||
|  | 	existing := []byte{0xFF, 0xFF} | ||||||
|  |  | ||||||
|  | 	// Test AppendVarUint | ||||||
|  | 	result := varn.AppendVarUint(existing, 42) | ||||||
|  | 	assert.Equal(t, []byte{0xFF, 0xFF, 0x2A}, result) | ||||||
|  |  | ||||||
|  | 	// Test AppendVarBytes | ||||||
|  | 	data := []byte{0x01, 0x02, 0x03} | ||||||
|  | 	result = varn.AppendVarBytes(existing, data) | ||||||
|  | 	assert.Equal(t, []byte{0xFF, 0xFF, 0x03, 0x01, 0x02, 0x03}, result) | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								verifier.go
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								verifier.go
									
									
									
									
									
								
							| @@ -1,45 +0,0 @@ | |||||||
| package opentimestamps |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	"github.com/btcsuite/btcd/chaincfg/chainhash" |  | ||||||
| 	"github.com/btcsuite/btcd/wire" |  | ||||||
| 	"golang.org/x/exp/slices" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type Bitcoin interface { |  | ||||||
| 	GetBlockHash(height int64) (*chainhash.Hash, error) |  | ||||||
| 	GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (seq Sequence) Verify(bitcoin Bitcoin, initial []byte) error { |  | ||||||
| 	if len(seq) == 0 { |  | ||||||
| 		return fmt.Errorf("empty sequence") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	att := seq[len(seq)-1] |  | ||||||
| 	if att.Attestation == nil || att.BitcoinBlockHeight == 0 { |  | ||||||
| 		return fmt.Errorf("sequence doesn't include a bitcoin attestation") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to get block %d hash: %w", att.BitcoinBlockHeight, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	blockHeader, err := bitcoin.GetBlockHeader(blockHash) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to get block %s header: %w", blockHash, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	merkleRoot := blockHeader.MerkleRoot[:] |  | ||||||
|  |  | ||||||
| 	result := seq.Compute(initial) |  | ||||||
| 	if !slices.Equal(result, merkleRoot) { |  | ||||||
| 		return fmt.Errorf("sequence result '%x' doesn't match the bitcoin merkle root for block %d: %x", |  | ||||||
| 			result, att.BitcoinBlockHeight, merkleRoot) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
							
								
								
									
										15
									
								
								verifyer/bitcoin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								verifyer/bitcoin.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | package verifyer | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/btcsuite/btcd/chaincfg/chainhash" | ||||||
|  | 	"github.com/btcsuite/btcd/rpcclient" | ||||||
|  | 	"github.com/btcsuite/btcd/wire" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Bitcoin interface { | ||||||
|  | 	GetBlockHash(height int64) (*chainhash.Hash, error) | ||||||
|  | 	GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ Bitcoin = (*esplora)(nil) | ||||||
|  | var _ Bitcoin = (*rpcclient.Client)(nil) | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package opentimestamps | package verifyer | ||||||
| 
 | 
 | ||||||
| import "github.com/btcsuite/btcd/rpcclient" | import "github.com/btcsuite/btcd/rpcclient" | ||||||
| 
 | 
 | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package opentimestamps | package verifyer | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| @@ -8,35 +8,46 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"slices" | ||||||
| 
 | 
 | ||||||
| 	"github.com/btcsuite/btcd/chaincfg/chainhash" | 	"github.com/btcsuite/btcd/chaincfg/chainhash" | ||||||
| 	"github.com/btcsuite/btcd/wire" | 	"github.com/btcsuite/btcd/wire" | ||||||
| 	"golang.org/x/exp/slices" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func NewEsploraClient(url string) Bitcoin { | // esplora is a client for the Esplora API, which provides access to Bitcoin | ||||||
| 	if strings.HasSuffix(url, "/") { | // blockchain data. | ||||||
| 		url = url[0 : len(url)-1] | type esplora struct { | ||||||
| 	} | 	httpClient *http.Client | ||||||
| 	return esplora{url} | 	baseURL    string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type esplora struct{ baseurl string } | func NewEsploraClient(url string, timeout time.Duration) Bitcoin { | ||||||
|  | 	url = strings.TrimSuffix(url, "/") | ||||||
|  | 
 | ||||||
|  | 	h := &http.Client{ | ||||||
|  | 		Timeout: timeout, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return esplora{h, url} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| func (e esplora) GetBlockHash(height int64) (*chainhash.Hash, error) { | func (e esplora) GetBlockHash(height int64) (*chainhash.Hash, error) { | ||||||
| 	resp, err := http.Get(e.baseurl + "/block-height/" + strconv.FormatInt(height, 10)) | 	resp, err := e.httpClient.Get(e.baseURL + "/block-height/" + strconv.FormatInt(height, 10)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, fmt.Errorf("failed to get block hash: %w", err) | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
| 	hexb, err := io.ReadAll(resp.Body) | 	hexb, err := io.ReadAll(resp.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, fmt.Errorf("failed to read block hash: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	hash, err := hex.DecodeString(string(hexb)) | 	hash, err := hex.DecodeString(string(hexb)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, fmt.Errorf("failed to decode block hash: %w", err) | ||||||
| 	} | 	} | ||||||
| 	if len(hash) != chainhash.HashSize { | 	if len(hash) != chainhash.HashSize { | ||||||
| 		return nil, fmt.Errorf("got block hash (%x) of invalid size (expected %d)", hash, chainhash.HashSize) | 		return nil, fmt.Errorf("got block hash (%x) of invalid size (expected %d)", hash, chainhash.HashSize) | ||||||
| @@ -49,11 +60,12 @@ func (e esplora) GetBlockHash(height int64) (*chainhash.Hash, error) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (e esplora) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) { | func (e esplora) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) { | ||||||
| 	resp, err := http.Get(fmt.Sprintf("%s/block/%s/header", e.baseurl, hash.String())) | 	resp, err := e.httpClient.Get(fmt.Sprintf("%s/block/%s/header", e.baseURL, hash.String())) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
| 	hexb, err := io.ReadAll(resp.Body) | 	hexb, err := io.ReadAll(resp.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
		Reference in New Issue
	
	Block a user