Add coinbase airdrop proof validation
This commit is contained in:
parent
1aa20aecd4
commit
eaaa398c34
4 changed files with 493 additions and 5 deletions
358
pkg/covenant/airdrop_proof.go
Normal file
358
pkg/covenant/airdrop_proof.go
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package covenant
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
const (
|
||||
airdropReward = 4246994314
|
||||
airdropLeaves = 216199
|
||||
airdropDepth = 18
|
||||
airdropSubdepth = 3
|
||||
airdropSubleaves = 8
|
||||
faucetDepth = 11
|
||||
faucetLeaves = 1358
|
||||
maxAirdropProofLen = 3400
|
||||
airdropKeyAddress = 4
|
||||
)
|
||||
|
||||
type airdropProof struct {
|
||||
index uint32
|
||||
proof [][]byte
|
||||
subindex uint8
|
||||
subproof [][]byte
|
||||
key []byte
|
||||
version uint8
|
||||
address []byte
|
||||
fee uint64
|
||||
signature []byte
|
||||
}
|
||||
|
||||
type airdropKey struct {
|
||||
version uint8
|
||||
address []byte
|
||||
value uint64
|
||||
}
|
||||
|
||||
func decodeAirdropProof(data []byte) (*airdropProof, error) {
|
||||
if len(data) > maxAirdropProofLen {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "proof too large", nil)
|
||||
}
|
||||
|
||||
proof := &airdropProof{}
|
||||
var n int
|
||||
var err error
|
||||
|
||||
if proof.index, n, err = readProofU32(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode index", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
var count uint8
|
||||
if count, n, err = readProofU8(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode proof length", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if int(count) > airdropDepth {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "invalid proof depth", nil)
|
||||
}
|
||||
|
||||
proof.proof = make([][]byte, 0, count)
|
||||
for i := 0; i < int(count); i++ {
|
||||
var hash []byte
|
||||
if hash, n, err = readProofBytes(data, 32); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode proof hash", err)
|
||||
}
|
||||
data = data[n:]
|
||||
proof.proof = append(proof.proof, hash)
|
||||
}
|
||||
|
||||
if proof.subindex, n, err = readProofU8(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode subindex", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
var total uint8
|
||||
if total, n, err = readProofU8(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode subproof length", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if int(total) > airdropSubdepth {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "invalid subproof depth", nil)
|
||||
}
|
||||
|
||||
proof.subproof = make([][]byte, 0, total)
|
||||
for i := 0; i < int(total); i++ {
|
||||
var hash []byte
|
||||
if hash, n, err = readProofBytes(data, 32); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode subproof hash", err)
|
||||
}
|
||||
data = data[n:]
|
||||
proof.subproof = append(proof.subproof, hash)
|
||||
}
|
||||
|
||||
if proof.key, n, err = readProofVarBytes(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode key", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if len(proof.key) == 0 {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "missing key", nil)
|
||||
}
|
||||
|
||||
if proof.version, n, err = readProofU8(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode version", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
var size uint8
|
||||
if size, n, err = readProofU8(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode address length", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if size < 2 || size > 40 {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "invalid address length", nil)
|
||||
}
|
||||
|
||||
if proof.address, n, err = readProofBytes(data, int(size)); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode address", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if proof.fee, n, err = readProofVarint(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode fee", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if proof.signature, n, err = readProofVarBytes(data); err != nil {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "failed to decode signature", err)
|
||||
}
|
||||
data = data[n:]
|
||||
|
||||
if len(data) != 0 {
|
||||
return nil, core.E("covenant.decodeAirdropProof", "trailing data", nil)
|
||||
}
|
||||
|
||||
return proof, nil
|
||||
}
|
||||
|
||||
func (p *airdropProof) isAddress() bool {
|
||||
return len(p.key) > 0 && p.key[0] == airdropKeyAddress
|
||||
}
|
||||
|
||||
func (p *airdropProof) getValue() uint64 {
|
||||
if !p.isAddress() {
|
||||
return airdropReward
|
||||
}
|
||||
|
||||
key, ok := p.getKey()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
return key.value
|
||||
}
|
||||
|
||||
func (p *airdropProof) getKey() (airdropKey, bool) {
|
||||
if !p.isAddress() {
|
||||
return airdropKey{}, false
|
||||
}
|
||||
|
||||
if len(p.key) < 1+1+1+8+1 {
|
||||
return airdropKey{}, false
|
||||
}
|
||||
|
||||
key := airdropKey{}
|
||||
key.version = p.key[1]
|
||||
|
||||
addrLen := int(p.key[2])
|
||||
if addrLen < 2 || addrLen > 40 {
|
||||
return airdropKey{}, false
|
||||
}
|
||||
|
||||
want := 1 + 1 + 1 + addrLen + 8 + 1
|
||||
if len(p.key) != want {
|
||||
return airdropKey{}, false
|
||||
}
|
||||
|
||||
key.address = append([]byte(nil), p.key[3:3+addrLen]...)
|
||||
key.value = binary.LittleEndian.Uint64(p.key[3+addrLen : 3+addrLen+8])
|
||||
return key, true
|
||||
}
|
||||
|
||||
func (p *airdropProof) isSane() bool {
|
||||
if len(p.key) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.version > 31 {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.address) < 2 || len(p.address) > 40 {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.isAddress() {
|
||||
if _, ok := p.getKey(); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
value := p.getValue()
|
||||
|
||||
if p.fee > value {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.isAddress() {
|
||||
if len(p.subproof) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.subindex != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.proof) > faucetDepth {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.index >= faucetLeaves {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if len(p.subproof) > airdropSubdepth {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.subindex >= airdropSubleaves {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.proof) > airdropDepth {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.index >= airdropLeaves {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.size() > maxAirdropProofLen {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *airdropProof) size() int {
|
||||
size := 0
|
||||
size += 4
|
||||
size += 1
|
||||
size += len(p.proof) * 32
|
||||
size += 1
|
||||
size += 1
|
||||
size += len(p.subproof) * 32
|
||||
size += sizeProofVarBytes(len(p.key))
|
||||
size += 1
|
||||
size += 1
|
||||
size += len(p.address)
|
||||
size += sizeProofVarint(p.fee)
|
||||
size += sizeProofVarBytes(len(p.signature))
|
||||
return size
|
||||
}
|
||||
|
||||
func readProofU8(src []byte) (uint8, int, error) {
|
||||
if len(src) < 1 {
|
||||
return 0, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
|
||||
return src[0], 1, nil
|
||||
}
|
||||
|
||||
func readProofU32(src []byte) (uint32, int, error) {
|
||||
if len(src) < 4 {
|
||||
return 0, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(src[:4]), 4, nil
|
||||
}
|
||||
|
||||
func readProofBytes(src []byte, n int) ([]byte, int, error) {
|
||||
if n < 0 || len(src) < n {
|
||||
return nil, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
|
||||
buf := make([]byte, n)
|
||||
copy(buf, src[:n])
|
||||
return buf, n, nil
|
||||
}
|
||||
|
||||
func readProofVarint(src []byte) (uint64, int, error) {
|
||||
if len(src) == 0 {
|
||||
return 0, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
|
||||
switch prefix := src[0]; {
|
||||
case prefix < 0xfd:
|
||||
return uint64(prefix), 1, nil
|
||||
case prefix == 0xfd:
|
||||
if len(src) < 3 {
|
||||
return 0, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
return uint64(binary.LittleEndian.Uint16(src[1:3])), 3, nil
|
||||
case prefix == 0xfe:
|
||||
if len(src) < 5 {
|
||||
return 0, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
return uint64(binary.LittleEndian.Uint32(src[1:5])), 5, nil
|
||||
default:
|
||||
if len(src) < 9 {
|
||||
return 0, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
return binary.LittleEndian.Uint64(src[1:9]), 9, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readProofVarBytes(src []byte) ([]byte, int, error) {
|
||||
n, consumed, err := readProofVarint(src)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if uint64(len(src[consumed:])) < n {
|
||||
return nil, 0, core.E("covenant.decodeAirdropProof", "short buffer", nil)
|
||||
}
|
||||
|
||||
end := consumed + int(n)
|
||||
buf := make([]byte, int(n))
|
||||
copy(buf, src[consumed:end])
|
||||
return buf, end, nil
|
||||
}
|
||||
|
||||
func sizeProofVarint(n uint64) int {
|
||||
switch {
|
||||
case n < 0xfd:
|
||||
return 1
|
||||
case n <= 0xffff:
|
||||
return 3
|
||||
case n <= 0xffffffff:
|
||||
return 5
|
||||
default:
|
||||
return 9
|
||||
}
|
||||
}
|
||||
|
||||
func sizeProofVarBytes(n int) int {
|
||||
return sizeProofVarint(uint64(n)) + n
|
||||
}
|
||||
|
|
@ -148,6 +148,11 @@ func HasSaneCovenants(tx primitives.Transaction) bool {
|
|||
if len(input.Witness) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
proof, err := decodeAirdropProof(input.Witness[0])
|
||||
if err != nil || !proof.isSane() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case TypeClaim:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package covenant
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
|
|
@ -170,7 +171,7 @@ func TestHasSaneCovenants(t *testing.T) {
|
|||
Outputs: []primitives.Output{
|
||||
{
|
||||
Covenant: primitives.Covenant{
|
||||
Type: 99,
|
||||
Type: 99,
|
||||
Items: make([][]byte, 256),
|
||||
},
|
||||
},
|
||||
|
|
@ -487,3 +488,100 @@ func TestVerifyCovenantsCoinbaseLinkedOutputs(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func encodeTestVarint(n uint64) []byte {
|
||||
switch {
|
||||
case n < 0xfd:
|
||||
return []byte{byte(n)}
|
||||
case n <= 0xffff:
|
||||
return []byte{0xfd, byte(n), byte(n >> 8)}
|
||||
case n <= 0xffffffff:
|
||||
return []byte{0xfe, byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
|
||||
default:
|
||||
return []byte{
|
||||
0xff,
|
||||
byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24),
|
||||
byte(n >> 32), byte(n >> 40), byte(n >> 48), byte(n >> 56),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func encodeTestAirdropKey(version uint8, address []byte, value uint64, sponsor bool) []byte {
|
||||
key := []byte{4, version, byte(len(address))}
|
||||
key = append(key, address...)
|
||||
|
||||
var tmp [8]byte
|
||||
binary.LittleEndian.PutUint64(tmp[:], value)
|
||||
key = append(key, tmp[:]...)
|
||||
if sponsor {
|
||||
key = append(key, 1)
|
||||
} else {
|
||||
key = append(key, 0)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func encodeTestAirdropProof(key, address []byte, fee uint64) []byte {
|
||||
var out []byte
|
||||
var index [4]byte
|
||||
binary.LittleEndian.PutUint32(index[:], 3)
|
||||
out = append(out, index[:]...)
|
||||
out = append(out, 0) // proof length
|
||||
out = append(out, 0) // subindex
|
||||
out = append(out, 0) // subproof length
|
||||
out = appendTestVarBytes(out, key)
|
||||
out = append(out, 0) // version
|
||||
out = append(out, byte(len(address)))
|
||||
out = append(out, address...)
|
||||
out = append(out, encodeTestVarint(fee)...)
|
||||
out = appendTestVarBytes(out, nil)
|
||||
return out
|
||||
}
|
||||
|
||||
func appendTestVarBytes(dst, src []byte) []byte {
|
||||
dst = append(dst, encodeTestVarint(uint64(len(src)))...)
|
||||
return append(dst, src...)
|
||||
}
|
||||
|
||||
func TestCoinbaseAirdropProofValidation(t *testing.T) {
|
||||
address := bytes.Repeat([]byte{0x42}, 20)
|
||||
key := encodeTestAirdropKey(0, address, 1000, false)
|
||||
proof := encodeTestAirdropProof(key, address, 10)
|
||||
|
||||
tx := primitives.Transaction{
|
||||
Inputs: []primitives.Input{
|
||||
{Prevout: primitives.NewOutpoint()},
|
||||
{
|
||||
Prevout: primitives.Outpoint{TxHash: primitives.Hash{4}, Index: 0},
|
||||
Witness: [][]byte{proof},
|
||||
},
|
||||
},
|
||||
Outputs: []primitives.Output{
|
||||
{Covenant: primitives.Covenant{Type: uint8(TypeNone)}},
|
||||
{
|
||||
Value: 990,
|
||||
Address: primitives.Address{
|
||||
Version: 0,
|
||||
Hash: append([]byte(nil), address...),
|
||||
},
|
||||
Covenant: primitives.Covenant{Type: uint8(TypeNone)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !HasSaneCovenants(tx) {
|
||||
t.Fatal("HasSaneCovenants should accept a valid coinbase airdrop proof")
|
||||
}
|
||||
|
||||
if got := VerifyCovenants(tx, testCoinView{coins: map[primitives.Outpoint]primitives.Output{}}, 100, Network{}); got != 0 {
|
||||
t.Fatalf("VerifyCovenants returned %d for a valid coinbase airdrop proof, want 0", got)
|
||||
}
|
||||
|
||||
badTx := tx
|
||||
badTx.Outputs[1].Value = 991
|
||||
|
||||
if got := VerifyCovenants(badTx, testCoinView{coins: map[primitives.Outpoint]primitives.Output{}}, 100, Network{}); got != -1 {
|
||||
t.Fatalf("VerifyCovenants returned %d for a mismatched coinbase airdrop value, want -1", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,15 +33,16 @@ type Network struct {
|
|||
//
|
||||
// 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 conservatively validates the
|
||||
// coinbase-linked covenant shape without decoding the higher-level proof
|
||||
// formats that live in the wider blockchain stack.
|
||||
// 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
|
||||
|
|
@ -57,7 +58,33 @@ func VerifyCovenants(tx primitives.Transaction, view CoinView, height uint32, ne
|
|||
|
||||
switch cov.Type {
|
||||
case uint8(TypeNone):
|
||||
continue
|
||||
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):
|
||||
blockHeight, err := cov.GetU32(1)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue