This commit is contained in:
2025-04-11 13:31:02 +02:00
parent ba1196a962
commit 03f30f1968
48 changed files with 1685 additions and 474 deletions

203
README.md
View File

@@ -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 UpgradeSequence() to get the upgraded sequence (or an error if not yet available) func main() {
upgradedSeq, err := opentimestamps.UpgradeSequence(context.Background(), seq, 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 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
View 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
View 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
View 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
View File

@@ -0,0 +1,13 @@
package main
import (
"fmt"
"os"
)
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

64
cmd/ots/root.go Normal file
View File

@@ -0,0 +1,64 @@
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)
}
// 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
View 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))
}

155
cmd/ots/verify.go Normal file
View File

@@ -0,0 +1,155 @@
package main
import (
"crypto/sha256"
"log/slog"
"os"
"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()
slog.Info("Verification successful",
"block_height", att.BitcoinBlockHeight)
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")
}

View File

@@ -1,2 +0,0 @@
The timestamp on this file is well-formatted, but will fail Bitcoin block
header validation.

Binary file not shown.

View File

Binary file not shown.

View File

@@ -1 +0,0 @@
Hello World!

Binary file not shown.

View File

@@ -1 +0,0 @@
The timestamp on this file is incomplete, and can be upgraded.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
This file's timestamp has two attestations, one from a known notary, and one
from an unknown notary.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -1 +0,0 @@
This file has an (incomplete) timestamp with two different calendars.

Binary file not shown.

View File

@@ -1 +0,0 @@
This file's timestamp has a single attestation from an unknown notary.

Binary file not shown.

View File

@@ -0,0 +1 @@
earth is flat

Binary file not shown.

30
go.mod
View File

@@ -1,21 +1,27 @@
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/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
) )

57
go.sum
View File

@@ -1,18 +1,25 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
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 +30,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,10 +51,15 @@ 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=
@@ -59,13 +75,26 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
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/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/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.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/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=
@@ -81,8 +110,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=
@@ -95,11 +124,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
View 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"
}
}

View File

@@ -1,4 +1,4 @@
package opentimestamps package ots
import ( import (
deprecated_ripemd160 "golang.org/x/crypto/ripemd160" deprecated_ripemd160 "golang.org/x/crypto/ripemd160"

View File

@@ -1,59 +1,23 @@
package opentimestamps package ots
import ( import (
"bytes"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"slices" "slices"
"strings" "strings"
"github.com/btcsuite/btcd/wire" "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.
@@ -62,53 +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) 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
}
func (ts File) GetPendingSequences() []Sequence { func (ts File) GetPendingSequences() []Sequence {
bitcoin := ts.GetBitcoinAttestedSequences() bitcoin := ts.GetBitcoinAttestedSequences()
@@ -182,8 +99,8 @@ func (ts File) Human(withPartials bool) 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()...)
@@ -203,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
@@ -236,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
@@ -245,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))
@@ -263,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
} }

View File

@@ -1,11 +1,32 @@
package opentimestamps package ots
import ( import (
"fmt"
"slices"
"strings" "strings"
"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
View 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") }},
}

View File

@@ -1,66 +1,88 @@
package opentimestamps package ots
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"slices" "slices"
"git.intruders.space/public/opentimestamps/varn"
"git.intruders.space/public/opentimestamps/verifyer"
"github.com/btcsuite/btcd/wire"
) )
func parseCalendarServerResponse(buf Buffer) (Sequence, error) { type Sequence []Instruction
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 (seq Sequence) GetAttestation() Attestation {
if len(seq) == 0 {
return Attestation{}
}
att := seq[len(seq)-1]
if att.Attestation == nil {
return Attestation{}
}
return *att.Attestation
} }
func parseOTSFile(buf Buffer) (*File, error) { // Compute runs a sequence of operations on top of an initial digest and returns the result, which is often a
// read magic // Bitcoin block merkle root. It also tries to identify the point in the sequence in which an actual Bitcoin
// read version [1 byte] // transaction is formed and parse that.
// read crypto operation for file digest [1 byte] func (seq Sequence) Compute(initial []byte) (merkleRoot []byte, bitcoinTx *wire.MsgTx) {
// read file digest [32 byte (depends)] current := initial
if magic, err := buf.readBytes(len(headerMagic)); err != nil || !slices.Equal(headerMagic, magic) { for i, inst := range seq {
return nil, fmt.Errorf("invalid ots file header '%s': %w", magic, err) if inst.Operation == nil {
} break
}
if version, err := buf.readVarUint(); err != nil || version != 1 { // the first time we do a double-sha256 that is likely a bitcoin transaction
return nil, fmt.Errorf("invalid ots file version '%v': %w", version, err) 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
}
tag, err := buf.readByte() current = inst.Operation.Apply(current, inst.Argument)
if err != nil {
return nil, fmt.Errorf("failed to read operation byte: %w", err)
} }
return current, bitcoinTx
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) { // 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 // read instructions
// if operation = push // if operation = push
// if 0x00 = attestation // if 0x00 = attestation
@@ -83,7 +105,7 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
// go read these tags // go read these tags
for { for {
tag, err := buf.readByte() tag, err := buf.ReadByte()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
return seqs, nil return seqs, nil
@@ -93,20 +115,20 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
if tag == 0x00 { if tag == 0x00 {
// enter an attestation context // enter an attestation context
magic, err := buf.readBytes(8) magic, err := buf.ReadBytes(8)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read attestion magic bytes: %w", err) return nil, fmt.Errorf("failed to read attestion magic bytes: %w", err)
} }
this, err := buf.readVarBytes() this, err := buf.ReadVarBytes()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read attestation bytes: %w", err) return nil, fmt.Errorf("failed to read attestation bytes: %w", err)
} }
abuf := newBuffer(this) abuf := varn.NewBuffer(this)
switch { switch {
case slices.Equal(magic, pendingMagic): case slices.Equal(magic, PendingMagic):
val, err := abuf.readVarBytes() val, err := abuf.ReadVarBytes()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed reading calendar server url: %w", err) return nil, fmt.Errorf("failed reading calendar server url: %w", err)
} }
@@ -114,8 +136,8 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
seqs[currInstructionsBlock], seqs[currInstructionsBlock],
Instruction{Attestation: &Attestation{CalendarServerURL: string(val)}}, Instruction{Attestation: &Attestation{CalendarServerURL: string(val)}},
) )
case slices.Equal(magic, bitcoinMagic): case slices.Equal(magic, BitcoinMagic):
val, err := abuf.readVarUint() val, err := abuf.ReadVarUint()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed reading bitcoin block number: %w", err) return nil, fmt.Errorf("failed reading bitcoin block number: %w", err)
} }
@@ -144,7 +166,7 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
checkpoints = append(checkpoints, chp) checkpoints = append(checkpoints, chp)
} else { } else {
// a new operation in this block // a new operation in this block
inst, err := readInstruction(buf, tag) inst, err := ReadInstruction(buf, tag)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read instruction: %w", err) return nil, fmt.Errorf("failed to read instruction: %w", err)
} }
@@ -152,24 +174,3 @@ func parseTimestamp(buf Buffer) ([]Sequence, error) {
} }
} }
} }
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
}

View File

@@ -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,11 +44,11 @@ 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 UpgradeSequence(ctx context.Context, seq Sequence, 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.GetAttestation() attestation := seq.GetAttestation()
@@ -54,7 +61,8 @@ func UpgradeSequence(ctx context.Context, seq Sequence, initial []byte) (Sequenc
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,14 +77,34 @@ func UpgradeSequence(ctx context.Context, seq Sequence, initial []byte) (Sequenc
} }
resp.Body.Close() resp.Body.Close()
tail, 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(Sequence, len(seq)+len(tail)-1) newSeq := make(ots.Sequence, len(seq)+len(tail)-1)
copy(newSeq, seq[0:len(seq)-1]) copy(newSeq, seq[0:len(seq)-1])
copy(newSeq[len(seq)-1:], tail) 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
View File

@@ -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
View 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
View 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
View 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
View 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)
}

View File

@@ -1,47 +0,0 @@
package opentimestamps
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)
type Bitcoin interface {
GetBlockHash(height int64) (*chainhash.Hash, error)
GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error)
}
// 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 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
}

15
verifyer/bitcoin.go Normal file
View 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)

View File

@@ -1,4 +1,4 @@
package opentimestamps package verifyer
import "github.com/btcsuite/btcd/rpcclient" import "github.com/btcsuite/btcd/rpcclient"

View File

@@ -1,4 +1,4 @@
package opentimestamps package verifyer
import ( import (
"bytes" "bytes"
@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"slices" "slices"
@@ -15,29 +16,38 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
) )
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)
@@ -50,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