refactor
This commit is contained in:
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 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
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
cmd/ots/root.go
Normal file
64
cmd/ots/root.go
Normal 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
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))
|
||||||
|
}
|
||||||
155
cmd/ots/verify.go
Normal file
155
cmd/ots/verify.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -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.ots
Normal file
BIN
fixtures/flatearthers-united.txt.ots
Normal file
Binary file not shown.
30
go.mod
30
go.mod
@@ -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
57
go.sum
@@ -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
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,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
|
||||||
}
|
}
|
||||||
@@ -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
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") }},
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
46
stamp.go
46
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,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
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)
|
||||||
|
}
|
||||||
47
verifier.go
47
verifier.go
@@ -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
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,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
|
||||||
Reference in New Issue
Block a user