feature/1220 origin handle exists method (#1241)

* feat: add docs and CI workflow for publishing to onsonr.dev

* (refactor): Move hway,motr executables to their own repos

* feat: simplify devnet and testnet configurations

* refactor: update import path for didcrypto package

* docs(networks): Add README with project overview, architecture, and community links

* refactor: Move network configurations to deploy directory

* build: update golang version to 1.23

* refactor: move logger interface to appropriate package

* refactor: Move devnet configuration to networks/devnet

* chore: improve release process with date variable

* (chore): Move Crypto Library

* refactor: improve code structure and readability in DID module

* feat: integrate Trunk CI checks

* ci: optimize CI workflow by removing redundant build jobs

---------

Co-authored-by: Darp Alakun <i@prad.nu>
This commit is contained in:
Prad Nukala
2025-01-06 17:06:10 +00:00
committed by GitHub
co-authored by root
parent 9bd5e41fa0
commit 807b2e86ec
483 changed files with 8067 additions and 12559 deletions
+9
View File
@@ -9,6 +9,7 @@ The DID module maintains several key state structures:
### Controller State
The Controller state represents a Sonr DWN Vault. It includes:
- Unique identifier (number)
- DID
- Sonr address
@@ -22,6 +23,7 @@ The Controller state represents a Sonr DWN Vault. It includes:
### Assertion State
The Assertion state includes:
- DID
- Controller
- Subject
@@ -33,6 +35,7 @@ The Assertion state includes:
### Authentication State
The Authentication state includes:
- DID
- Controller
- Subject
@@ -44,6 +47,7 @@ The Authentication state includes:
### Verification State
The Verification state includes:
- DID
- Controller
- DID method
@@ -57,6 +61,7 @@ The Verification state includes:
## State Transitions
State transitions are triggered by the following messages:
- LinkAssertion
- LinkAuthentication
- UnlinkAssertion
@@ -89,6 +94,7 @@ The DID module provides the following query endpoints:
## Params
The module parameters include:
- Allowed public keys (map of KeyInfo)
- Conveyance preference
- Attestation formats
@@ -123,6 +129,7 @@ This module utilizes UCAN (User Controlled Authorization Networks) to provide a
## Future Improvements
Potential future improvements could include:
1. Enhanced privacy features for DID operations, potentially leveraging UCAN capabilities for privacy-preserving authorization.
2. Integration with more blockchain networks
3. Support for additional key types and cryptographic algorithms
@@ -131,6 +138,7 @@ Potential future improvements could include:
## Tests
Acceptance tests should cover all major functionality, including:
- Creating and managing DIDs
- Linking and unlinking assertions and authentications
- Executing transactions with DIDs
@@ -154,6 +162,7 @@ A Verifiable Credential (VC) is a digital statement that can be cryptographicall
### Key Types
The module supports various key types, including:
- Role
- Algorithm (e.g., ES256, EdDSA, ES256K)
- Encoding (e.g., hex, base64, multibase)
+2 -2
View File
@@ -9,8 +9,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
"github.com/onsonr/sonr/internal/chain/transaction"
"github.com/onsonr/sonr/x/did/types/internal/accounts"
"github.com/onsonr/sonr/internal/accounts"
"github.com/onsonr/sonr/internal/transaction"
)
var (
-66
View File
@@ -1,66 +0,0 @@
package address
import (
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
"errors"
"math/big"
"github.com/btcsuite/btcd/btcec/v2"
)
// ComputePublicKey computes the public key of a child key given the extended public key, chain code, coin type, and index.
func ComputePublicKey(extPubKey []byte, chainCode []byte, coinType uint32, index int) ([]byte, error) {
// Check if the index is a hardened child key
if uint32(index) >= HardenedOffset {
return nil, errors.New("cannot derive hardened child key from public key")
}
// Serialize the public key
pubKey, err := btcec.ParsePubKey(extPubKey)
if err != nil {
return nil, err
}
pubKeyBytes := pubKey.SerializeCompressed()
// Serialize the index
indexBytes := make([]byte, 4)
binary.BigEndian.PutUint32(indexBytes, uint32(index))
// Compute the HMAC-SHA512
mac := hmac.New(sha512.New, chainCode)
mac.Write(pubKeyBytes)
mac.Write(indexBytes)
I := mac.Sum(nil)
// Split I into two 32-byte sequences
IL := I[:32]
// Convert IL to a big integer
ilNum := new(big.Int).SetBytes(IL)
// Check if parse256(IL) >= n
curve := btcec.S256()
if ilNum.Cmp(curve.N) >= 0 {
return nil, errors.New("invalid child key")
}
// Compute the child public key: pubKey + IL * G
ilx, ily := curve.ScalarBaseMult(IL)
childX, childY := curve.Add(ilx, ily, pubKey.X(), pubKey.Y())
lx := newBigIntFieldVal(childX)
ly := newBigIntFieldVal(childY)
// Create the child public key
childPubKey := btcec.NewPublicKey(lx, ly)
childPubKeyBytes := childPubKey.SerializeCompressed()
return childPubKeyBytes, nil
}
// newBigIntFieldVal creates a new field value from a big integer.
func newBigIntFieldVal(val *big.Int) *btcec.FieldVal {
lx := new(btcec.FieldVal)
lx.SetByteSlice(val.Bytes())
return lx
}
-18
View File
@@ -1,18 +0,0 @@
package address
type CoinType uint32
const (
// Hardened offset for BIP-44 derivation
HardenedOffset uint32 = 0x80000000
// Registered coin types for BIP-44
CoinTypeBitcoin CoinType = CoinType(0 + HardenedOffset)
CoinTypeEthereum CoinType = CoinType(60 + HardenedOffset)
CoinTypeSonr CoinType = CoinType(703 + HardenedOffset)
)
// Uint32 returns the coin type as a uint32.
func (c CoinType) Uint32() uint32 {
return uint32(c)
}
-231
View File
@@ -1,231 +0,0 @@
package crypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
crypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/multiformats/go-multicodec"
"github.com/multiformats/go-varint"
)
// GenerateEd25519 generates an Ed25519 private key and the matching DID.
// This is the RECOMMENDED algorithm.
func GenerateEd25519() (crypto.PrivKey, DID, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, Undef, nil
}
did, err := FromPubKey(pub)
return priv, did, err
}
// GenerateRSA generates a RSA private key and the matching DID.
func GenerateRSA() (crypto.PrivKey, DID, error) {
// NIST Special Publication 800-57 Part 1 Revision 5
// Section 5.6.1.1 (Table 2)
// Paraphrased: 2048-bit RSA keys are secure until 2030 and 3072-bit keys are recommended for longer-term security.
const keyLength = 3072
priv, pub, err := crypto.GenerateRSAKeyPair(keyLength, rand.Reader)
if err != nil {
return nil, Undef, nil
}
did, err := FromPubKey(pub)
return priv, did, err
}
// GenerateSecp256k1 generates a Secp256k1 private key and the matching DID.
func GenerateSecp256k1() (crypto.PrivKey, DID, error) {
priv, pub, err := crypto.GenerateSecp256k1Key(rand.Reader)
if err != nil {
return nil, Undef, nil
}
did, err := FromPubKey(pub)
return priv, did, err
}
// GenerateECDSA generates an ECDSA private key and the matching DID
// for the default P256 curve.
func GenerateECDSA() (crypto.PrivKey, DID, error) {
return GenerateECDSAWithCurve(P256)
}
// GenerateECDSAWithCurve generates an ECDSA private key and matching
// DID for the user-supplied curve
func GenerateECDSAWithCurve(code multicodec.Code) (crypto.PrivKey, DID, error) {
var curve elliptic.Curve
switch code {
case P256:
curve = elliptic.P256()
case P384:
curve = elliptic.P384()
case P521:
curve = elliptic.P521()
default:
return nil, Undef, errors.New("unsupported ECDSA curve")
}
priv, pub, err := crypto.GenerateECDSAKeyPairWithCurve(curve, rand.Reader)
if err != nil {
return nil, Undef, err
}
did, err := FromPubKey(pub)
return priv, did, err
}
// FromPrivKey is a convenience function that returns the DID associated
// with the public key associated with the provided private key.
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
return FromPubKey(privKey.GetPublic())
}
// FromPubKey returns a did:key constructed from the provided public key.
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
var code multicodec.Code
switch pubKey.Type() {
case pb.KeyType_Ed25519:
code = multicodec.Ed25519Pub
case pb.KeyType_RSA:
code = RSA
case pb.KeyType_Secp256k1:
code = Secp256k1
case pb.KeyType_ECDSA:
var err error
if code, err = codeForCurve(pubKey); err != nil {
return Undef, err
}
default:
return Undef, errors.New("unsupported key type")
}
if pubKey.Type() == pb.KeyType_ECDSA && code == Secp256k1 {
var err error
pubKey, err = coerceECDSAToSecp256k1(pubKey)
if err != nil {
return Undef, err
}
}
var bytes []byte
switch pubKey.Type() {
case pb.KeyType_ECDSA:
pkix, err := pubKey.Raw()
if err != nil {
return Undef, err
}
publicKey, err := x509.ParsePKIXPublicKey(pkix)
if err != nil {
return Undef, err
}
ecdsaPublicKey := publicKey.(*ecdsa.PublicKey)
bytes = elliptic.MarshalCompressed(ecdsaPublicKey.Curve, ecdsaPublicKey.X, ecdsaPublicKey.Y)
case pb.KeyType_Ed25519, pb.KeyType_Secp256k1:
var err error
if bytes, err = pubKey.Raw(); err != nil {
return Undef, err
}
case pb.KeyType_RSA:
var err error
pkix, err := pubKey.Raw()
if err != nil {
return Undef, err
}
publicKey, err := x509.ParsePKIXPublicKey(pkix)
if err != nil {
return Undef, err
}
bytes = x509.MarshalPKCS1PublicKey(publicKey.(*rsa.PublicKey))
}
return DID{
code: code,
bytes: string(append(varint.ToUvarint(uint64(code)), bytes...)),
}, nil
}
// ToPubKey returns the crypto.PubKey encapsulated in the DID formed by
// parsing the provided string.
func ToPubKey(s string) (crypto.PubKey, error) {
id, err := Parse(s)
if err != nil {
return nil, err
}
return id.PubKey()
}
func codeForCurve(pubKey crypto.PubKey) (multicodec.Code, error) {
stdPub, err := crypto.PubKeyToStdKey(pubKey)
if err != nil {
return multicodec.Identity, err
}
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
if !ok {
return multicodec.Identity, errors.New("failed to assert type for code to curve")
}
switch ecdsaPub.Curve {
case elliptic.P256():
return P256, nil
case elliptic.P384():
return P384, nil
case elliptic.P521():
return P521, nil
case secp256k1.S256():
return Secp256k1, nil
default:
return multicodec.Identity, fmt.Errorf("unsupported ECDSA curve: %s", ecdsaPub.Curve.Params().Name)
}
}
// secp256k1.S256 is a valid ECDSA curve, but the go-libp2p/core/crypto
// package treats it as a different type and has a different format for
// the raw bytes of the public key.
//
// If a valid ECDSA public key was created using the secp256k1.S256 curve,
// this function will "convert" it from a crypto.ECDSAPubKey to a
// crypto.Secp256k1PublicKey.
func coerceECDSAToSecp256k1(pubKey crypto.PubKey) (crypto.PubKey, error) {
stdPub, err := crypto.PubKeyToStdKey(pubKey)
if err != nil {
return nil, err
}
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("failed to assert type for secp256k1 coersion")
}
ecdsaPubBytes := append([]byte{0x04}, append(ecdsaPub.X.Bytes(), ecdsaPub.Y.Bytes()...)...)
secp256k1Pub, err := secp256k1.ParsePubKey(ecdsaPubBytes)
if err != nil {
return nil, err
}
cryptoPub := crypto.Secp256k1PublicKey(*secp256k1Pub)
return &cryptoPub, nil
}
-108
View File
@@ -1,108 +0,0 @@
package crypto_test
import (
"crypto/elliptic"
"crypto/rand"
"testing"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
libp2p_crypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/multiformats/go-multicodec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/onsonr/sonr/x/did/types/crypto"
)
const (
exampleDIDStr = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
examplePubKeyStr = "Lm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY="
)
func TestFromPubKey(t *testing.T) {
t.Parallel()
_, ecdsaP256, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(elliptic.P256(), rand.Reader)
require.NoError(t, err)
_, ecdsaP384, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(elliptic.P384(), rand.Reader)
require.NoError(t, err)
_, ecdsaP521, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(elliptic.P521(), rand.Reader)
require.NoError(t, err)
_, ecdsaSecp256k1, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(secp256k1.S256(), rand.Reader)
require.NoError(t, err)
_, ed25519, err := libp2p_crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
_, rsa, err := libp2p_crypto.GenerateRSAKeyPair(2048, rand.Reader)
require.NoError(t, err)
_, secp256k1PubKey1, err := libp2p_crypto.GenerateSecp256k1Key(rand.Reader)
require.NoError(t, err)
test := func(pub libp2p_crypto.PubKey, code multicodec.Code) func(t *testing.T) {
t.Helper()
return func(t *testing.T) {
t.Parallel()
id, err := crypto.FromPubKey(pub)
require.NoError(t, err)
p, err := id.PubKey()
require.NoError(t, err)
assert.Equal(t, pub, p)
}
}
t.Run("ECDSA with P256 curve", test(ecdsaP256, crypto.P256))
t.Run("ECDSA with P384 curve", test(ecdsaP384, crypto.P384))
t.Run("ECDSA with P521 curve", test(ecdsaP521, crypto.P521))
t.Run("Ed25519", test(ed25519, crypto.Ed25519))
t.Run("RSA", test(rsa, crypto.RSA))
t.Run("secp256k1", test(secp256k1PubKey1, crypto.Secp256k1))
t.Run("ECDSA with secp256k1 curve (coerced)", func(t *testing.T) {
t.Parallel()
id, err := crypto.FromPubKey(ecdsaSecp256k1)
require.NoError(t, err)
p, err := id.PubKey()
require.NoError(t, err)
require.Equal(t, pb.KeyType_Secp256k1, p.Type())
})
t.Run("unmarshaled example key (secp256k1)", func(t *testing.T) {
t.Parallel()
id, err := crypto.FromPubKey(examplePubKey(t))
require.NoError(t, err)
require.Equal(t, exampleDID(t), id)
})
}
func TestToPubKey(t *testing.T) {
t.Parallel()
pubKey, err := crypto.ToPubKey(exampleDIDStr)
require.NoError(t, err)
require.Equal(t, examplePubKey(t), pubKey)
}
func exampleDID(t *testing.T) crypto.DID {
t.Helper()
id, err := crypto.Parse(exampleDIDStr)
require.NoError(t, err)
return id
}
func examplePubKey(t *testing.T) libp2p_crypto.PubKey {
t.Helper()
pubKeyCfg, err := libp2p_crypto.ConfigDecodeKey(examplePubKeyStr)
require.NoError(t, err)
pubKey, err := libp2p_crypto.UnmarshalEd25519PublicKey(pubKeyCfg)
require.NoError(t, err)
return pubKey
}
-140
View File
@@ -1,140 +0,0 @@
package crypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"fmt"
"strings"
crypto "github.com/libp2p/go-libp2p/core/crypto"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-multicodec"
varint "github.com/multiformats/go-varint"
)
// Signature algorithms from the [did:key specification]
//
// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
const (
X25519 = multicodec.X25519Pub
Ed25519 = multicodec.Ed25519Pub // UCAN required/recommended
P256 = multicodec.P256Pub // UCAN required
P384 = multicodec.P384Pub
P521 = multicodec.P521Pub
Secp256k1 = multicodec.Secp256k1Pub // UCAN required
RSA = multicodec.RsaPub
)
// Undef can be used to represent a nil or undefined DID, using DID{}
// directly is also acceptable.
var Undef = DID{}
// DID is a Decentralized Identifier of the did:key type, directly holding a cryptographic public key.
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
type DID struct {
code multicodec.Code
bytes string // as string instead of []byte to allow the == operator
}
// Parse returns the DID from the string representation or an error if
// the prefix and method are incorrect, if an unknown encryption algorithm
// is specified or if the method-specific-identifier's bytes don't
// represent a public key for the specified encryption algorithm.
func Parse(str string) (DID, error) {
const keyPrefix = "did:key:"
if !strings.HasPrefix(str, keyPrefix) {
return Undef, fmt.Errorf("must start with 'did:key'")
}
baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):])
if err != nil {
return Undef, err
}
if baseCodec != mbase.Base58BTC {
return Undef, fmt.Errorf("not Base58BTC encoded")
}
code, _, err := varint.FromUvarint(bytes)
if err != nil {
return Undef, err
}
switch multicodec.Code(code) {
case Ed25519, P256, Secp256k1, RSA:
return DID{bytes: string(bytes), code: multicodec.Code(code)}, nil
default:
return Undef, fmt.Errorf("unsupported did:key multicodec: 0x%x", code)
}
}
// MustParse is like Parse but panics instead of returning an error.
func MustParse(str string) DID {
did, err := Parse(str)
if err != nil {
panic(err)
}
return did
}
// Defined tells if the DID is defined, not equal to Undef.
func (d DID) Defined() bool {
return d.code != 0 || len(d.bytes) > 0
}
// PubKey returns the public key encapsulated by the did:key.
func (d DID) PubKey() (crypto.PubKey, error) {
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
X25519: crypto.UnmarshalEd25519PublicKey,
Ed25519: crypto.UnmarshalEd25519PublicKey,
P256: ecdsaPubKeyUnmarshaler(elliptic.P256()),
P384: ecdsaPubKeyUnmarshaler(elliptic.P384()),
P521: ecdsaPubKeyUnmarshaler(elliptic.P521()),
Secp256k1: crypto.UnmarshalSecp256k1PublicKey,
RSA: rsaPubKeyUnmarshaller,
}[d.code]
if !ok {
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
}
codeSize := varint.UvarintSize(uint64(d.code))
return unmarshaler([]byte(d.bytes)[codeSize:])
}
// String formats the decentralized identity document (DID) as a string.
func (d DID) String() string {
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
return "did:key:" + key
}
func ecdsaPubKeyUnmarshaler(curve elliptic.Curve) crypto.PubKeyUnmarshaller {
return func(data []byte) (crypto.PubKey, error) {
x, y := elliptic.UnmarshalCompressed(curve, data)
ecdsaPublicKey := &ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
}
pkix, err := x509.MarshalPKIXPublicKey(ecdsaPublicKey)
if err != nil {
return nil, err
}
return crypto.UnmarshalECDSAPublicKey(pkix)
}
}
func rsaPubKeyUnmarshaller(data []byte) (crypto.PubKey, error) {
rsaPublicKey, err := x509.ParsePKCS1PublicKey(data)
if err != nil {
return nil, err
}
pkix, err := x509.MarshalPKIXPublicKey(rsaPublicKey)
if err != nil {
return nil, err
}
return crypto.UnmarshalRsaPublicKey(pkix)
}
-41
View File
@@ -1,41 +0,0 @@
package crypto
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseDIDKey(t *testing.T) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
d, err := Parse(str)
require.NoError(t, err)
require.Equal(t, str, d.String())
}
func TestMustParseDIDKey(t *testing.T) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
require.NotPanics(t, func() {
d := MustParse(str)
require.Equal(t, str, d.String())
})
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
require.Panics(t, func() {
MustParse(str)
})
}
func TestEquivalence(t *testing.T) {
undef0 := DID{}
undef1 := Undef
did0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
require.NoError(t, err)
did1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
require.NoError(t, err)
require.True(t, undef0 == undef1)
require.False(t, undef0 == did0)
require.True(t, did0 == did1)
require.False(t, undef1 == did1)
}
@@ -1,112 +0,0 @@
package accounts
import (
"context"
"errors"
"fmt"
"github.com/onsonr/sonr/internal/chain/transaction"
)
var (
errNoInitHandler = errors.New("no init handler")
errNoExecuteHandler = errors.New("account does not accept messages")
errInvalidMessage = errors.New("invalid message")
)
// NewInitBuilder creates a new InitBuilder instance.
func NewInitBuilder() *InitBuilder {
return &InitBuilder{}
}
// InitBuilder defines a smart account's initialisation handler builder.
type InitBuilder struct {
// handler is the handler function that will be called when the smart account is initialized.
// Although the function here is defined to take an any, the smart account will work
// with a typed version of it.
handler func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error)
// schema is the schema of the message that will be passed to the handler function.
schema HandlerSchema
}
// makeHandler returns the handler function that will be called when the smart account is initialized.
// It returns an error if no handler was registered.
func (i *InitBuilder) makeHandler() (func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error), error) {
if i.handler == nil {
return nil, errNoInitHandler
}
return i.handler, nil
}
// NewExecuteBuilder creates a new ExecuteBuilder instance.
func NewExecuteBuilder() *ExecuteBuilder {
return &ExecuteBuilder{
handlers: make(map[string]func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error)),
handlersSchema: make(map[string]HandlerSchema),
}
}
// ExecuteBuilder defines a smart account's execution router, it will be used to map an execution message
// to a handler function for a specific account.
type ExecuteBuilder struct {
// handlers is a map of handler functions that will be called when the smart account is executed.
handlers map[string]func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error)
// handlersSchema is a map of schemas for the messages that will be passed to the handler functions
// and the messages that will be returned by the handler functions.
handlersSchema map[string]HandlerSchema
// err is the error that occurred before building the handler function.
err error
}
func (r *ExecuteBuilder) makeHandler() (func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error), error) {
// if no handler is registered it's fine, it means the account will not be accepting execution or query messages.
if len(r.handlers) == 0 {
return func(ctx context.Context, _ transaction.Msg) (_ transaction.Msg, err error) {
return nil, errNoExecuteHandler
}, nil
}
if r.err != nil {
return nil, r.err
}
// build the real execution handler
return func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error) {
messageName := MessageName(executeRequest)
handler, ok := r.handlers[messageName]
if !ok {
return nil, fmt.Errorf("%w: no handler for message %s", errInvalidMessage, messageName)
}
return handler(ctx, executeRequest)
}, nil
}
// NewQueryBuilder creates a new QueryBuilder instance.
func NewQueryBuilder() *QueryBuilder {
return &QueryBuilder{
er: NewExecuteBuilder(),
}
}
// QueryBuilder defines a smart account's query router, it will be used to map a query message
// to a handler function for a specific account.
type QueryBuilder struct {
// er is the ExecuteBuilder, since there's no difference between the execution and query handlers API.
er *ExecuteBuilder
}
func (r *QueryBuilder) makeHandler() (func(ctx context.Context, queryRequest transaction.Msg) (queryResponse transaction.Msg, err error), error) {
return r.er.makeHandler()
}
// IsRoutingError returns true if the error is a routing error,
// which typically occurs when a message cannot be matched to a handler.
func IsRoutingError(err error) bool {
if err == nil {
return false
}
return errors.Is(err, errInvalidMessage)
}
-124
View File
@@ -1,124 +0,0 @@
package accounts
import (
"context"
"encoding/binary"
"cosmossdk.io/collections"
"cosmossdk.io/core/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/onsonr/sonr/internal/chain/transaction"
"github.com/onsonr/sonr/x/did/types/internal/prefixstore"
)
var AccountStatePrefix = collections.NewPrefix(255)
type (
ModuleExecFunc = func(ctx context.Context, sender []byte, msg transaction.Msg) (transaction.Msg, error)
ModuleQueryFunc = func(ctx context.Context, queryReq transaction.Msg) (transaction.Msg, error)
)
type contextKey struct{}
type contextValue struct {
store store.KVStore // store is the prefixed store for the account.
sender []byte // sender is the address of the entity invoking the account action.
whoami []byte // whoami is the address of the account being invoked.
funds sdk.Coins // funds reports the coins sent alongside the request.
parentContext context.Context // parentContext that was used to build the account context.
moduleExec ModuleExecFunc // moduleExec is a function that executes a module message, when the resp type is unknown.
moduleQuery ModuleQueryFunc // moduleQuery is a function that queries a module.
}
func addCtx(ctx context.Context, value contextValue) context.Context {
return context.WithValue(ctx, contextKey{}, value)
}
func getCtx(ctx context.Context) contextValue {
return ctx.Value(contextKey{}).(contextValue)
}
// MakeAccountContext creates a new account execution context given:
// storeSvc: which fetches the x/accounts module store.
// accountAddr: the address of the account being invoked, which is used to give the
// account a prefixed storage.
// sender: the address of entity invoking the account action.
// moduleExec: a function that executes a module message.
// moduleQuery: a function that queries a module.
func MakeAccountContext(
ctx context.Context,
storeSvc store.KVStoreService,
accNumber uint64,
accountAddr []byte,
sender []byte,
funds sdk.Coins,
moduleExec ModuleExecFunc,
moduleQuery ModuleQueryFunc,
) context.Context {
return addCtx(ctx, contextValue{
store: makeAccountStore(ctx, storeSvc, accNumber),
sender: sender,
whoami: accountAddr,
funds: funds,
parentContext: ctx,
moduleExec: moduleExec,
moduleQuery: moduleQuery,
})
}
func SetSender(ctx context.Context, sender []byte) context.Context {
v := getCtx(ctx)
v.sender = sender
return addCtx(v.parentContext, v)
}
// makeAccountStore creates the prefixed store for the account.
// It uses the number of the account, this gives constant size
// bytes prefixes for the account state.
func makeAccountStore(ctx context.Context, storeSvc store.KVStoreService, accNum uint64) store.KVStore {
prefix := make([]byte, 8)
binary.BigEndian.PutUint64(prefix, accNum)
return prefixstore.New(storeSvc.OpenKVStore(ctx), append(AccountStatePrefix, prefix...))
}
// ExecModule can be used to execute a message towards a module, when the response type is unknown.
func ExecModule(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) {
// get sender
v := getCtx(ctx)
resp, err := v.moduleExec(v.parentContext, v.whoami, msg)
if err != nil {
return nil, err
}
return resp, nil
}
// QueryModule can be used by an account to execute a module query.
func QueryModule(ctx context.Context, req transaction.Msg) (transaction.Msg, error) {
// we do not need to check the sender in a query because it is not a state transition.
// we also unwrap the original context.
v := getCtx(ctx)
resp, err := v.moduleQuery(v.parentContext, req)
if err != nil {
return nil, err
}
return resp, nil
}
// openKVStore returns the prefixed store for the account given the context.
func openKVStore(ctx context.Context) store.KVStore { return getCtx(ctx).store }
// Sender returns the address of the entity invoking the account action.
func Sender(ctx context.Context) []byte {
return getCtx(ctx).sender
}
// Whoami returns the address of the account being invoked.
func Whoami(ctx context.Context) []byte {
return getCtx(ctx).whoami
}
// Funds returns the funds associated with the execution context.
func Funds(ctx context.Context) sdk.Coins { return getCtx(ctx).funds }
-66
View File
@@ -1,66 +0,0 @@
package accounts
import (
"fmt"
"reflect"
"strings"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/gogoproto/proto"
"github.com/onsonr/sonr/internal/chain/transaction"
)
// ProtoMsgG is a generic interface for protobuf messages.
type ProtoMsgG[T any] interface {
*T
transaction.Msg
}
type Any = codectypes.Any
func FindMessageByName(name string) (transaction.Msg, error) {
typ := proto.MessageType(name)
if typ == nil {
return nil, fmt.Errorf("no message type found for %s", name)
}
return reflect.New(typ.Elem()).Interface().(transaction.Msg), nil
}
func MessageName(msg transaction.Msg) string {
return proto.MessageName(msg)
}
// PackAny packs a proto message into an anypb.Any.
func PackAny(msg transaction.Msg) (*Any, error) {
return codectypes.NewAnyWithValue(msg)
}
// UnpackAny unpacks an anypb.Any into a proto message.
func UnpackAny[T any, PT ProtoMsgG[T]](anyPB *Any) (PT, error) {
to := new(T)
return to, UnpackAnyTo(anyPB, PT(to))
}
func UnpackAnyTo(anyPB *Any, to transaction.Msg) error {
return proto.Unmarshal(anyPB.Value, to)
}
func UnpackAnyRaw(anyPB *Any) (proto.Message, error) {
split := strings.Split(anyPB.TypeUrl, "/")
name := split[len(split)-1]
typ := proto.MessageType(name)
if typ == nil {
return nil, fmt.Errorf("no message type found for %s", name)
}
to := reflect.New(typ.Elem()).Interface().(proto.Message)
return to, UnpackAnyTo(anyPB, to)
}
func Merge(a, b transaction.Msg) {
proto.Merge(a, b)
}
func Equal(a, b transaction.Msg) bool {
return proto.Equal(a, b)
}
@@ -1,157 +0,0 @@
package accounts
import (
"context"
"fmt"
"cosmossdk.io/collections"
"cosmossdk.io/core/address"
"github.com/cosmos/cosmos-sdk/codec"
gogoproto "github.com/cosmos/gogoproto/proto"
"github.com/onsonr/sonr/internal/chain/appmodule"
"github.com/onsonr/sonr/internal/chain/transaction"
)
// Dependencies are passed to the constructor of a smart account.
type Dependencies struct {
SchemaBuilder *collections.SchemaBuilder
AddressCodec address.Codec
Environment appmodule.Environment
LegacyStateCodec interface {
Marshal(gogoproto.Message) ([]byte, error)
Unmarshal([]byte, gogoproto.Message) error
}
}
// AccountCreatorFunc is a function that creates an account.
type AccountCreatorFunc = func(deps Dependencies) (string, Account, error)
// MakeAccountsMap creates a map of account names to account implementations
// from a list of account creator functions.
func MakeAccountsMap(
cdc codec.Codec,
addressCodec address.Codec,
env appmodule.Environment,
accounts []AccountCreatorFunc,
) (map[string]Implementation, error) {
accountsMap := make(map[string]Implementation, len(accounts))
for _, makeAccount := range accounts {
stateSchemaBuilder := collections.NewSchemaBuilderFromAccessor(openKVStore)
deps := Dependencies{
SchemaBuilder: stateSchemaBuilder,
AddressCodec: addressCodec,
Environment: env,
LegacyStateCodec: cdc,
}
name, accountInterface, err := makeAccount(deps)
if err != nil {
return nil, fmt.Errorf("failed to create account %s: %w", name, err)
}
if _, ok := accountsMap[name]; ok {
return nil, fmt.Errorf("account %s is already registered", name)
}
impl, err := newImplementation(stateSchemaBuilder, accountInterface)
if err != nil {
return nil, fmt.Errorf("failed to create implementation for account %s: %w", name, err)
}
accountsMap[name] = impl
}
return accountsMap, nil
}
// newImplementation creates a new Implementation instance given an Account implementer.
func newImplementation(schemaBuilder *collections.SchemaBuilder, account Account) (Implementation, error) {
// make init handler
ir := NewInitBuilder()
account.RegisterInitHandler(ir)
initHandler, err := ir.makeHandler()
if err != nil {
return Implementation{}, err
}
// make execute handler
er := NewExecuteBuilder()
account.RegisterExecuteHandlers(er)
executeHandler, err := er.makeHandler()
if err != nil {
return Implementation{}, err
}
// make query handler
qr := NewQueryBuilder()
account.RegisterQueryHandlers(qr)
queryHandler, err := qr.makeHandler()
if err != nil {
return Implementation{}, err
}
// build schema
schema, err := schemaBuilder.Build()
if err != nil {
return Implementation{}, err
}
return Implementation{
Init: initHandler,
Execute: executeHandler,
Query: queryHandler,
CollectionsSchema: schema,
InitHandlerSchema: ir.schema,
QueryHandlersSchema: qr.er.handlersSchema,
ExecuteHandlersSchema: er.handlersSchema,
}, nil
}
// Implementation wraps an Account implementer in order to provide a concrete
// and non-generic implementation usable by the x/accounts module.
type Implementation struct {
// Init defines the initialisation handler for the smart account.
Init func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
// Execute defines the execution handler for the smart account.
Execute func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
// Query defines the query handler for the smart account.
Query func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
// CollectionsSchema represents the state schema.
CollectionsSchema collections.Schema
// InitHandlerSchema represents the init handler schema.
InitHandlerSchema HandlerSchema
// QueryHandlersSchema is the schema of the query handlers.
QueryHandlersSchema map[string]HandlerSchema
// ExecuteHandlersSchema is the schema of the execute handlers.
ExecuteHandlersSchema map[string]HandlerSchema
}
// HasExec returns true if the account can execute the given msg.
func (i Implementation) HasExec(m transaction.Msg) bool {
_, ok := i.ExecuteHandlersSchema[MessageName(m)]
return ok
}
// HasQuery returns true if the account can execute the given request.
func (i Implementation) HasQuery(m transaction.Msg) bool {
_, ok := i.QueryHandlersSchema[MessageName(m)]
return ok
}
// HasInit returns true if the account uses the provided init message.
func (i Implementation) HasInit(m transaction.Msg) bool {
return i.InitHandlerSchema.RequestSchema.Name == MessageName(m)
}
// MessageSchema defines the schema of a message.
// A message can also define a state schema.
type MessageSchema struct {
// Name identifies the message name, this must be queryable from some reflection service.
Name string
// New is used to create a new message instance for the schema.
New func() transaction.Msg
}
// HandlerSchema defines the schema of a handler.
type HandlerSchema struct {
// RequestSchema defines the schema of the request.
RequestSchema MessageSchema
// ResponseSchema defines the schema of the response.
ResponseSchema MessageSchema
}
@@ -1,17 +0,0 @@
package accounts
// Account defines a smart account interface.
type Account interface {
// RegisterInitHandler allows the smart account to register an initialisation handler, using
// the provided InitBuilder. The handler will be called when the smart account is initialized
// (deployed).
RegisterInitHandler(builder *InitBuilder)
// RegisterExecuteHandlers allows the smart account to register execution handlers.
// The smart account might also decide to not register any execution handler.
RegisterExecuteHandlers(builder *ExecuteBuilder)
// RegisterQueryHandlers allows the smart account to register query handlers. The smart account
// might also decide to not register any query handler.
RegisterQueryHandlers(builder *QueryBuilder)
}
@@ -1,78 +0,0 @@
package accounts
import (
"context"
"fmt"
"google.golang.org/protobuf/proto"
"github.com/onsonr/sonr/internal/chain/transaction"
)
// RegisterInitHandler registers an initialisation handler for a smart account that uses protobuf.
func RegisterInitHandler[
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
](router *InitBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
) {
reqName := MessageName(ProtoReq(new(Req)))
router.handler = func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error) {
concrete, ok := initRequest.(ProtoReq)
if !ok {
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, initRequest)
}
return handler(ctx, concrete)
}
router.schema = HandlerSchema{
RequestSchema: *NewProtoMessageSchema[Req, ProtoReq](),
ResponseSchema: *NewProtoMessageSchema[Resp, ProtoResp](),
}
}
// RegisterExecuteHandler registers an execution handler for a smart account that uses protobuf.
func RegisterExecuteHandler[
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
](router *ExecuteBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
) {
reqName := MessageName(ProtoReq(new(Req)))
// check if not registered already
if _, ok := router.handlers[reqName]; ok {
router.err = fmt.Errorf("handler already registered for message %s", reqName)
return
}
router.handlers[reqName] = func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error) {
concrete, ok := executeRequest.(ProtoReq)
if !ok {
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, executeRequest)
}
return handler(ctx, concrete)
}
router.handlersSchema[reqName] = HandlerSchema{
RequestSchema: *NewProtoMessageSchema[Req, ProtoReq](),
ResponseSchema: *NewProtoMessageSchema[Resp, ProtoResp](),
}
}
// RegisterQueryHandler registers a query handler for a smart account that uses protobuf.
func RegisterQueryHandler[
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
](router *QueryBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
) {
RegisterExecuteHandler(router.er, handler)
}
func NewProtoMessageSchema[T any, PT ProtoMsgG[T]]() *MessageSchema {
msg := PT(new(T))
if _, ok := (interface{}(msg)).(proto.Message); ok {
panic("protov2 messages are not supported")
}
return &MessageSchema{
Name: MessageName(msg),
New: func() transaction.Msg {
return PT(new(T))
},
}
}
@@ -1,223 +0,0 @@
// Package prefixstore provides a store that prefixes all keys with a given
// prefix. It is used to isolate storage reads and writes for an account.
// Implementation taken from cosmossdk.io/store/prefix, and adapted to
// the cosmossdk.io/core/store.KVStore interface.
package prefixstore
import (
"bytes"
"errors"
"cosmossdk.io/core/store"
)
// New creates a new prefix store using the provided bytes prefix.
func New(store store.KVStore, prefix []byte) store.KVStore {
return Store{
parent: store,
prefix: prefix,
}
}
var _ store.KVStore = Store{}
// Store is similar with cometbft/cometbft-db/blob/v1.0.1/prefixdb.go
// both gives access only to the limited subset of the store
// for convenience or safety
type Store struct {
parent store.KVStore
prefix []byte
}
func cloneAppend(bz, tail []byte) (res []byte) {
res = make([]byte, len(bz)+len(tail))
copy(res, bz)
copy(res[len(bz):], tail)
return
}
func (s Store) key(key []byte) (res []byte) {
if key == nil {
panic("nil key on Store")
}
res = cloneAppend(s.prefix, key)
return
}
// Implements KVStore
func (s Store) Get(key []byte) ([]byte, error) {
return s.parent.Get(s.key(key))
}
// Implements KVStore
func (s Store) Has(key []byte) (bool, error) {
return s.parent.Has(s.key(key))
}
// Implements KVStore
func (s Store) Set(key, value []byte) error {
return s.parent.Set(s.key(key), value)
}
// Implements KVStore
func (s Store) Delete(key []byte) error { return s.parent.Delete(s.key(key)) }
// Implements KVStore
// Check https://github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go#L109
func (s Store) Iterator(start, end []byte) (store.Iterator, error) {
newstart := cloneAppend(s.prefix, start)
var newend []byte
if end == nil {
newend = cpIncr(s.prefix)
} else {
newend = cloneAppend(s.prefix, end)
}
iter, err := s.parent.Iterator(newstart, newend)
if err != nil {
return nil, err
}
return newPrefixIterator(s.prefix, start, end, iter), nil
}
// ReverseIterator implements KVStore
// Check https://github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go#L132
func (s Store) ReverseIterator(start, end []byte) (store.Iterator, error) {
newstart := cloneAppend(s.prefix, start)
var newend []byte
if end == nil {
newend = cpIncr(s.prefix)
} else {
newend = cloneAppend(s.prefix, end)
}
iter, err := s.parent.ReverseIterator(newstart, newend)
if err != nil {
return nil, err
}
return newPrefixIterator(s.prefix, start, end, iter), nil
}
var _ store.Iterator = (*prefixIterator)(nil)
type prefixIterator struct {
prefix []byte
start []byte
end []byte
iter store.Iterator
valid bool
}
func newPrefixIterator(prefix, start, end []byte, parent store.Iterator) *prefixIterator {
return &prefixIterator{
prefix: prefix,
start: start,
end: end,
iter: parent,
valid: parent.Valid() && bytes.HasPrefix(parent.Key(), prefix),
}
}
// Implements Iterator
func (pi *prefixIterator) Domain() ([]byte, []byte) {
return pi.start, pi.end
}
// Implements Iterator
func (pi *prefixIterator) Valid() bool {
return pi.valid && pi.iter.Valid()
}
// Implements Iterator
func (pi *prefixIterator) Next() {
if !pi.valid {
panic("prefixIterator invalid, cannot call Next()")
}
if pi.iter.Next(); !pi.iter.Valid() || !bytes.HasPrefix(pi.iter.Key(), pi.prefix) {
// TODO: shouldn't pi be set to nil instead?
pi.valid = false
}
}
// Implements Iterator
func (pi *prefixIterator) Key() (key []byte) {
if !pi.valid {
panic("prefixIterator invalid, cannot call Key()")
}
key = pi.iter.Key()
key = stripPrefix(key, pi.prefix)
return
}
// Implements Iterator
func (pi *prefixIterator) Value() []byte {
if !pi.valid {
panic("prefixIterator invalid, cannot call Value()")
}
return pi.iter.Value()
}
// Implements Iterator
func (pi *prefixIterator) Close() error {
return pi.iter.Close()
}
// Error returns an error if the prefixIterator is invalid defined by the Valid
// method.
func (pi *prefixIterator) Error() error {
if !pi.Valid() {
return errors.New("invalid prefixIterator")
}
return nil
}
// copied from github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go
func stripPrefix(key, prefix []byte) []byte {
if len(key) < len(prefix) || !bytes.Equal(key[:len(prefix)], prefix) {
panic("should not happen")
}
return key[len(prefix):]
}
// wrapping types.PrefixEndBytes
func cpIncr(bz []byte) []byte {
return prefixEndBytes(bz)
}
// prefixEndBytes returns the []byte that would end a
// range query for all []byte with a certain prefix
// Deals with last byte of prefix being FF without overflowing
func prefixEndBytes(prefix []byte) []byte {
if len(prefix) == 0 {
return nil
}
end := make([]byte, len(prefix))
copy(end, prefix)
for {
if end[len(end)-1] != byte(255) {
end[len(end)-1]++
break
}
end = end[:len(end)-1]
if len(end) == 0 {
end = nil
break
}
}
return end
}