go-lns/pkg/covenant/verify.go
2026-04-04 08:07:34 +00:00

337 lines
6.5 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"bytes"
"dappco.re/go/lns/pkg/primitives"
)
// maxScriptStack mirrors the JS consensus limit used for covenant sanity checks.
const maxScriptStack = 1000
// CoinView is the minimal lookup interface required by VerifyCovenants.
//
// It returns the previous output for an outpoint, which is sufficient for the
// covenant transition checks implemented in this package.
type CoinView interface {
GetOutput(primitives.Outpoint) (primitives.Output, bool)
}
// Network captures the covenant rollout inputs used by VerifyCovenants.
//
// The structure mirrors the `network.names` and `network.deflationHeight`
// fields accessed by the JS reference implementation while keeping the Go API
// self-contained.
type Network struct {
Names NameRules
DeflationHeight uint32
}
// VerifyCovenants performs contextual covenant verification for a transaction.
//
// The function mirrors the JS reference return convention: 0 on success and
// -1 on failure. The Go port covers the covenant transitions that depend only
// on the transaction and coin view, and it decodes the coinbase airdrop proof
// shape needed for the wire-level sanity checks implemented here.
func VerifyCovenants(tx primitives.Transaction, view CoinView, height uint32, network Network) int {
if !HasSaneCovenants(tx) {
return -1
}
if isCoinbaseTx(tx) {
var conjured uint64
for i := 1; i < len(tx.Inputs); i++ {
if i >= len(tx.Outputs) {
return -1
}
input := tx.Inputs[i]
output := tx.Outputs[i]
cov := output.Covenant
if len(input.Witness) != 1 {
return -1
}
switch cov.Type {
case uint8(TypeNone):
proof, err := decodeAirdropProof(input.Witness[0])
if err != nil || !proof.isSane() {
return -1
}
value := proof.getValue()
if value < proof.fee {
return -1
}
if output.Value != value-proof.fee {
return -1
}
if output.Address.Version != proof.version {
return -1
}
if !bytes.Equal(output.Address.Hash, proof.address) {
return -1
}
if value > ^uint64(0)-conjured {
return -1
}
conjured += value
case uint8(TypeClaim):
var claim primitives.Claim
if err := claim.UnmarshalBinary(input.Witness[0]); err != nil {
return -1
}
blockHeight, err := cov.GetU32(1)
if err != nil || blockHeight != height {
return -1
}
if output.Value > ^uint64(0)-conjured {
return -1
}
conjured += output.Value
default:
return -1
}
}
return 0
}
if view == nil {
return -1
}
for i, input := range tx.Inputs {
coin, ok := view.GetOutput(input.Prevout)
if !ok {
return -1
}
var output *primitives.Output
if i < len(tx.Outputs) {
output = &tx.Outputs[i]
}
uc := coin.Covenant
if output == nil {
switch uc.Type {
case uint8(TypeNone), uint8(TypeOpen), uint8(TypeRedeem):
continue
default:
return -1
}
}
cov := output.Covenant
switch uc.Type {
case uint8(TypeNone), uint8(TypeOpen), uint8(TypeRedeem):
switch cov.Type {
case uint8(TypeNone), uint8(TypeOpen), uint8(TypeBid):
default:
return -1
}
case uint8(TypeBid):
if cov.Type != uint8(TypeReveal) {
return -1
}
if !covenantHashEquals(cov, 0, uc) {
return -1
}
if !covenantU32Equals(cov, 1, uc, 1) {
return -1
}
nonce, err := cov.GetHash(2)
if err != nil {
return -1
}
blind, err := Blind(output.Value, nonce)
if err != nil {
return -1
}
ucBlind, err := uc.GetHash(3)
if err != nil || blind != ucBlind {
return -1
}
if coin.Value < output.Value {
return -1
}
case uint8(TypeClaim), uint8(TypeReveal):
switch cov.Type {
case uint8(TypeRegister):
if !covenantHashEquals(cov, 0, uc) {
return -1
}
if !covenantU32Equals(cov, 1, uc, 1) {
return -1
}
if !output.Address.Equals(coin.Address) {
return -1
}
case uint8(TypeRedeem):
if !covenantHashEquals(cov, 0, uc) {
return -1
}
if !covenantU32Equals(cov, 1, uc, 1) {
return -1
}
if uc.Type == uint8(TypeClaim) {
return -1
}
default:
return -1
}
case uint8(TypeRegister), uint8(TypeUpdate), uint8(TypeRenew), uint8(TypeFinalize):
switch cov.Type {
case uint8(TypeUpdate), uint8(TypeRenew), uint8(TypeTransfer), uint8(TypeRevoke):
default:
return -1
}
if output.Value != coin.Value {
return -1
}
if !output.Address.Equals(coin.Address) {
return -1
}
if !covenantHashEquals(cov, 0, uc) {
return -1
}
if !covenantU32Equals(cov, 1, uc, 1) {
return -1
}
case uint8(TypeTransfer):
switch cov.Type {
case uint8(TypeUpdate), uint8(TypeRenew), uint8(TypeRevoke):
if output.Value != coin.Value {
return -1
}
if !output.Address.Equals(coin.Address) {
return -1
}
if !covenantHashEquals(cov, 0, uc) {
return -1
}
if !covenantU32Equals(cov, 1, uc, 1) {
return -1
}
case uint8(TypeFinalize):
if output.Value != coin.Value {
return -1
}
if !covenantHashEquals(cov, 0, uc) {
return -1
}
if !covenantU32Equals(cov, 1, uc, 1) {
return -1
}
version, err := uc.GetU8(2)
if err != nil || output.Address.Version != version {
return -1
}
addr, err := uc.Get(3)
if err != nil || !bytes.Equal(output.Address.Hash, addr) {
return -1
}
default:
return -1
}
case uint8(TypeRevoke):
return -1
default:
if len(cov.Items) > maxScriptStack {
return -1
}
if cov.GetVarSize() > MaxCovenantSize {
return -1
}
if cov.IsName() {
return -1
}
}
}
return 0
}
// GetVerifyCovenants is an alias for VerifyCovenants.
//
// status := covenant.GetVerifyCovenants(tx, view, height, network)
func GetVerifyCovenants(tx primitives.Transaction, view CoinView, height uint32, network Network) int {
return VerifyCovenants(tx, view, height, network)
}
func covenantHashEquals(cov primitives.Covenant, index int, other primitives.Covenant) bool {
hash, err := cov.GetHash(index)
if err != nil {
return false
}
otherHash, err := other.GetHash(index)
if err != nil {
return false
}
return hash == otherHash
}
func covenantU32Equals(left primitives.Covenant, leftIndex int, right primitives.Covenant, rightIndex int) bool {
leftValue, err := left.GetU32(leftIndex)
if err != nil {
return false
}
rightValue, err := right.GetU32(rightIndex)
if err != nil {
return false
}
return leftValue == rightValue
}